@@ -215,6 +215,20 @@ const SUBAGENT_REPORTED_EVENT_TYPES = [
215215] as const ;
216216const TODO_EVENT_TYPES = [ "todo_updated" ] as const ;
217217
218+ /**
219+ * `Array.prototype.includes` accepts any value, but these literal tuples are what
220+ * make a dispatch branch exhaustive, so membership has to narrow `type` instead of
221+ * merely comparing it — `type && TODO_EVENT_TYPES.includes(type)` still hands a
222+ * plain `string` to a `"todo_updated"` parameter. A frame carrying no event name
223+ * belongs to no branch, which is what `includes(undefined)` already answered.
224+ */
225+ function isEventTypeOf < T extends string > (
226+ type : string | undefined ,
227+ allowed : readonly T [ ] ,
228+ ) : type is T {
229+ return type !== undefined && ( allowed as readonly string [ ] ) . includes ( type ) ;
230+ }
231+
218232function terminalSubagentStatus (
219233 type : string ,
220234 data : Record < string , unknown > ,
@@ -231,11 +245,11 @@ export function reduceWebuiWorkspaceProgressEvent(
231245 sessionId ?: string ,
232246) : WebuiWorkspaceProgressState {
233247 const type = eventType ( value ) ;
234- if ( TODO_EVENT_TYPES . includes ( type ) ) {
248+ if ( isEventTypeOf ( type , TODO_EVENT_TYPES ) ) {
235249 const todos = normalizeTodos ( value . todos ?? eventData ( value ) . todos ) ;
236250 return todos ? { ...state , todos, hasTodoSnapshot : true } : state ;
237251 }
238- if ( SUBAGENT_SPAWN_EVENT_TYPES . includes ( type ) ) {
252+ if ( isEventTypeOf ( type , SUBAGENT_SPAWN_EVENT_TYPES ) ) {
239253 const subagent = subagentFromEvent ( value , sessionId ) ;
240254 return subagent
241255 ? {
@@ -245,7 +259,7 @@ export function reduceWebuiWorkspaceProgressEvent(
245259 }
246260 : state ;
247261 }
248- if ( SUBAGENT_STATUS_EVENT_TYPES . includes ( type ) ) {
262+ if ( isEventTypeOf ( type , SUBAGENT_STATUS_EVENT_TYPES ) ) {
249263 const data = eventData ( value ) ;
250264 const childSessionId = stringValue ( data , [ "sessionId" , "session_id" ] ) ;
251265 if ( ! childSessionId ) return state ;
@@ -262,7 +276,7 @@ export function reduceWebuiWorkspaceProgressEvent(
262276 hasSubagentSnapshot : true ,
263277 } ;
264278 }
265- if ( SUBAGENT_REPORTED_EVENT_TYPES . includes ( type ) ) {
279+ if ( isEventTypeOf ( type , SUBAGENT_REPORTED_EVENT_TYPES ) ) {
266280 const data = eventData ( value ) ;
267281 const childSessionId = stringValue ( data , [ "sessionId" , "session_id" ] ) ;
268282 const existing = childSessionId
@@ -329,8 +343,12 @@ function applyTodoToolCalls(
329343 message : WebuiClientMessage | Record < string , unknown > ,
330344) : WebuiWorkspaceProgressState {
331345 const loose = message as Record < string , unknown > ;
346+ // `message` is a union, and its `Record<string, unknown>` arm types `toolCalls`
347+ // as `unknown` — so `message.toolCalls ?? []` leaves a non-iterable on the
348+ // right-hand side. Read the declared field through the same `Array.isArray`
349+ // guard as the snake_case one below it.
332350 const calls = [
333- ...( message . toolCalls ?? [ ] ) ,
351+ ...( Array . isArray ( message . toolCalls ) ? message . toolCalls : [ ] ) ,
334352 ...( Array . isArray ( loose . tool_calls ) ? loose . tool_calls : [ ] ) ,
335353 ] ;
336354 let next = state ;
0 commit comments