You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Currently, tool action responses (manual inputs and runtime interrupts) are harvested using an imperative DOM-scraping pattern (parseMessageData(harvestToolResponses) querying viewChildren(JsonEditorComponent)).
This pattern was historically inherited from MessageComponent (which required DOM scraping due to contenteditable), but is an anti-pattern for structured JSON tool calls. We should refactor ToolActionCard to maintain its draft response state in reactive Signals (or a form-free CdkStepper), emit responses directly on submission, and decouple it from MessageList's DOM parsing.
Motivation & Current Limitations
Forced DOM Persistence Hack ([class.hidden]):
Because ToolActionCard scrapes this.editors() on submit, all steps in multi-tool interruptions must remain mounted simultaneously in the DOM using CSS display: none. If inactive steps used @if or lazy rendering, their editor instances would be destroyed and unsubmitted inputs lost.
Index-Coupled and Fragile: parseMessageData iterates over this.messages() while manually incrementing parallel index counters (cardIndex++, componentIndex++). If the DOM tree and data array ever desynchronize, the indices misalign.
No Reactive Validation:
JSON validation only happens at submission time inside getToolCallResponses(), preventing real-time disabling of "Next" / "Resume" buttons when JSON is malformed.
Runner State Mismatch:
In AgentRunner, chatLog is a read-only computed() signal. Forcing it through a [(messages)] two-way binding and DOM-harvesting loop requires manual state synchronization hacks after scraping.
Proposed Architecture
flowchart TD
TAC["<b>ToolActionCard (Reactive)</b><br/>• Holds draft responses in signals: <code>toolResponses = signal<Record<string, ...>>({})</code><br/>• Lazy-renders active step only (@if / CdkStepper)<br/>• Emits payload directly: <code>submitEvent = output<ToolCallResponse[]>()</code>"]
ML["<b>MessageListComponent</b><br/>• Forwards <code>submitEvent</code> payload to parent runner<br/>• <code>parseMessageData()</code> strictly syncs contenteditable message text/media"]
MR["<b>ModelRunner</b><br/>1. Receives payload<br/>2. Syncs prior edited messages via <code>parseMessageData()</code><br/>3. Dispatches request"]
PR["<b>PromptRunner</b><br/>1. Receives payload<br/>2. Dispatches prompt run with resume payload"]
AR["<b>AgentRunner</b><br/>1. Receives payload<br/>2. Bypasses message list DOM entirely<br/>3. Dispatches bidi / turn resume directly"]
TAC -->|"(submitEvent)='onToolResume($event)'"| ML
ML --> MR
ML --> PR
ML --> AR
Loading
Impact by Component
1. ToolActionCard
Maintain in-progress inputs in a signal: protected readonly toolResponses = signal<Record<string, ToolCallResponse>>({}).
Update draft state in memory on (contentChangeEvent) from JsonEditorComponent.
Lazy-render steps using @if (!hasMultipleToolCalls() || currentStep() === i) (or headless CdkStepper without forms), unmounting inactive Monaco/JSON editors without losing state.
Update submitEvent = output<ToolCallResponse[]>() to emit ready-to-use payloads directly.
Derive isResumeValid = computed(...) to reactively disable the Resume button if any step contains invalid JSON.
2. MessageListComponent
Remove the harvestToolResponses boolean flag and toolActionCards indexing from parseMessageData().
Keep parseMessageData() focused exclusively on extracting rich text and media from MessageComponent instances.
Forward (submitEvent)="submitEvent.emit($event)" with the payload.
3. ModelRunner
Why it still needs message parsing: In ModelRunner, multi-turn and Append Mode allow users to edit historical turns in-place (contenteditable). When resuming, the model API requires the entire conversation history along with the resume payload.
Changes: Catch the reactive tool response payload, call parseMessageData() to sync any edited historical messages, and package both into buildPrompt().
4. PromptRunner
Remove parseMessageData(true) call before running.
Pass the emitted ToolCallResponse[] payload directly into resume options.
5. AgentRunner
Remove the [(messages)]="chatLog()" two-way binding mismatch on the computed() signal in favor of one-way [messages]="chatLog()".
Handle (submitEvent)="sendMessage($event)" by dispatching the turn/bidi resume payload immediately without touching the message list DOM.
Acceptance Criteria
ToolActionCard holds draft state in memory/signals and emits ToolCallResponse[] on submitEvent.
Steps in multi-tool interruptions are rendered lazily with @if or CdkStepper rather than kept in DOM with display: none.
MessageListComponent.parseMessageData() no longer queries or mutates ToolActionCard.
In ModelRunner, user edits to historical messages are still captured when resuming a tool action.
In AgentRunner, message-list is bound one-way and tool resume bypasses parseMessageData().
Unit tests for ToolActionCard, MessageList, and all three runners updated to test the reactive data flow.
Description
Currently, tool action responses (manual inputs and runtime interrupts) are harvested using an imperative DOM-scraping pattern (
parseMessageData(harvestToolResponses)queryingviewChildren(JsonEditorComponent)).This pattern was historically inherited from
MessageComponent(which required DOM scraping due tocontenteditable), but is an anti-pattern for structured JSON tool calls. We should refactorToolActionCardto maintain its draft response state in reactive Signals (or a form-freeCdkStepper), emit responses directly on submission, and decouple it fromMessageList's DOM parsing.Motivation & Current Limitations
[class.hidden]):Because
ToolActionCardscrapesthis.editors()on submit, all steps in multi-tool interruptions must remain mounted simultaneously in the DOM using CSSdisplay: none. If inactive steps used@ifor lazy rendering, their editor instances would be destroyed and unsubmitted inputs lost.parseMessageDataiterates overthis.messages()while manually incrementing parallel index counters (cardIndex++,componentIndex++). If the DOM tree and data array ever desynchronize, the indices misalign.JSON validation only happens at submission time inside
getToolCallResponses(), preventing real-time disabling of "Next" / "Resume" buttons when JSON is malformed.In
AgentRunner,chatLogis a read-onlycomputed()signal. Forcing it through a[(messages)]two-way binding and DOM-harvesting loop requires manual state synchronization hacks after scraping.Proposed Architecture
flowchart TD TAC["<b>ToolActionCard (Reactive)</b><br/>• Holds draft responses in signals: <code>toolResponses = signal<Record<string, ...>>({})</code><br/>• Lazy-renders active step only (@if / CdkStepper)<br/>• Emits payload directly: <code>submitEvent = output<ToolCallResponse[]>()</code>"] ML["<b>MessageListComponent</b><br/>• Forwards <code>submitEvent</code> payload to parent runner<br/>• <code>parseMessageData()</code> strictly syncs contenteditable message text/media"] MR["<b>ModelRunner</b><br/>1. Receives payload<br/>2. Syncs prior edited messages via <code>parseMessageData()</code><br/>3. Dispatches request"] PR["<b>PromptRunner</b><br/>1. Receives payload<br/>2. Dispatches prompt run with resume payload"] AR["<b>AgentRunner</b><br/>1. Receives payload<br/>2. Bypasses message list DOM entirely<br/>3. Dispatches bidi / turn resume directly"] TAC -->|"(submitEvent)='onToolResume($event)'"| ML ML --> MR ML --> PR ML --> ARImpact by Component
1.
ToolActionCardprotected readonly toolResponses = signal<Record<string, ToolCallResponse>>({}).(contentChangeEvent)fromJsonEditorComponent.@if (!hasMultipleToolCalls() || currentStep() === i)(or headlessCdkStepperwithout forms), unmounting inactive Monaco/JSON editors without losing state.submitEvent = output<ToolCallResponse[]>()to emit ready-to-use payloads directly.isResumeValid = computed(...)to reactively disable the Resume button if any step contains invalid JSON.2.
MessageListComponentharvestToolResponsesboolean flag andtoolActionCardsindexing fromparseMessageData().parseMessageData()focused exclusively on extracting rich text and media fromMessageComponentinstances.(submitEvent)="submitEvent.emit($event)"with the payload.3.
ModelRunnerModelRunner, multi-turn and Append Mode allow users to edit historical turns in-place (contenteditable). When resuming, the model API requires the entire conversation history along with the resume payload.parseMessageData()to sync any edited historical messages, and package both intobuildPrompt().4.
PromptRunnerparseMessageData(true)call before running.ToolCallResponse[]payload directly intoresumeoptions.5.
AgentRunner[(messages)]="chatLog()"two-way binding mismatch on thecomputed()signal in favor of one-way[messages]="chatLog()".(submitEvent)="sendMessage($event)"by dispatching the turn/bidi resume payload immediately without touching the message list DOM.Acceptance Criteria
ToolActionCardholds draft state in memory/signals and emitsToolCallResponse[]onsubmitEvent.@iforCdkStepperrather than kept in DOM withdisplay: none.MessageListComponent.parseMessageData()no longer queries or mutatesToolActionCard.ModelRunner, user edits to historical messages are still captured when resuming a tool action.AgentRunner,message-listis bound one-way and tool resume bypassesparseMessageData().ToolActionCard,MessageList, and all three runners updated to test the reactive data flow.