99import { readFile , readdir , access } from 'fs/promises' ;
1010import { join } from 'path' ;
1111import type { Thread , ThreadVisibility } from '../../shared/types.js' ;
12- import { THREADS_DIR , isHandoffRelationship , type ThreadFile } from './threadTypes.js' ;
12+ import {
13+ THREADS_DIR ,
14+ isHandoffRelationship ,
15+ isToolUseContent ,
16+ type ThreadFile ,
17+ type ThreadMessage ,
18+ } from './threadTypes.js' ;
1319import { listThreads , type AmpThreadSummary } from './amp-api.js' ;
1420import { formatRelativeTime , parseFileUri , runAmp } from './utils.js' ;
1521
@@ -56,8 +62,9 @@ export async function listAllThreads(): Promise<Thread[]> {
5662
5763/**
5864 * Scan local thread files for handoff relationships and merge them into
59- * the thread list. This is fast (~5ms for 25 local files) and only reads
60- * the `relationships` field from each file.
65+ * the thread list. Checks two sources:
66+ * 1. `relationships[]` array (older Amp format)
67+ * 2. `handoff` tool_use/tool_result blocks in messages (newer Amp format)
6168 */
6269async function enrichRelationships ( threads : Thread [ ] ) : Promise < void > {
6370 const threadMap = new Map ( threads . map ( ( t ) => [ t . id , t ] ) ) ;
@@ -80,9 +87,9 @@ async function enrichRelationships(threads: Thread[]): Promise<void> {
8087 try {
8188 const content = await readFile ( join ( THREADS_DIR , file ) , 'utf-8' ) ;
8289 const data = JSON . parse ( content ) as ThreadFile ;
83- const relationships = data . relationships || [ ] ;
8490
85- for ( const rel of relationships ) {
91+ // Source 1: explicit relationships array (older format)
92+ for ( const rel of data . relationships || [ ] ) {
8693 if ( isHandoffRelationship ( rel ) ) {
8794 if ( rel . role === 'child' ) {
8895 thread . handoffParentId = rel . threadID ;
@@ -93,6 +100,20 @@ async function enrichRelationships(threads: Thread[]): Promise<void> {
93100 }
94101 }
95102
103+ // Source 2: handoff tool blocks in messages (newer format)
104+ const childIds = extractHandoffChildIds ( data . messages || [ ] , threadId ) ;
105+ if ( childIds . length > 0 ) {
106+ thread . handoffChildIds = thread . handoffChildIds || [ ] ;
107+ thread . handoffChildIds . push ( ...childIds ) ;
108+ // Set the reverse link: mark each child's parent
109+ for ( const childId of childIds ) {
110+ const child = threadMap . get ( childId ) ;
111+ if ( child && ! child . handoffParentId ) {
112+ child . handoffParentId = threadId ;
113+ }
114+ }
115+ }
116+
96117 if ( thread . handoffChildIds ?. length ) {
97118 thread . handoffChildIds = [ ...new Set ( thread . handoffChildIds ) ] ;
98119 }
@@ -103,6 +124,45 @@ async function enrichRelationships(threads: Thread[]): Promise<void> {
103124 ) ;
104125}
105126
127+ const THREAD_ID_RE = / T - [ 0 - 9 a - f ] { 8 } - [ 0 - 9 a - f ] { 4 } - [ 0 - 9 a - f ] { 4 } - [ 0 - 9 a - f ] { 4 } - [ 0 - 9 a - f ] { 12 } / g;
128+
129+ /**
130+ * Extract child thread IDs from handoff tool_result blocks.
131+ * When a thread uses the `handoff` tool, the tool_result contains the new thread ID.
132+ */
133+ function extractHandoffChildIds ( messages : ThreadMessage [ ] , selfId : string ) : string [ ] {
134+ const handoffToolUseIds = new Set < string > ( ) ;
135+ const childIds : string [ ] = [ ] ;
136+
137+ for ( const msg of messages ) {
138+ if ( ! Array . isArray ( msg . content ) ) continue ;
139+
140+ for ( const block of msg . content ) {
141+ // Collect tool_use IDs for handoff tools
142+ if ( isToolUseContent ( block ) && block . name === 'handoff' ) {
143+ const id = ( block as unknown as Record < string , unknown > ) . id as string | undefined ;
144+ if ( id ) handoffToolUseIds . add ( id ) ;
145+ }
146+
147+ // Check tool_result blocks that reference a handoff tool_use
148+ const b = block as Record < string , unknown > ;
149+ if ( b . type === 'tool_result' && typeof b . toolUseID === 'string' ) {
150+ if ( handoffToolUseIds . has ( b . toolUseID ) ) {
151+ // Extract thread IDs from the result content
152+ const resultText =
153+ typeof b . content === 'string' ? b . content : JSON . stringify ( b . content ?? b . run ?? '' ) ;
154+ const matches = resultText . match ( THREAD_ID_RE ) || [ ] ;
155+ for ( const tid of matches ) {
156+ if ( tid !== selfId ) childIds . push ( tid ) ;
157+ }
158+ }
159+ }
160+ }
161+ }
162+
163+ return [ ...new Set ( childIds ) ] ;
164+ }
165+
106166function toThread ( s : AmpThreadSummary ) : Thread {
107167 const tree = s . env ?. initial ?. trees ?. [ 0 ] ;
108168 const repoUrl = tree ?. repository ?. url ;
0 commit comments