Conversation
These serve as the type definitions for interacting with the `chrome-devtools-mcp` AI runtime debugging functionality. Eventually this will hopefully be upstreamed to some more authoritative location, but for now this will do.
This provides an `angular:signal_graph` in-page tool which exposes the signal graph from the component rendered for a particular DOM element. It leverages the algorithm defined for Angular DevTools, which essentially means it takes the effects registered on the components injector and walks transitive dependencies to find all signals referenced by the component in an effect or the template.
This will centrally manage all the AI tools supported by Angular out of the box.
This registers AI runtime debugging tools during platform creation and unregisters them when the platform is destroyed. This roughly matches existing usage of global utils with respect to timing. It is limited to dev mode only because these tools are exclusively for debugging Angular's internals and not something production users would leverage.
This walks all transitive descendant directives via the `LView` structure of the given input. This is a generic utility, but useful for finding all components in a tree to look for their associated `Injector` objects. One known limitation is that this does not cover child components of i18n messages as that was more complicated than I wanted to get into right now.
4d3caa6 to
9a835d6
Compare
This creates a new `angular:di-graph` in-page tool which returns the entire dependency injection graph for the application. We use the following rough algorithm for discovering all element injectors: 1. Find all root `LView` objects by querying for `[ng-version]`. 2. Walk all the transitive `LView` descendants of the roots. 3. Filter these `LView` objects to just directives. 4. Find the injector for a given directive and walk up its ancestors to find all element injectors. Discovering environment injectors works mostly the same way, just following the environment injector graph instead. This approach has a few known limitations which are out of scope for the moment: 1. Any given component typically has both an element injector *and* an environment injector. The relationship of "component -> environment injector" is not expressed in the result as of now, meaning the AI doesn't really have any insight into _which_ environment injector is being used for a particular component, though the injector will be one of the returned values. 2. The implementation does not support MFE use cases of multiple applications on the page at the same time. 3. The performance is not ideal, as we walk `LView` descendants twice and walk up the injector tree for every directive, repeatedly covering the same scope (ideally we'd just walk up every *leaf* directive, which would cover the same result for less effort). However for a debug tool, this is likely fine for now and we can optimize later if/when it becomes necessary. I did consider reusing more of the existing implementation in `global_utils` which exists to support Angular DevTools (we are already using some of it), however the existing support in `@angular/core` is actually fairly limited, returning very primitive data structures and relying on Angular DevTools to do the heavier lifting of collapsing the code into a usable graph representation. There's a potential path in the future to converge these implementations and potentially have `global_utils` use some of this code instead, but I will leave that for a future cleanup effort.
| - \`name\`: The identifier for the environment injector. | ||
| - \`type\`: 'environment' or 'null'. | ||
| - \`providers\`: Array of providers configured on that injector. | ||
| - \`children\`: Array of child environment injectors. |
There was a problem hiding this comment.
Is it worth having the same token and value list items here as well?
I guess that the agent will be able to extrapolate, but still.
| return { | ||
| token: record.token, | ||
| value: injector.get(record.token, null, {optional: true, self: true}), | ||
| }; |
There was a problem hiding this comment.
Shouldn't we convert the token to a string?
On that note, I guess there isn't much that we can do about the values. It's interesting how they will be handled by the LLMs.
| /** | ||
| * Gets a human-readable name for a DI token. | ||
| */ | ||
| export function getTokenName(token: any): string { |
There was a problem hiding this comment.
In relation to my previous comment: this doesn't seem to be used.
hawkgs
left a comment
There was a problem hiding this comment.
LGTM 👍
Given the size of the PR and the nature of the change, especially the Ivy utils, it's good to have a second pair of eyes. My review was accompanied by a fair share of learning moments.
| // Only attempt to get providers for types supported by getInjectorProviders. | ||
| let allProviders: SerializedProvider[] = []; | ||
| if (metadata?.type === 'element' || metadata?.type === 'environment') { | ||
| allProviders = getInjectorProviders(injector).map((record) => { |
There was a problem hiding this comment.
nit: I wonder if it's worth splitting provider token discovery from value discovery. This currently eagerly instantiates every provider in an injector which could have a bunch of side effects.
| if (isLView(componentLView)) { | ||
| const componentTView = componentLView[TVIEW]; | ||
| const firstChild = componentTView.firstChild; | ||
| if (firstChild) yield [firstChild, componentLView]; |
There was a problem hiding this comment.
Does this break if a component has two sibling directives in it's template? While reading this code I was trying to reason about why these if conditions don't have the while(child) loop and it feels like they should.
@Directive({
selector: '[dir-a]',
})
class DirA { }
@Directive({
selector: '[dir-b]',
})
class DirB { }
@Component({
selector: 'test-comp',
template: '<div dir-a></div><div dir-b></div>',
imports: [DirA, DirB],
})
class TestComponent { }
| const embeddedLView = slot[i] as LView; | ||
| const embeddedTView = embeddedLView[TVIEW]; | ||
| const firstChild = embeddedTView.firstChild; | ||
| if (firstChild) yield [firstChild, embeddedLView]; |
There was a problem hiding this comment.
Similar comment to the one above
@Directive({
selector: '[dir-a]',
})
class DirA {}
@Directive({
selector: '[dir-b]',
})
class DirB {}
@Component({
selector: 'test-comp',
template: '@if (true) { <div dir-a></div><div dir-b></div> }',
imports: [DirA, DirB],
})
class TestComponent {}
NOTE FOR REVIEWERS: This PR is dependent on #67985, you can ignore the first 4 commits which will be merged in that PR and rebased here before landing.
This is part of an experiment with chrome-devtools-mcp to expose runtime data about framework internal state such as the dependency injection graph to AI agents and see if it improves debuggability of Angular applications.
Unlike the signal graph tool, I'm far from convinced this is a good output structure for agents to consume. It misses some important edges (notably connecting elements to specific environment injectors) and is likely too much information for an AI to usefully consume. I suspect more targeted tools will be more useful for certain use cases. However, this gives us an initial implementation to experiment with and see what evals best with various agents.
/cc @wolfib