@@ -261,6 +261,11 @@ export function ConnectApp(props: ConnectAppProps): React.ReactElement {
261261 // past the newest line (or esc) returns to the composer. The highlighted
262262 // line renders the cyan selection glyph in its gutter.
263263 const [ navKey , setNavKey ] = useState < string | null > ( null )
264+ // Local ✦ lines in the transcript, for things the CLIENT did that the record
265+ // log will never carry: so far, /stop. It belongs in the conversation because
266+ // it is an event in the conversation — the notice bar above the composer is
267+ // for transient status, and scrolls away with nothing to show you asked.
268+ const [ chatNotes , setChatNotes ] = useState < readonly { key : string ; text : string } [ ] > ( [ ] )
264269 // Lines opened in place with → while highlighted: a grp:* fold expands into
265270 // its tool calls, a clamped long body un-clamps. ← closes them again.
266271 const [ openedKeys , setOpenedKeys ] = useState < ReadonlySet < string > > ( new Set ( ) )
@@ -307,10 +312,15 @@ export function ConnectApp(props: ConnectAppProps): React.ReactElement {
307312 // one-line progress block up top (sandboxProgress), not as transcript rows.
308313 // Each turn's closing duration/cost summary is dropped too (see
309314 // reshapeTranscript) — the footer carries the session's spend.
310- const { items } = useMemo (
311- ( ) => reshapeTranscript ( snapshot . records , props . minRenderFeedSeq ) ,
312- [ snapshot . records , props . minRenderFeedSeq ] ,
313- )
315+ const { items } = useMemo ( ( ) => {
316+ const shaped = reshapeTranscript ( snapshot . records , props . minRenderFeedSeq )
317+ // Client-side notes land at the end: they describe what you just did, so
318+ // they belong under everything the server has sent so far.
319+ for ( const note of chatNotes ) {
320+ shaped . items . push ( { key : note . key , kind : 'notice' , text : note . text , spaceBefore : true } )
321+ }
322+ return shaped
323+ } , [ snapshot . records , props . minRenderFeedSeq , chatNotes ] )
314324
315325 // Footer spend: the server's ledger total (the session frame's four cost
316326 // columns — the billing authority, resent on every cost tick) with the
@@ -564,7 +574,14 @@ export function ConnectApp(props: ConnectAppProps): React.ReactElement {
564574 try {
565575 if ( text === '/stop' ) {
566576 const s = await api . stopAgentSession ( sessionId )
567- setNotice ( `stop requested (${ s . status } ) — the conversation survives` )
577+ setNotice ( null )
578+ setChatNotes ( ( prev ) => [
579+ ...prev ,
580+ {
581+ key : `note${ prev . length } ` ,
582+ text : `Stopped the agent (${ s . status } ). The conversation is saved. Send a message to pick it back up.` ,
583+ } ,
584+ ] )
568585 return
569586 }
570587 // Show the message as queued, then post it. The POST returns the
@@ -899,6 +916,15 @@ export function ConnectApp(props: ConnectAppProps): React.ReactElement {
899916 return rowViewport ( allRows . length , viewBudget , anchor )
900917 } , [ allRows , viewBudget , scrollAnchor ] )
901918
919+ // The one row that wears the ▶ marker: the highlighted block's FIRST row with
920+ // a gutter glyph. The whole block tints, but the marker points at a single
921+ // line — a block with nested tool activity has a glyph on the call and on its
922+ // ⎿ result, and marking both reads as two separate selections.
923+ const markerRowId = useMemo ( ( ) => {
924+ if ( navKey === null ) return null
925+ return allRows . find ( ( r ) => navKeyOf ( r ) === navKey && r . gutter ) ?. id ?? null
926+ } , [ allRows , navKey ] )
927+
902928 // Move the window by `delta` ROWS. Reaching the last row re-pins it to the
903929 // bottom, so streamed content follows again.
904930 const scrollByRows = useCallback (
@@ -1233,6 +1259,7 @@ export function ConnectApp(props: ConnectAppProps): React.ReactElement {
12331259 selected = {
12341260 navKey !== null && navKeyOf ( row ) === navKey && ( ! row . spacer || row . panel === true )
12351261 }
1262+ marker = { row . id === markerRowId }
12361263 // Both ticking values are passed as constants to rows that don't
12371264 // use them, so React.memo skips those rows entirely: the
12381265 // once-a-second clock and the pulse repaint the live lines, not
@@ -1967,12 +1994,17 @@ const RowLine = React.memo(function RowLine({
19671994 row,
19681995 cols,
19691996 selected,
1997+ marker,
19701998 seconds,
19711999 pulseOn,
19722000} : {
19732001 row : TranscriptRow
19742002 cols : number
19752003 selected : boolean
2004+ // Whether THIS row carries the ▶ selection marker in its gutter. Every row of
2005+ // the highlighted block is `selected` (they all tint), but only one is the
2006+ // marker row — see markerRowId.
2007+ marker : boolean
19762008 // The row's ticking duration, resolved here so the once-a-second tick
19772009 // repaints this line instead of rebuilding the transcript's rows.
19782010 seconds : number
@@ -2015,8 +2047,9 @@ const RowLine = React.memo(function RowLine({
20152047 { row . panel && < Box width = { MESSAGE_PAD } flexShrink = { 0 } /> }
20162048 { row . indent ? < Box width = { row . indent } flexShrink = { 0 } /> : null }
20172049 < Box width = { GUTTER_COLS } flexShrink = { 0 } >
2018- { /* The gutter glyph, or the selection marker in its place — same
2019- 1-char slot, so the text never shifts when the highlight lands.
2050+ { /* The gutter glyph, or the selection marker in its place on the one
2051+ marker row — same 1-char slot, so text never shifts when the
2052+ highlight lands.
20202053 A live row's mark pulses by DIMMING on the off beat: the glyph
20212054 itself never changes, so the column holds still and the eye reads
20222055 a heartbeat rather than a character swapping in and out. */ }
@@ -2025,7 +2058,7 @@ const RowLine = React.memo(function RowLine({
20252058 dimColor = { ! selected && ! row . gutter ?. pulse && ( row . gutter ?. dim ?? false ) }
20262059 wrap = "truncate"
20272060 >
2028- { selected && row . gutter ? SELECTION_GLYPH : ( row . gutter ?. text ?? '' ) }
2061+ { marker ? SELECTION_GLYPH : ( row . gutter ?. text ?? '' ) }
20292062 </ Text >
20302063 </ Box >
20312064 < Box flexGrow = { 1 } flexShrink = { 1 } overflow = "hidden" >
0 commit comments