@@ -32,13 +32,109 @@ function runLine(text: string, fg: string = UI.text): StyledBodyLine {
3232
3333function appendRunLine (
3434 lines : readonly StyledBodyLine [ ] ,
35- text : string ,
35+ line : StyledBodyLine ,
3636) : readonly StyledBodyLine [ ] {
3737 if ( lines . length > MAX_RUN_LINES ) return lines ;
3838 if ( lines . length === MAX_RUN_LINES ) {
3939 return [ ...lines , runLine ( "… more answers" , UI . textDim ) ] ;
4040 }
41- return [ ...lines , runLine ( text ) ] ;
41+ return [ ...lines , line ] ;
42+ }
43+
44+ /**
45+ * One member's line in a lane's expanded body: which call, then what it got.
46+ * The label is dim so the outcome — the part that changes per member — reads
47+ * first.
48+ */
49+ function memberRunLine ( label : string , outcome : string ) : StyledBodyLine {
50+ if ( label . length === 0 ) return runLine ( outcome ) ;
51+ return [
52+ { text : label , fg : UI . textDim } ,
53+ { text : ` — ${ outcome } ` , fg : UI . text } ,
54+ ] ;
55+ }
56+
57+ /**
58+ * Argument keys that name which object a call acted on, most-identifying
59+ * first. Only consulted when the call's painted summary is empty — an MCP
60+ * call's verb is already the whole sentence, so its subject lives in the
61+ * arguments (the issue id on a save_comment, not the comment body).
62+ */
63+ const LANE_MEMBER_KEYS = [
64+ "issueId" ,
65+ "issue" ,
66+ "commentId" ,
67+ "id" ,
68+ "key" ,
69+ "command" ,
70+ "query" ,
71+ "url" ,
72+ "pattern" ,
73+ "path" ,
74+ "file_path" ,
75+ "name" ,
76+ "description" ,
77+ "prompt" ,
78+ ] as const ;
79+
80+ const LANE_MEMBER_SUBJECT_MAX = 48 ;
81+
82+ function clipMemberSubject ( value : string ) : string {
83+ const oneLine = value . replace ( / \s + / g, " " ) . trim ( ) ;
84+ return oneLine . length <= LANE_MEMBER_SUBJECT_MAX
85+ ? oneLine
86+ : `${ oneLine . slice ( 0 , LANE_MEMBER_SUBJECT_MAX - 1 ) } …` ;
87+ }
88+
89+ function memberArgs ( raw : string ) : Record < string , unknown > | null {
90+ let parsed : unknown ;
91+ try {
92+ parsed = JSON . parse ( raw ) ;
93+ } catch {
94+ return null ;
95+ }
96+ if ( typeof parsed !== "object" || parsed === null || Array . isArray ( parsed ) ) {
97+ return null ;
98+ }
99+ return parsed as Record < string , unknown > ;
100+ }
101+
102+ /** Which object a lane member acted on, read off its painted summary or args. */
103+ function laneMemberLabel ( row : StreamRow ) : string {
104+ // A settled row's label was recorded at merge time, while its arguments
105+ // were still on the row — re-deriving now would read keys off the answer
106+ // payload that replaced them.
107+ if ( row . callId !== undefined ) {
108+ const index = ( row . memberIds ?? [ ] ) . indexOf ( row . callId ) ;
109+ const recorded = index >= 0 ? ( row . memberLabels ?. [ index ] ?? "" ) : "" ;
110+ if ( recorded . length > 0 ) return recorded ;
111+ }
112+ const summary = row . summary ?. trim ( ) ?? "" ;
113+ if ( summary . length > 0 ) return clipMemberSubject ( summary ) ;
114+ const args = memberArgs ( row . text ) ;
115+ if ( args !== null ) {
116+ for ( const key of LANE_MEMBER_KEYS ) {
117+ const value = args [ key ] ;
118+ if ( typeof value === "string" && value . trim ( ) . length > 0 ) {
119+ return clipMemberSubject ( value ) ;
120+ }
121+ if ( typeof value === "number" || typeof value === "boolean" ) {
122+ return String ( value ) ;
123+ }
124+ }
125+ const first = Object . values ( args ) . find (
126+ ( value ) => typeof value === "string" && value . trim ( ) . length > 0 ,
127+ ) ;
128+ if ( typeof first === "string" ) return clipMemberSubject ( first ) ;
129+ }
130+ return row . verb ?? row . meta ?? row . toolName ?? "" ;
131+ }
132+
133+ /** The label the result's call carried when the lane absorbed it, if known. */
134+ function memberLabelFor ( call : StreamRow , result : StreamRow ) : string {
135+ if ( result . callId === undefined ) return "" ;
136+ const index = ( call . memberIds ?? [ ] ) . indexOf ( result . callId ) ;
137+ return index >= 0 ? ( call . memberLabels ?. [ index ] ?? "" ) : "" ;
42138}
43139
44140/** Longest an answer's own words may run before they belong behind the arrow. */
@@ -187,7 +283,10 @@ export function mergeToolRows(call: StreamRow, result: StreamRow): StreamRow {
187283 ...( remaining > 0 ? { pending : true } : { } ) ,
188284 detail : appendRunLine (
189285 call . detail ?? [ ] ,
190- failed ? "call failed" : ( addendum ?? "answered" ) ,
286+ memberRunLine (
287+ memberLabelFor ( call , result ) ,
288+ failed ? ( addendum ?? "call failed" ) : ( addendum ?? "answered" ) ,
289+ ) ,
191290 ) ,
192291 } ;
193292 }
@@ -196,6 +295,14 @@ export function mergeToolRows(call: StreamRow, result: StreamRow): StreamRow {
196295 const showsPayload = payload . length > 0 && payload !== base . stat ;
197296 return {
198297 ...base ,
298+ // The merge is the last moment this call's own arguments are on the
299+ // row — record who it was so a later repeat can name it in the lane.
300+ ...( call . callId !== undefined
301+ ? {
302+ memberIds : [ call . callId ] ,
303+ memberLabels : [ laneMemberLabel ( call ) ] ,
304+ }
305+ : { } ) ,
199306 ...( result . structured !== undefined
200307 ? { structured : result . structured }
201308 : { } ) ,
@@ -232,12 +339,20 @@ export function canCoalesceCall(
232339
233340/** Calls a lane remembers so a later result can still find this row. */
234341
235- function laneMembers ( tail : StreamRow , next : StreamRow ) : string [ ] | undefined {
236- const members = [
342+ function laneMembers (
343+ tail : StreamRow ,
344+ next : StreamRow ,
345+ ) : { ids : string [ ] ; labels : string [ ] } {
346+ const ids = [
237347 ...( tail . memberIds ?? ( tail . callId !== undefined ? [ tail . callId ] : [ ] ) ) ,
238348 ...( next . callId !== undefined ? [ next . callId ] : [ ] ) ,
239349 ] ;
240- return members . length > 0 ? members : undefined ;
350+ const labels = [
351+ ...( tail . memberLabels ??
352+ ( tail . callId !== undefined ? [ laneMemberLabel ( tail ) ] : [ ] ) ) ,
353+ ...( next . callId !== undefined ? [ laneMemberLabel ( next ) ] : [ ] ) ,
354+ ] ;
355+ return { ids, labels } ;
241356}
242357
243358/**
@@ -252,7 +367,15 @@ export function coalesceCallRows(tail: StreamRow, next: StreamRow): StreamRow {
252367 ? ( tail . detail ?? [ ] )
253368 : tail . pending === true
254369 ? [ ]
255- : appendRunLine ( [ ] , tail . stat ?? "answered" ) ;
370+ : appendRunLine (
371+ [ ] ,
372+ memberRunLine (
373+ laneMemberLabel ( tail ) ,
374+ tail . failed === true
375+ ? ( tail . stat ?? "call failed" )
376+ : ( tail . stat ?? "answered" ) ,
377+ ) ,
378+ ) ;
256379 // A run's body is the answers it collected; the argument view, table and diff
257380 // belong to a single call, which this row no longer stands alone for.
258381 const {
@@ -262,11 +385,12 @@ export function coalesceCallRows(tail: StreamRow, next: StreamRow): StreamRow {
262385 ...call
263386 } = next ;
264387 const inFlight = tail . outstanding ?? ( tail . pending === true ? 1 : 0 ) ;
265- const memberIds = laneMembers ( tail , next ) ;
388+ const members = laneMembers ( tail , next ) ;
266389 return {
267390 ...call ,
268391 callCount : ( tail . callCount ?? 1 ) + 1 ,
269- ...( memberIds !== undefined ? { memberIds } : { } ) ,
392+ ...( members . ids . length > 0 ? { memberIds : members . ids } : { } ) ,
393+ ...( members . labels . length > 0 ? { memberLabels : members . labels } : { } ) ,
270394 coalesced : true ,
271395 outstanding : inFlight + 1 ,
272396 ...( tail . failed === true ? { failed : true } : { } ) ,
0 commit comments