Render a finished living-artifact: the screenshot (if any) + verdict + the live link, with * Approve / Edit / Reject buttons. Falls back to text when the adapter can't send a photo.
(t: Transport, chatId: string, card: ArtifactCard)
| 170 | const ac = new AbortController(); |
| 171 | this.busy.set(key, ac); |
| 172 | const pump = new StreamPump({ |
| 173 | maxLen: t.maxLen, |
| 174 | minIntervalMs: t.minEditIntervalMs, |
| 175 | now: this.now, |
| 176 | send: s => t.send(chatId, s), |
| 177 | edit: (ref, s) => t.edit(chatId, ref, s), |
| 178 | }); |
| 179 | const drainTimer = setInterval(() => void pump.drain(), t.minEditIntervalMs); |
| 180 | if (typeof (drainTimer as any).unref === 'function') (drainTimer as any).unref(); |
| 181 | |
| 182 | let turnCard: ArtifactCard | undefined; |
| 183 | const sink: TurnSink = { |
| 184 | onDelta: s => pump.push(s), |
| 185 | onStatus: () => {}, // tool/status lines stay quiet to avoid noise |
| 186 | ask: (prompt, options) => getOperatorHub().requestApproval('bot', prompt, options, { |
| 187 | lane: botLane(key), |
| 188 | origin: key, |
| 189 | }), |
| 190 | artifact: card => { turnCard = card; }, // held until the stream finishes, then rendered below |
| 191 | }; |
| 192 |