@@ -124,29 +124,73 @@ function isScalar(value: unknown): boolean {
124124}
125125
126126/**
127- * Scalar arguments as `key value` pairs with their newlines intact — a shell
128- * command or a prompt is written to be read as text, and pretty-printed JSON
129- * would hand it back with its line breaks escaped.
127+ * Scalar (or scalar-array) arguments as `key value` pairs with their newlines
128+ * intact — a shell command or a spawn prompt is written to be read as text, and
129+ * pretty-printed JSON would hand it back with its line breaks escaped (CL-5762).
130+ *
131+ * Nested objects recurse one level so a task brief expands as fields rather than
132+ * a JSON dump; deeper nesting collapses to a compact token.
130133 */
131- function scalarDetail ( args : Record < string , unknown > ) : readonly StyledBodyLine [ ] | null {
132- const entries = Object . entries ( args )
133- if ( entries . length === 0 || ! entries . every ( ( [ , value ] ) => isScalar ( value ) ) ) {
134- return null
135- }
134+ function fieldDetail (
135+ args : Record < string , unknown > ,
136+ indent = 0 ,
137+ ) : readonly StyledBodyLine [ ] {
138+ const pad = " " . repeat ( indent )
136139 const lines : StyledBodyLine [ ] = [ ]
137- for ( const [ key , value ] of entries ) {
138- const text = typeof value === "string" ? value : JSON . stringify ( value )
139- const rows = ( text ?? "null" ) . split ( "\n" )
140- rows . forEach ( ( row , i ) => {
141- lines . push (
142- i === 0
143- ? [
144- { text : `${ key } : ` , fg : UI . textDim } ,
145- { text : row , fg : UI . text } ,
146- ]
147- : [ { text : `${ " " . repeat ( key . length + 2 ) } ${ row } ` , fg : UI . text } ] ,
148- )
149- } )
140+ for ( const [ key , value ] of Object . entries ( args ) ) {
141+ if ( isScalar ( value ) ) {
142+ const text = typeof value === "string" ? value : JSON . stringify ( value )
143+ const rows = ( text ?? "null" ) . split ( "\n" )
144+ rows . forEach ( ( row , i ) => {
145+ lines . push (
146+ i === 0
147+ ? [
148+ { text : `${ pad } ${ key } : ` , fg : UI . textDim } ,
149+ { text : row , fg : UI . text } ,
150+ ]
151+ : [ { text : `${ pad } ${ " " . repeat ( key . length + 2 ) } ${ row } ` , fg : UI . text } ] ,
152+ )
153+ } )
154+ continue
155+ }
156+ if ( Array . isArray ( value ) && value . every ( isScalar ) ) {
157+ if ( value . length === 0 ) {
158+ lines . push ( [
159+ { text : `${ pad } ${ key } : ` , fg : UI . textDim } ,
160+ { text : "[]" , fg : UI . text } ,
161+ ] )
162+ continue
163+ }
164+ lines . push ( [ { text : `${ pad } ${ key } :` , fg : UI . textDim } ] )
165+ for ( const item of value ) {
166+ const text = typeof item === "string" ? item : JSON . stringify ( item )
167+ for ( const row of text . split ( "\n" ) ) {
168+ lines . push ( [ { text : `${ pad } - ${ row } ` , fg : UI . text } ] )
169+ }
170+ }
171+ continue
172+ }
173+ if ( typeof value === "object" && value !== null && ! Array . isArray ( value ) ) {
174+ // One level of nesting is enough for a spawn brief; deeper stays compact.
175+ if ( indent === 0 ) {
176+ lines . push ( [ { text : `${ pad } ${ key } :` , fg : UI . textDim } ] )
177+ lines . push ( ...fieldDetail ( value as Record < string , unknown > , indent + 2 ) )
178+ } else {
179+ lines . push ( [
180+ { text : `${ pad } ${ key } : ` , fg : UI . textDim } ,
181+ { text : "{…}" , fg : UI . text } ,
182+ ] )
183+ }
184+ continue
185+ }
186+ // Arrays of objects, etc. — compact rather than a wall of JSON.
187+ lines . push ( [
188+ { text : `${ pad } ${ key } : ` , fg : UI . textDim } ,
189+ {
190+ text : Array . isArray ( value ) ? `[${ value . length } items]` : "{…}" ,
191+ fg : UI . text ,
192+ } ,
193+ ] )
150194 }
151195 return lines . slice ( 0 , MAX_DETAIL_LINES )
152196}
@@ -221,7 +265,9 @@ function subjectFor(
221265 args : Record < string , unknown > ,
222266) : string {
223267 const { summary } = summarizeToolArgs ( name , raw )
224- if ( ! isArgumentList ( args , summary ) ) return summary
268+ // An empty formatter summary is not a subject — fall through to primarySubject
269+ // so a task without description still paints its prompt rather than raw JSON.
270+ if ( summary . length > 0 && ! isArgumentList ( args , summary ) ) return summary
225271 return primarySubject ( args ) ?? summary
226272}
227273
@@ -246,7 +292,7 @@ export function toolArgsView(name: string, rawArgs: string): ToolArgsView | null
246292 // Its arguments are a query, not a subject: nobody reads a transcript for
247293 // the pagination cursor, so they belong behind the expand key or nowhere.
248294 if ( args !== null && isMcpToolName ( name ) ) {
249- return withDetail ( "" , scalarDetail ( args ) ?? jsonDetail ( args ) )
295+ return withDetail ( "" , fieldDetail ( args ) )
250296 }
251297
252298 if ( args === null && raw . length <= INLINE_MAX && ! raw . includes ( "\n" ) ) return null
@@ -256,8 +302,10 @@ export function toolArgsView(name: string, rawArgs: string): ToolArgsView | null
256302 return summary . length === 0 ? null : withDetail ( summary , jsonDetail ( raw ) )
257303 }
258304 const subject = subjectFor ( name , raw , args )
259- if ( subject . length === 0 ) return null
260- return withDetail ( subject , scalarDetail ( args ) ?? jsonDetail ( args ) )
305+ // Object args always get a summarised view — even with an empty subject the
306+ // verb alone names the call and the body expands with real line breaks. A
307+ // null return here is what used to dump raw argument JSON into the transcript.
308+ return withDetail ( subject , fieldDetail ( args ) )
261309}
262310
263311/**
0 commit comments