@@ -195,6 +195,99 @@ export function persistPiMessageEndModelMeta(args: {
195195 }
196196}
197197
198+ type TodoOverlayUpdater = { update : ( sessionId ?: string ) => void } ;
199+
200+ type CompatiblePiTodoCapture = {
201+ normalized : string ;
202+ todos : unknown [ ] ;
203+ } ;
204+
205+ function getCompatiblePiTodoCapture (
206+ todos : unknown ,
207+ ) : CompatiblePiTodoCapture | null {
208+ if ( ! Array . isArray ( todos ) ) return null ;
209+ const normalized = normalizeTodoStateJson ( todos ) ;
210+ if ( normalized === null ) return null ;
211+ return { normalized, todos } ;
212+ }
213+
214+ function applyCompatiblePiTodoCapture ( args : {
215+ db : ContextDatabase ;
216+ sessionId : string ;
217+ todowriteEnabled : boolean ;
218+ todoOverlay ?: TodoOverlayUpdater ;
219+ persist : boolean ;
220+ capture : CompatiblePiTodoCapture ;
221+ } ) : void {
222+ if ( args . todowriteEnabled ) {
223+ setTodoSnapshot ( args . sessionId , args . capture . todos ) ;
224+ args . todoOverlay ?. update ( args . sessionId ) ;
225+ }
226+ if ( args . persist ) {
227+ updateSessionMeta ( args . db , args . sessionId , {
228+ lastTodoState : args . capture . normalized ,
229+ } ) ;
230+ }
231+ }
232+
233+ /**
234+ * Capture a `todowrite` args.todos payload only when it matches Magic Context's
235+ * exact todo enum contract. Third-party Pi extensions can reuse the same tool
236+ * name, so incompatible shapes must leave `last_todo_state` untouched.
237+ */
238+ export function capturePiTodowriteArgsIfCompatible ( args : {
239+ db : ContextDatabase ;
240+ sessionId : string ;
241+ todos : unknown ;
242+ todowriteEnabled : boolean ;
243+ todoOverlay ?: TodoOverlayUpdater ;
244+ persist : boolean ;
245+ } ) : boolean {
246+ const capture = getCompatiblePiTodoCapture ( args . todos ) ;
247+ if ( capture === null ) return false ;
248+ applyCompatiblePiTodoCapture ( { ...args , capture } ) ;
249+ return true ;
250+ }
251+
252+ /**
253+ * Scan an assistant `message_end` payload for the first compatible `todowrite`
254+ * call. This keeps interop with third-party tools that share the name but only
255+ * captures state when their payload matches Magic Context's todo enums exactly.
256+ */
257+ export function capturePiTodowriteMessageIfCompatible ( args : {
258+ db : ContextDatabase ;
259+ sessionId : string ;
260+ message : unknown ;
261+ todowriteEnabled : boolean ;
262+ todoOverlay ?: TodoOverlayUpdater ;
263+ persist : boolean ;
264+ } ) : boolean {
265+ const msg = args . message as { role ?: unknown ; content ?: unknown } | undefined ;
266+ if ( msg ?. role !== "assistant" || ! Array . isArray ( msg . content ) ) {
267+ return false ;
268+ }
269+
270+ for ( const block of msg . content ) {
271+ if ( ! block || typeof block !== "object" ) continue ;
272+ const b = block as {
273+ type ?: unknown ;
274+ name ?: unknown ;
275+ arguments ?: unknown ;
276+ } ;
277+ if ( b . type !== "toolCall" ) continue ;
278+ if ( typeof b . name !== "string" ) continue ;
279+ if ( b . name !== "todowrite" ) continue ;
280+ const capture = getCompatiblePiTodoCapture (
281+ ( b . arguments as { todos ?: unknown } | null | undefined ) ?. todos ,
282+ ) ;
283+ if ( capture === null ) continue ;
284+ applyCompatiblePiTodoCapture ( { ...args , capture } ) ;
285+ return true ;
286+ }
287+
288+ return false ;
289+ }
290+
198291function info ( message : string , data ?: unknown ) : void {
199292 log ( `${ PREFIX } ${ message } ` , data ) ;
200293}
@@ -1470,10 +1563,6 @@ export default async function (pi: ExtensionAPI): Promise<void> {
14701563 const sessionMeta = Array . isArray ( todos )
14711564 ? getOrCreateSessionMeta ( db , sessionId )
14721565 : null ;
1473- if ( todowriteEnabled && Array . isArray ( todos ) ) {
1474- setTodoSnapshot ( sessionId , todos ) ;
1475- todoOverlay ?. update ( sessionId ) ;
1476- }
14771566
14781567 // Synthetic-todowrite snapshot capture (Pi parity with
14791568 // OpenCode hook-handlers.ts:386-401). Persist normalized
@@ -1482,15 +1571,17 @@ export default async function (pi: ExtensionAPI): Promise<void> {
14821571 // snapshot to replay on the next cache-busting pass.
14831572 // Cache-safe: this is a pure DB write with no message
14841573 // mutation. Subagents skip — they do not get synthetic
1485- // todowrite injection.
1486- if ( sessionMeta && ! sessionMeta . isSubagent ) {
1487- const normalizedTodos = normalizeTodoStateJson ( todos ) ;
1488- if ( normalizedTodos !== null ) {
1489- updateSessionMeta ( db , sessionId , {
1490- lastTodoState : normalizedTodos ,
1491- } ) ;
1492- }
1493- }
1574+ // todowrite injection. Foreign Pi extensions can share the
1575+ // `todowrite` name, so only the exact Magic Context todo
1576+ // shape updates the stored snapshot.
1577+ capturePiTodowriteArgsIfCompatible ( {
1578+ db,
1579+ sessionId,
1580+ todos,
1581+ todowriteEnabled,
1582+ todoOverlay,
1583+ persist : Boolean ( sessionMeta && ! sessionMeta . isSubagent ) ,
1584+ } ) ;
14941585
14951586 if (
14961587 Array . isArray ( todos ) &&
@@ -1725,43 +1816,14 @@ export default async function (pi: ExtensionAPI): Promise<void> {
17251816 try {
17261817 const sessionMetaForTodo = getOrCreateSessionMeta ( db , sessionId ) ;
17271818 if ( ! sessionMetaForTodo . isSubagent ) {
1728- const msg = event . message as
1729- | { role ?: string ; content ?: unknown }
1730- | undefined ;
1731- if ( msg && msg . role === "assistant" && Array . isArray ( msg . content ) ) {
1732- for ( const block of msg . content ) {
1733- if ( ! block || typeof block !== "object" ) continue ;
1734- const b = block as {
1735- type ?: unknown ;
1736- name ?: unknown ;
1737- arguments ?: unknown ;
1738- } ;
1739- if ( b . type !== "toolCall" ) continue ;
1740- if ( typeof b . name !== "string" ) continue ;
1741- if ( b . name !== "todowrite" ) {
1742- continue ;
1743- }
1744- const args = b . arguments as
1745- | { todos ?: unknown }
1746- | null
1747- | undefined ;
1748- const todos = args ?. todos ;
1749- if ( ! Array . isArray ( todos ) ) continue ;
1750- const normalized = normalizeTodoStateJson ( todos ) ;
1751- if ( normalized === null ) continue ;
1752- if ( todowriteEnabled ) {
1753- setTodoSnapshot ( sessionId , todos ) ;
1754- todoOverlay ?. update ( sessionId ) ;
1755- }
1756- updateSessionMeta ( db , sessionId , {
1757- lastTodoState : normalized ,
1758- } ) ;
1759- // First valid todowrite block wins — mirrors OpenCode's
1760- // `tool.execute.after` behavior of capturing one
1761- // snapshot per tool invocation.
1762- break ;
1763- }
1764- }
1819+ capturePiTodowriteMessageIfCompatible ( {
1820+ db,
1821+ sessionId,
1822+ message : event . message ,
1823+ todowriteEnabled,
1824+ todoOverlay,
1825+ persist : true ,
1826+ } ) ;
17651827 }
17661828 } catch ( err ) {
17671829 warn ( "message_end: synthetic todowrite capture failed:" , err ) ;
0 commit comments