aegiscode 3.1.8 → 3.1.10

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 (206) hide show
  1. package/README.md +23 -15
  2. package/dist/main.js +549 -552
  3. package/package.json +4 -2
  4. package/src/agent/Agent.ts +903 -0
  5. package/src/agent/SimpleAgent.ts +48 -0
  6. package/src/agent/index.ts +54 -0
  7. package/src/agent/orchestrator/AppBuilder.ts +443 -0
  8. package/src/agent/orchestrator/CouncilAgent.ts +310 -0
  9. package/src/agent/orchestrator/DiscussionRoom.ts +462 -0
  10. package/src/agent/orchestrator/OrchestratorAgent.ts +637 -0
  11. package/src/agent/orchestrator/index.ts +38 -0
  12. package/src/agent/orchestrator/utils.ts +397 -0
  13. package/src/agent/pricing.ts +115 -0
  14. package/src/agent/router.ts +74 -0
  15. package/src/agent/routerStats.ts +121 -0
  16. package/src/agent/types.ts +318 -0
  17. package/src/auth/login.ts +383 -0
  18. package/src/cli/config.ts +189 -0
  19. package/src/cli/index.ts +17 -0
  20. package/src/cli/middleware.ts +119 -0
  21. package/src/cli/types.ts +75 -0
  22. package/src/config/ConfigManager.ts +587 -0
  23. package/src/config/index.ts +7 -0
  24. package/src/config/types.ts +584 -0
  25. package/src/context/CompactionService.ts +300 -0
  26. package/src/context/ContextManager.ts +450 -0
  27. package/src/context/FileAnalyzer.ts +267 -0
  28. package/src/context/TokenCounter.ts +265 -0
  29. package/src/context/index.ts +27 -0
  30. package/src/context/storage/CacheStore.ts +176 -0
  31. package/src/context/storage/JSONLStore.ts +201 -0
  32. package/src/context/storage/MemoryStore.ts +205 -0
  33. package/src/context/storage/PersistentStore.ts +327 -0
  34. package/src/context/storage/index.ts +9 -0
  35. package/src/context/storage/pathUtils.ts +114 -0
  36. package/src/context/test.ts +309 -0
  37. package/src/context/types.ts +268 -0
  38. package/src/hooks/HookExecutor.ts +434 -0
  39. package/src/hooks/HookManager.ts +596 -0
  40. package/src/hooks/HookService.ts +269 -0
  41. package/src/hooks/Matcher.ts +157 -0
  42. package/src/hooks/index.ts +63 -0
  43. package/src/hooks/types.ts +424 -0
  44. package/src/main.tsx +596 -0
  45. package/src/mcp/HealthMonitor.ts +150 -0
  46. package/src/mcp/McpClient.ts +491 -0
  47. package/src/mcp/McpRegistry.ts +321 -0
  48. package/src/mcp/createMcpTool.ts +251 -0
  49. package/src/mcp/index.ts +15 -0
  50. package/src/mcp/server.ts +334 -0
  51. package/src/mcp/test-server.ts +88 -0
  52. package/src/mcp/test.ts +372 -0
  53. package/src/mcp/types.ts +247 -0
  54. package/src/memory/AgentMemoryBus.ts +432 -0
  55. package/src/memory/CloudSync.ts +99 -0
  56. package/src/memory/DriveSync.ts +106 -0
  57. package/src/memory/SharedMemory.ts +951 -0
  58. package/src/memory/index.ts +14 -0
  59. package/src/memory/machineFingerprint.ts +40 -0
  60. package/src/orchestrator/SubAgentMetadata.ts +136 -0
  61. package/src/prompts/builder.ts +213 -0
  62. package/src/prompts/default.ts +144 -0
  63. package/src/prompts/index.ts +16 -0
  64. package/src/prompts/plan.ts +64 -0
  65. package/src/prompts/test.ts +78 -0
  66. package/src/services/AnthropicChatService.ts +341 -0
  67. package/src/services/ChatService.ts +347 -0
  68. package/src/services/ClaudeCliChatService.ts +256 -0
  69. package/src/services/CloudSync.ts +168 -0
  70. package/src/services/CostLedger.ts +211 -0
  71. package/src/services/Heartbeat.ts +135 -0
  72. package/src/services/LearningCollector.ts +291 -0
  73. package/src/services/OllamaInstaller.ts +342 -0
  74. package/src/services/VersionChecker.ts +445 -0
  75. package/src/services/index.ts +57 -0
  76. package/src/services/streaming/OpenAIEventAdapter.ts +126 -0
  77. package/src/services/streaming/RenderingProfile.ts +90 -0
  78. package/src/services/streaming/StreamEventParser.ts +181 -0
  79. package/src/services/streaming/ThrottledRenderer.ts +139 -0
  80. package/src/services/streaming/TranscriptBuffer.ts +574 -0
  81. package/src/services/streaming/eventStatusMap.ts +52 -0
  82. package/src/services/streaming/index.ts +46 -0
  83. package/src/services/streaming/renderFormatting.ts +79 -0
  84. package/src/services/streaming/types.ts +234 -0
  85. package/src/skills/SkillLoader.ts +126 -0
  86. package/src/skills/SkillRegistry.ts +366 -0
  87. package/src/skills/index.ts +48 -0
  88. package/src/skills/types.ts +146 -0
  89. package/src/slash-commands/billing.ts +70 -0
  90. package/src/slash-commands/build.ts +413 -0
  91. package/src/slash-commands/builtinCommands.ts +2733 -0
  92. package/src/slash-commands/clone.ts +242 -0
  93. package/src/slash-commands/council.ts +125 -0
  94. package/src/slash-commands/custom/CustomCommandExecutor.ts +143 -0
  95. package/src/slash-commands/custom/CustomCommandLoader.ts +251 -0
  96. package/src/slash-commands/custom/CustomCommandRegistry.ts +144 -0
  97. package/src/slash-commands/custom/index.ts +7 -0
  98. package/src/slash-commands/debate.ts +254 -0
  99. package/src/slash-commands/gmail.ts +105 -0
  100. package/src/slash-commands/index.ts +388 -0
  101. package/src/slash-commands/mcpCommand.ts +205 -0
  102. package/src/slash-commands/types.ts +201 -0
  103. package/src/store/index.ts +76 -0
  104. package/src/store/selectors.ts +246 -0
  105. package/src/store/slices/appSlice.ts +205 -0
  106. package/src/store/slices/commandSlice.ts +115 -0
  107. package/src/store/slices/configSlice.ts +46 -0
  108. package/src/store/slices/focusSlice.ts +64 -0
  109. package/src/store/slices/index.ts +9 -0
  110. package/src/store/slices/sessionSlice.ts +424 -0
  111. package/src/store/streaming-buffer.ts +425 -0
  112. package/src/store/test.ts +296 -0
  113. package/src/store/types.ts +274 -0
  114. package/src/store/vanilla.ts +186 -0
  115. package/src/tools/builtin/bash.ts +236 -0
  116. package/src/tools/builtin/council.ts +105 -0
  117. package/src/tools/builtin/edit.ts +213 -0
  118. package/src/tools/builtin/glob.ts +136 -0
  119. package/src/tools/builtin/grep.ts +263 -0
  120. package/src/tools/builtin/index.ts +61 -0
  121. package/src/tools/builtin/memory.ts +66 -0
  122. package/src/tools/builtin/read.ts +168 -0
  123. package/src/tools/builtin/skill.ts +97 -0
  124. package/src/tools/builtin/snapshot.ts +40 -0
  125. package/src/tools/builtin/task.ts +106 -0
  126. package/src/tools/builtin/write.ts +134 -0
  127. package/src/tools/createTool.ts +221 -0
  128. package/src/tools/execution/ExecutionPipeline.ts +263 -0
  129. package/src/tools/execution/index.ts +40 -0
  130. package/src/tools/execution/stages/CacheStage.ts +131 -0
  131. package/src/tools/execution/stages/ConfirmationStage.ts +203 -0
  132. package/src/tools/execution/stages/DiscoveryStage.ts +46 -0
  133. package/src/tools/execution/stages/ExecutionStage.ts +48 -0
  134. package/src/tools/execution/stages/FormattingStage.ts +44 -0
  135. package/src/tools/execution/stages/HookStage.ts +72 -0
  136. package/src/tools/execution/stages/PermissionStage.ts +287 -0
  137. package/src/tools/execution/stages/PostHookStage.ts +71 -0
  138. package/src/tools/execution/stages/index.ts +12 -0
  139. package/src/tools/execution/test.ts +266 -0
  140. package/src/tools/execution/types.ts +273 -0
  141. package/src/tools/index.ts +81 -0
  142. package/src/tools/registry.ts +304 -0
  143. package/src/tools/schemas.ts +109 -0
  144. package/src/tools/test.ts +220 -0
  145. package/src/tools/types.ts +175 -0
  146. package/src/tools/validation/PermissionChecker.ts +242 -0
  147. package/src/tools/validation/SensitiveFileDetector.ts +210 -0
  148. package/src/tools/validation/index.ts +11 -0
  149. package/src/ui/App.tsx +166 -0
  150. package/src/ui/components/AegisInterface.tsx +484 -0
  151. package/src/ui/components/common/ChatSearch.tsx +150 -0
  152. package/src/ui/components/common/ErrorBoundary.tsx +82 -0
  153. package/src/ui/components/common/ExitMessage.tsx +120 -0
  154. package/src/ui/components/common/LoadingIndicator.tsx +49 -0
  155. package/src/ui/components/common/index.ts +6 -0
  156. package/src/ui/components/dialog/ConfirmationPrompt.tsx +208 -0
  157. package/src/ui/components/dialog/InteractiveSelector.tsx +149 -0
  158. package/src/ui/components/dialog/SetupWizard.tsx +297 -0
  159. package/src/ui/components/dialog/UpdatePrompt.tsx +155 -0
  160. package/src/ui/components/dialog/index.ts +8 -0
  161. package/src/ui/components/index.ts +28 -0
  162. package/src/ui/components/input/CommandSuggestions.tsx +139 -0
  163. package/src/ui/components/input/CustomTextInput.tsx +220 -0
  164. package/src/ui/components/input/InputArea.tsx +361 -0
  165. package/src/ui/components/input/PromptSuggestions.tsx +66 -0
  166. package/src/ui/components/input/index.ts +6 -0
  167. package/src/ui/components/layout/ChatStatusBar.tsx +90 -0
  168. package/src/ui/components/layout/ContextBar.tsx +79 -0
  169. package/src/ui/components/layout/MessageArea.tsx +96 -0
  170. package/src/ui/components/layout/MessageList.tsx +647 -0
  171. package/src/ui/components/layout/MessageSeparator.tsx +26 -0
  172. package/src/ui/components/layout/WelcomeMessage.tsx +93 -0
  173. package/src/ui/components/layout/index.ts +7 -0
  174. package/src/ui/components/markdown/CodeHighlighter.tsx +292 -0
  175. package/src/ui/components/markdown/MessageRenderer.tsx +1211 -0
  176. package/src/ui/components/markdown/index.ts +8 -0
  177. package/src/ui/components/markdown/parser.ts +336 -0
  178. package/src/ui/components/markdown/types.ts +66 -0
  179. package/src/ui/focus/FocusManager.ts +137 -0
  180. package/src/ui/focus/index.ts +13 -0
  181. package/src/ui/focus/types.ts +54 -0
  182. package/src/ui/focus/useFocus.ts +75 -0
  183. package/src/ui/hooks/index.ts +11 -0
  184. package/src/ui/hooks/useAgent.ts +284 -0
  185. package/src/ui/hooks/useCommandHistory.ts +87 -0
  186. package/src/ui/hooks/useCommandProcessor.ts +443 -0
  187. package/src/ui/hooks/useConfirmation.ts +99 -0
  188. package/src/ui/hooks/useCtrlCHandler.ts +100 -0
  189. package/src/ui/hooks/useInputBuffer.ts +122 -0
  190. package/src/ui/hooks/useTerminalSize.ts +68 -0
  191. package/src/ui/hooks/useTerminalWidth.ts +5 -0
  192. package/src/ui/hooks/useWindowedList.ts +118 -0
  193. package/src/ui/render-debugger.ts +621 -0
  194. package/src/ui/test.ts +189 -0
  195. package/src/ui/themes/ThemeManager.ts +332 -0
  196. package/src/ui/themes/aegisTheme.ts +87 -0
  197. package/src/ui/themes/darkTheme.ts +87 -0
  198. package/src/ui/themes/defaultTheme.ts +85 -0
  199. package/src/ui/themes/index.ts +10 -0
  200. package/src/ui/themes/lightTheme.ts +89 -0
  201. package/src/ui/themes/popularThemes.ts +187 -0
  202. package/src/ui/themes/types.ts +130 -0
  203. package/src/utils/clipboard.ts +48 -0
  204. package/src/utils/debug.ts +43 -0
  205. package/src/utils/environment.ts +68 -0
  206. package/src/utils/index.ts +10 -0
