(e: React.FormEvent<HTMLFormElement>)
| 118 | const userInput: ChatInputType = { |
| 119 | chatId: targetChatId, |
| 120 | message, |
| 121 | model: model ?? selectedModel, |
| 122 | role: 'user', |
| 123 | }; |
| 124 | if (saveUser) { |
| 125 | saveMessage({ |
| 126 | variables: { |
| 127 | input: userInput as ChatInputType, |
| 128 | }, |
| 129 | }); |
| 130 | } |
| 131 | |
| 132 | // The agent loop runs on the backend, against the project's real files. |
| 133 | // The bubble appears with the first text delta rather than up front, so a |
| 134 | // failed turn does not leave an empty message behind. |
| 135 | // The turn is built as steps, not as one growing string: the agent |
| 136 | // narrates, runs a tool, narrates again, and only its last words are the |
| 137 | // answer. `content` still holds the joined text so everything that reads |
| 138 | // a message as plain text keeps working. |
| 139 | const flush = () => |
| 140 | setMessages((prev) => { |
| 141 | const next: Message = { |
| 142 | id: replyId, |
| 143 | role: 'assistant', |
| 144 | content: steps |
| 145 | .filter( |
| 146 | (s): s is Extract<TurnStep, { kind: 'text' }> => |
| 147 | s.kind === 'text' |
| 148 | ) |
| 149 | .map((s) => s.text) |
| 150 | .join(''), |
| 151 | createdAt: new Date().toISOString(), |
| 152 | steps: [...steps], |
| 153 | }; |
| 154 | return prev.some((m) => m.id === replyId) |
| 155 | ? prev.map((m) => (m.id === replyId ? { ...m, ...next } : m)) |
| 156 | : [...prev, next]; |
| 157 | }); |
nothing calls this directly
no test coverage detected