@@ -0,0 +1,621 @@
1
+ /**
2
+ * AEGISCode Rendering Debugger
3
+ *
4
+ * Injects probes into the rendering pipeline to detect:
5
+ * 1. Re-render storms (>10 renders/sec on any component)
6
+ * 2. Cascading re-renders (parent triggers child unnecessarily)
7
+ * 3. Stale closures / missing deps (state change without re-render)
8
+ * 4. Infinite render loops (render → state → render without RAF break)
9
+ * 5. Streaming render latency (time between store flush and DOM update)
10
+ * 6. Memo comparator regressions (skipped re-render when content changed)
11
+ * 7. Zustand subscription leaks (subscriptions not cleaned up)
12
+ *
13
+ * Usage:
14
+ * import { startRenderDebugger, stopRenderDebugger } from './render-debugger.js'
15
+ * startRenderDebugger()
16
+ *
17
+ * Or via CLI:
18
+ * aegis --debug-rendering
19
+ */
20
+
21
+ // ============ Types ============
22
+
23
+ interface RenderEvent {
24
+ type: 'mount' | 'update' | 'unmount' | 'state-change' | 'store-sub' | 'raf-poll'
25
+ component: string
26
+ timestamp: number
27
+ duration?: number // ms since last render of same component
28
+ prevProps?: string // JSON-hash of previous props
29
+ nextProps?: string // JSON-hash of current props
30
+ stack?: string // limited call stack
31
+ messageId?: string // for streaming messages
32
+ contentLen?: number // content length delta
33
+ storeAction?: string // which store action triggered this
34
+ }
35
+
36
+ interface ComponentStats {
37
+ renderCount: number
38
+ lastRender: number
39
+ minInterval: number
40
+ maxInterval: number
41
+ totalTime: number // ms since first render
42
+ warnings: string[]
43
+ }
44
+
45
+ interface DebuggerState {
46
+ enabled: boolean
47
+ events: RenderEvent[]
48
+ componentStats: Map<string, ComponentStats>
49
+ intervalId: ReturnType<typeof setInterval> | null
50
+ originalRaf: typeof requestAnimationFrame
51
+ originalSetState: any
52
+ componentRenderCounts: Map<string, number>
53
+ history: RenderEvent[]
54
+ }
55
+
56
+ // ============ Globals ============
57
+
58
+ const RENDER_STORM_THRESHOLD = 10 // renders/sec
59
+ const CASCADE_THRESHOLD = 50 // ms between parent→child renders
60
+ const MAX_EVENTS = 1000
61
+ const REPORT_INTERVAL = 3000 // ms between auto-reports
62
+
63
+ const state: DebuggerState = {
64
+ enabled: false,
65
+ events: [],
66
+ componentStats: new Map(),
67
+ intervalId: null,
68
+ originalRaf: globalThis.requestAnimationFrame,
69
+ originalSetState: null,
70
+ componentRenderCounts: new Map(),
71
+ history: [],
72
+ }
73
+
74
+ // ============ Helpers ============
75
+
76
+ function componentName(element: any): string {
77
+ if (!element) return '<unknown>'
78
+ if (typeof element === 'string') return element
79
+ if (element.displayName) return element.displayName
80
+ if (element.name) return element.name
81
+ if (element.type) {
82
+ if (typeof element.type === 'string') return element.type
83
+ return element.type.displayName || element.type.name || '<anonymous>'
84
+ }
85
+ return '<unknown>'
86
+ }
87
+
88
+ function hashProps(props: Record<string, any>): string {
89
+ const keys = Object.keys(props).sort()
90
+ const parts = keys.map(k => {
91
+ const v = props[k]
92
+ if (typeof v === 'function') return `${k}:fn`
93
+ if (typeof v === 'object' && v !== null) return `${k}:obj`
94
+ return `${k}:${String(v).slice(0, 50)}`
95
+ })
96
+ return parts.join('|')
97
+ }
98
+
99
+ function getCallStack(limit = 3): string {
100
+ const err = new Error()
101
+ const stack = err.stack?.split('\n').slice(3, 3 + limit) || []
102
+ return stack.map(s => s.trim()).join(' ← ')
103
+ }
104
+
105
+ // ============ Component Render Hooks ============
106
+
107
+ /**
108
+ * Patch React.createElement to track component renders.
109
+ * We wrap the original to count renders per component type.
110
+ */
111
+ function patchReactCreateElement(React: any): () => void {
112
+ if (!React || !React.createElement) return () => {}
113
+
114
+ const originalCreateElement = React.createElement
115
+
116
+ React.createElement = function patchedCreateElement(type: any, props: any, ...children: any[]) {
117
+ if (state.enabled) {
118
+ const name = componentName({ type })
119
+ const count = state.componentRenderCounts.get(name) || 0
120
+ state.componentRenderCounts.set(name, count + 1)
121
+
122
+ const event: RenderEvent = {
123
+ type: count === 0 ? 'mount' : 'update',
124
+ component: name,
125
+ timestamp: performance.now(),
126
+ stack: getCallStack(2),
127
+ nextProps: props ? hashProps(props) : '{}',
128
+ }
129
+ state.events.push(event)
130
+ if (state.events.length > MAX_EVENTS) state.events.shift()
131
+ }
132
+ return originalCreateElement.call(React, type, props, ...children)
133
+ }
134
+
135
+ return () => {
136
+ React.createElement = originalCreateElement
137
+ }
138
+ }
139
+
140
+ // ============ Store Subscription Monitor ============
141
+
142
+ let storeUnsubPatched = false
143
+
144
+ /**
145
+ * Patch zustand subscribe to detect subscription leaks
146
+ * and log store action → render causality.
147
+ */
148
+ function patchStoreSubscribe(store: any): () => void {
149
+ if (!store || storeUnsubPatched) return () => {}
150
+ storeUnsubPatched = true
151
+
152
+ const origSubscribe = store.subscribe.bind(store)
153
+
154
+ const wrappedSubscribe = (selector: any, callback?: any) => {
155
+ const wrappedCallback = callback
156
+ ? (state: any, prevState?: any) => {
157
+ if (state.enabled) {
158
+ state.events.push({
159
+ type: 'store-sub',
160
+ component: 'store',
161
+ timestamp: performance.now(),
162
+ storeAction: 'subscribe-callback fired',
163
+ })
164
+ }
165
+ callback(state, prevState)
166
+ }
167
+ : selector
168
+
169
+ const unsub = origSubscribe(selector, wrappedCallback)
170
+ return unsub
171
+ }
172
+
173
+ store.subscribe = wrappedSubscribe
174
+
175
+ return () => {
176
+ store.subscribe = origSubscribe
177
+ storeUnsubPatched = false
178
+ }
179
+ }
180
+
181
+ // ============ RAF Monitor ============
182
+
183
+ let rafCallCount = 0
184
+ let rafResetTimer: ReturnType<typeof setTimeout> | null = null
185
+
186
+ function patchRAF(): () => void {
187
+ const originalRaf = globalThis.requestAnimationFrame
188
+ const originalCaf = globalThis.cancelAnimationFrame
189
+
190
+ const activeRafs = new Set<number>()
191
+
192
+ ;(globalThis as any).requestAnimationFrame = (cb: FrameRequestCallback): number => {
193
+ rafCallCount++
194
+ if (rafResetTimer) clearTimeout(rafResetTimer)
195
+ rafResetTimer = setTimeout(() => { rafCallCount = 0 }, 1000)
196
+
197
+ const wrapped: FrameRequestCallback = (time) => {
198
+ activeRafs.delete(id)
199
+ cb(time)
200
+ }
201
+ const id = originalRaf.call(globalThis, wrapped)
202
+ activeRafs.add(id)
203
+ return id
204
+ }
205
+
206
+ ;(globalThis as any).cancelAnimationFrame = (id: number) => {
207
+ activeRafs.delete(id)
208
+ originalCaf.call(globalThis, id)
209
+ }
210
+
211
+ return () => {
212
+ ;(globalThis as any).requestAnimationFrame = originalRaf
213
+ ;(globalThis as any).cancelAnimationFrame = originalCaf
214
+ activeRafs.forEach(id => originalCaf.call(globalThis, id))
215
+ activeRafs.clear()
216
+ }
217
+ }
218
+
219
+ // ============ Console Monitoring ============
220
+
221
+ function patchConsole(): () => void {
222
+ const origWarn = console.warn
223
+ const origError = console.error
224
+
225
+ ;(console as any).warn = (...args: any[]) => {
226
+ const msg = args.join(' ')
227
+ if (
228
+ msg.includes('maximum update depth') ||
229
+ msg.includes('React has detected') ||
230
+ msg.includes('Cannot update during') ||
231
+ msg.includes('render') && msg.includes('state')
232
+ ) {
233
+ state.events.push({
234
+ type: 'update',
235
+ component: '[React Warning]',
236
+ timestamp: performance.now(),
237
+ nextProps: msg.slice(0, 200),
238
+ stack: getCallStack(5),
239
+ })
240
+ }
241
+ return origWarn.apply(console, args)
242
+ }
243
+
244
+ ;(console as any).error = (...args: any[]) => {
245
+ const msg = args.join(' ')
246
+ if (
247
+ msg.includes('Minified React error') ||
248
+ msg.includes('Rendered more hooks') ||
249
+ msg.includes('Rendered fewer hooks')
250
+ ) {
251
+ state.events.push({
252
+ type: 'update',
253
+ component: '[React Error]',
254
+ timestamp: performance.now(),
255
+ nextProps: msg.slice(0, 200),
256
+ stack: getCallStack(5),
257
+ })
258
+ }
259
+ return origError.apply(console, args)
260
+ }
261
+
262
+ return () => {
263
+ console.warn = origWarn
264
+ console.error = origError
265
+ }
266
+ }
267
+
268
+ // ============ Analysis ============
269
+
270
+ function analyzeRenderEvents(): {
271
+ storms: string[]
272
+ cascades: string[]
273
+ loops: boolean
274
+ streamingIssues: string[]
275
+ summary: Record<string, number>
276
+ } {
277
+ const result = {
278
+ storms: [] as string[],
279
+ cascades: [] as string[],
280
+ loops: false,
281
+ streamingIssues: [] as string[],
282
+ summary: {} as Record<string, number>,
283
+ }
284
+
285
+ const perComponent = new Map<string, RenderEvent[]>()
286
+ for (const ev of state.events) {
287
+ if (ev.type === 'mount' || ev.type === 'update') {
288
+ const list = perComponent.get(ev.component) || []
289
+ list.push(ev)
290
+ perComponent.set(ev.component, list)
291
+ }
292
+ }
293
+
294
+ // 1. Re-render storms
295
+ for (const [name, events] of Array.from(perComponent.entries())) {
296
+ if (events.length < 5) continue
297
+
298
+ // Check any 1-second window
299
+ for (let i = 0; i < events.length - RENDER_STORM_THRESHOLD; i++) {
300
+ const windowEnd = events[i].timestamp + 1000
301
+ const count = events.filter(e => e.timestamp >= events[i].timestamp && e.timestamp <= windowEnd).length
302
+ if (count >= RENDER_STORM_THRESHOLD) {
303
+ result.storms.push(`${name}: ${count} renders within 1s window (threshold: ${RENDER_STORM_THRESHOLD})`)
304
+ break
305
+ }
306
+ }
307
+ }
308
+
309
+ // 2. Cascading re-renders (rapid parent→child chain)
310
+ const allEvents = state.events.filter(e => e.type === 'mount' || e.type === 'update')
311
+ for (let i = 1; i < allEvents.length; i++) {
312
+ const interval = allEvents[i].timestamp - allEvents[i - 1].timestamp
313
+ if (interval < CASCADE_THRESHOLD && allEvents[i].component !== allEvents[i - 1].component) {
314
+ result.cascades.push(
315
+ `${allEvents[i - 1].component} → ${allEvents[i].component} (${interval.toFixed(1)}ms)`
316
+ )
317
+ }
318
+ }
319
+
320
+ // 3. Infinite render loop detection (same component in rapid succession)
321
+ let loopStreak = 0
322
+ for (let i = 1; i < allEvents.length; i++) {
323
+ if (
324
+ allEvents[i].component === allEvents[i - 1].component &&
325
+ allEvents[i].timestamp - allEvents[i - 1].timestamp < 16 // < 1 frame
326
+ ) {
327
+ loopStreak++
328
+ if (loopStreak >= 5) {
329
+ result.loops = true
330
+ result.storms.push(`${allEvents[i].component}: POSSIBLE INFINITE LOOP (${loopStreak} renders in <16ms each)`)
331
+ break
332
+ }
333
+ } else {
334
+ loopStreak = 0
335
+ }
336
+ }
337
+
338
+ // 4. Streaming latency
339
+ const subEvents = state.events.filter(e => e.type === 'store-sub' && e.timestamp > 0)
340
+ const renderEvents = state.events.filter(e => e.type === 'update' && e.component === 'MessageRenderer')
341
+ if (subEvents.length > 0 && renderEvents.length > 0) {
342
+ const avg = renderEvents.length / (subEvents.length || 1)
343
+ if (avg < 0.5) {
344
+ result.streamingIssues.push(`Store subscriptions (${subEvents.length}) vs renders (${renderEvents.length}): possible skipped updates (ratio: ${avg.toFixed(2)})`)
345
+ }
346
+ }
347
+
348
+ // RAF call rate
349
+ if (rafCallCount > 60) {
350
+ result.streamingIssues.push(`High RAF call rate: ${rafCallCount}/sec (should be ~60 max)`)
351
+ }
352
+
353
+ // Summary
354
+ for (const [name, events] of Array.from(perComponent.entries())) {
355
+ result.summary[name] = events.length
356
+ }
357
+
358
+ // Deduplicate using forEach
359
+ const stormSet = new Set<string>();
360
+ result.storms.forEach(s => stormSet.add(s));
361
+ result.storms = Array.from(stormSet);
362
+ result.cascades = result.cascades.slice(0, 20)
363
+
364
+ return result
365
+ }
366
+
367
+ // ============ Reporting ============
368
+
369
+ function formatDuration(ms: number): string {
370
+ if (ms < 1) return `${(ms * 1000).toFixed(0)}μs`
371
+ if (ms < 1000) return `${ms.toFixed(1)}ms`
372
+ return `${(ms / 1000).toFixed(1)}s`
373
+ }
374
+
375
+ function generateReport(): string {
376
+ const analysis = analyzeRenderEvents()
377
+ const lines: string[] = []
378
+ const now = new Date().toISOString()
379
+
380
+ lines.push(`\n${'═'.repeat(60)}`)
381
+ lines.push(` AEGISCode Rendering Report — ${now}`)
382
+ lines.push(`${'═'.repeat(60)}`)
383
+
384
+ // Summary
385
+ lines.push(`\n Total render events tracked: ${state.events.length}`)
386
+ lines.push(` Components tracked: ${Object.keys(analysis.summary).length}`)
387
+
388
+ lines.push(`\n ── Render counts per component ──`)
389
+ const sorted = Object.entries(analysis.summary).sort((a, b) => b[1] - a[1])
390
+ for (const [name, count] of sorted.slice(0, 15)) {
391
+ lines.push(` ${name.padEnd(30)} ${String(count).padStart(5)} renders`)
392
+ }
393
+
394
+ // Storms
395
+ if (analysis.storms.length > 0) {
396
+ lines.push(`\n ── ⚠ RENDER STORMS ──`)
397
+ for (const storm of analysis.storms) {
398
+ lines.push(` šŸ”“ ${storm}`)
399
+ }
400
+ }
401
+
402
+ // Cascades
403
+ if (analysis.cascades.length > 0) {
404
+ lines.push(`\n ── Re-render Cascades (top 10) ──`)
405
+ for (const cascade of analysis.cascades.slice(0, 10)) {
406
+ lines.push(` 🟔 ${cascade}`)
407
+ }
408
+ }
409
+
410
+ // Infinite loops
411
+ if (analysis.loops) {
412
+ lines.push(`\n ── šŸ”“ INFINITE RENDER LOOP DETECTED ──`)
413
+ lines.push(` Immediate action required: check state updates in render cycle`)
414
+ }
415
+
416
+ // Streaming issues
417
+ if (analysis.streamingIssues.length > 0) {
418
+ lines.push(`\n ── Streaming Issues ──`)
419
+ for (const issue of analysis.streamingIssues) {
420
+ lines.push(` 🟠 ${issue}`)
421
+ }
422
+ }
423
+
424
+ // Healthy
425
+ if (
426
+ analysis.storms.length === 0 &&
427
+ analysis.cascades.length === 0 &&
428
+ !analysis.loops &&
429
+ analysis.streamingIssues.length === 0
430
+ ) {
431
+ lines.push(`\n āœ… No rendering problems detected.`)
432
+ }
433
+
434
+ // Recommendations
435
+ if (
436
+ analysis.storms.length > 0 ||
437
+ analysis.cascades.length > 0 ||
438
+ analysis.loops ||
439
+ analysis.streamingIssues.length > 0
440
+ ) {
441
+ lines.push(`\n ── Recommendations ──`)
442
+
443
+ if (analysis.loops) {
444
+ lines.push(` 1. Break render→setState→render cycle with useRef or useEffect`)
445
+ }
446
+ if (analysis.storms.length > 0) {
447
+ lines.push(` 2. Add React.memo() or refine memo comparator on storm components`)
448
+ lines.push(` 3. Batch store updates: use flush() pattern (already in AegisInterface)`)
449
+ }
450
+ if (analysis.cascades.length > 0) {
451
+ lines.push(` 4. Lift state up or use useContext with stable references`)
452
+ lines.push(` 5. Check useCallback/useMemo deps on parent components`)
453
+ }
454
+ if (analysis.streamingIssues.length > 0) {
455
+ lines.push(` 6. Ensure RAF poll catches content length changes (MessageList)`)
456
+ lines.push(` 7. Verify MessageRenderer comparator allows streaming updates`)
457
+ }
458
+ }
459
+
460
+ lines.push(`${'═'.repeat(60)}\n`)
461
+
462
+ return lines.join('\n')
463
+ }
464
+
465
+ function autoReport() {
466
+ if (!state.enabled) return
467
+ const report = generateReport()
468
+ console.log(report)
469
+ }
470
+
471
+ // ============ Public API ============
472
+
473
+ /**
474
+ * Start the rendering debugger.
475
+ * Patches React.createElement, RAF, console, and zustand store.
476
+ * Begins periodic reporting of rendering health.
477
+ *
478
+ * @param options.reportInterval - ms between auto-reports (default 3000)
479
+ * @param options.verbose - console.log every render event (default false)
480
+ */
481
+ export async function startRenderDebugger(options?: { reportInterval?: number; verbose?: boolean }): Promise<void> {
482
+ if (state.enabled) {
483
+ console.log('[RenderDebugger] Already running')
484
+ return
485
+ }
486
+
487
+ state.enabled = true
488
+ const interval = options?.reportInterval || REPORT_INTERVAL
489
+ const verbose = options?.verbose || false
490
+
491
+ console.log(`\nšŸ” AEGISCode Rendering Debugger started (report interval: ${interval}ms)`)
492
+ if (verbose) console.log(' Verbose mode: every render event will be logged')
493
+
494
+ // Patch RAF
495
+ const unpatches: (() => void)[] = []
496
+ unpatches.push(patchRAF())
497
+
498
+ // Patch console for React warnings
499
+ unpatches.push(patchConsole())
500
+
501
+ // Try to patch React if available
502
+ try {
503
+ const React = await import('react')
504
+ unpatches.push(patchReactCreateElement(React.default || React))
505
+ } catch {
506
+ // React not loaded yet; try again after a tick
507
+ setTimeout(async () => {
508
+ try {
509
+ const React = await import('react')
510
+ unpatches.push(patchReactCreateElement(React.default || React))
511
+ } catch {}
512
+ }, 100)
513
+ }
514
+
515
+ // Auto-reporting interval
516
+ state.intervalId = setInterval(() => {
517
+ autoReport()
518
+ }, interval)
519
+
520
+ // Store reference for cleanup
521
+ ;(globalThis as any).__RENDER_DEBUGGER__ = {
522
+ state,
523
+ unpatches,
524
+ stop: stopRenderDebugger,
525
+ report: generateReport,
526
+ }
527
+
528
+ if (!verbose) {
529
+ // In non-verbose mode, suppress individual render event logging
530
+ const origPush = state.events.push.bind(state.events)
531
+ state.events.push = (...items: RenderEvent[]): number => {
532
+ // Only log warnings to console
533
+ for (const item of items) {
534
+ if (item.stack?.includes('Warning') || item.component.startsWith('[React')) {
535
+ console.log(`[RenderDebugger] ${item.component}: ${item.nextProps?.slice(0, 80)}`)
536
+ }
537
+ }
538
+ return origPush(...items)
539
+ }
540
+ }
541
+ }
542
+
543
+ /**
544
+ * Stop the rendering debugger and generate a final report.
545
+ */
546
+ export function stopRenderDebugger(): string {
547
+ if (!state.enabled) {
548
+ return 'Render debugger was not running.'
549
+ }
550
+
551
+ state.enabled = false
552
+
553
+ // Restore patches
554
+ const debug = (globalThis as any).__RENDER_DEBUGGER__
555
+ if (debug?.unpatches) {
556
+ for (const unpatch of debug.unpatches) {
557
+ try { unpatch() } catch {}
558
+ }
559
+ }
560
+
561
+ // Clear interval
562
+ if (state.intervalId) {
563
+ clearInterval(state.intervalId)
564
+ state.intervalId = null
565
+ }
566
+
567
+ delete (globalThis as any).__RENDER_DEBUGGER__
568
+
569
+ const finalReport = generateReport()
570
+ console.log(finalReport)
571
+
572
+ console.log('šŸ“Š Render Debugger stopped.')
573
+ console.log(` Events captured: ${state.events.length}`)
574
+
575
+ // Reset state
576
+ state.events = []
577
+ state.componentStats.clear()
578
+ state.componentRenderCounts.clear()
579
+
580
+ return finalReport
581
+ }
582
+
583
+ /**
584
+ * Get the current analysis without stopping the debugger.
585
+ */
586
+ export function getRenderReport(): string {
587
+ return generateReport()
588
+ }
589
+
590
+ /**
591
+ * Get raw render events for programmatic analysis.
592
+ */
593
+ export function getRenderEvents(): RenderEvent[] {
594
+ return [...state.events]
595
+ }
596
+
597
+ /**
598
+ * Get the top N most frequently rendered components.
599
+ */
600
+ export function getHottestComponents(n = 10): Array<{ name: string; count: number }> {
601
+ const counts: Record<string, number> = {}
602
+ for (const ev of state.events) {
603
+ if (ev.type === 'mount' || ev.type === 'update') {
604
+ counts[ev.component] = (counts[ev.component] || 0) + 1
605
+ }
606
+ }
607
+ return Object.entries(counts)
608
+ .sort((a, b) => b[1] - a[1])
609
+ .slice(0, n)
610
+ .map(([name, count]) => ({ name, count }))
611
+ }
612
+
613
+ // ============ Auto-start via CLI flag ============
614
+
615
+ // Check for --debug-rendering flag
616
+ const debugRenderingArg = process.argv.includes('--debug-rendering')
617
+ if (debugRenderingArg) {
618
+ setTimeout(() => startRenderDebugger({ verbose: false }), 0)
619
+ }
620
+
621
+ export default { startRenderDebugger, stopRenderDebugger, getRenderReport }