@@ -10,8 +10,9 @@ import {
1010 isJsonRpcNotification ,
1111 isJsonRpcRequest ,
1212 isJsonRpcResponse ,
13+ type UserShellExecuteParams ,
1314} from "@shared/types/session-events" ;
14- import { memo , useEffect , useMemo , useRef } from "react" ;
15+ import { memo , useLayoutEffect , useMemo , useRef } from "react" ;
1516import { GitActionMessage , parseGitActionMessage } from "./GitActionMessage" ;
1617import { GitActionResult } from "./GitActionResult" ;
1718import { SessionFooter } from "./SessionFooter" ;
@@ -20,8 +21,13 @@ import {
2021 SessionUpdateView ,
2122} from "./session-update/SessionUpdateView" ;
2223import { UserMessage } from "./session-update/UserMessage" ;
24+ import {
25+ type UserShellExecute ,
26+ UserShellExecuteView ,
27+ } from "./session-update/UserShellExecuteView" ;
2328
2429interface Turn {
30+ type : "turn" ;
2531 id : string ;
2632 promptId : number ;
2733 userContent : string ;
@@ -32,6 +38,8 @@ interface Turn {
3238 toolCalls : Map < string , ToolCall > ;
3339}
3440
41+ type ConversationItem = Turn | UserShellExecute ;
42+
3543interface ConversationViewProps {
3644 events : AcpMessage [ ] ;
3745 isPromptPending : boolean ;
@@ -46,31 +54,39 @@ export function ConversationView({
4654 isCloud = false ,
4755} : ConversationViewProps ) {
4856 const scrollRef = useRef < HTMLDivElement > ( null ) ;
49- const turns = useMemo ( ( ) => buildTurns ( events ) , [ events ] ) ;
50- const lastTurn = turns [ turns . length - 1 ] ;
57+ const items = useMemo ( ( ) => buildConversationItems ( events ) , [ events ] ) ;
58+ const lastTurn = items . filter ( ( i ) : i is Turn => i . type === "turn" ) . pop ( ) ;
5159 const lastTurnComplete = lastTurn ?. isComplete ?? true ;
5260
53- useEffect ( ( ) => {
61+ // Scroll to bottom on initial mount
62+ const hasScrolledRef = useRef ( false ) ;
63+ useLayoutEffect ( ( ) => {
64+ if ( hasScrolledRef . current ) return ;
5465 const el = scrollRef . current ;
55- if ( el ) {
66+ if ( el && items . length > 0 ) {
5667 el . scrollTop = el . scrollHeight ;
68+ hasScrolledRef . current = true ;
5769 }
58- } , [ ] ) ;
70+ } , [ items ] ) ;
5971
6072 return (
6173 < div
6274 ref = { scrollRef }
6375 className = "scrollbar-hide flex-1 overflow-auto bg-white p-2 pb-16 dark:bg-gray-1"
6476 >
6577 < div className = "flex flex-col gap-3" >
66- { turns . map ( ( turn ) => (
67- < TurnView
68- key = { turn . id }
69- turn = { turn }
70- repoPath = { repoPath }
71- isCloud = { isCloud }
72- />
73- ) ) }
78+ { items . map ( ( item ) =>
79+ item . type === "turn" ? (
80+ < TurnView
81+ key = { item . id }
82+ turn = { item }
83+ repoPath = { repoPath }
84+ isCloud = { isCloud }
85+ />
86+ ) : (
87+ < UserShellExecuteView key = { item . id } item = { item } />
88+ ) ,
89+ ) }
7490 </ div >
7591 < SessionFooter
7692 isPromptPending = { isPromptPending || ! lastTurnComplete }
@@ -138,20 +154,38 @@ const TurnView = memo(function TurnView({
138154
139155// --- Event Processing ---
140156
141- function buildTurns ( events : AcpMessage [ ] ) : Turn [ ] {
142- const turns : Turn [ ] = [ ] ;
143- let current : Turn | null = null ;
157+ function buildConversationItems ( events : AcpMessage [ ] ) : ConversationItem [ ] {
158+ const items : ConversationItem [ ] = [ ] ;
159+ let currentTurn : Turn | null = null ;
144160 // Map prompt request IDs to their turns for matching responses
145161 const pendingPrompts = new Map < number , Turn > ( ) ;
162+ let shellExecuteCounter = 0 ;
146163
147164 for ( const event of events ) {
148165 const msg = event . message ;
149166
167+ // User shell execute notification - standalone item
168+ if (
169+ isJsonRpcNotification ( msg ) &&
170+ msg . method === "_array/user_shell_execute"
171+ ) {
172+ const params = msg . params as UserShellExecuteParams ;
173+ items . push ( {
174+ type : "user_shell_execute" ,
175+ id : `shell-exec-${ shellExecuteCounter ++ } ` ,
176+ command : params . command ,
177+ cwd : params . cwd ,
178+ result : params . result ,
179+ } ) ;
180+ continue ;
181+ }
182+
150183 // session/prompt request - starts a new turn
151184 if ( isJsonRpcRequest ( msg ) && msg . method === "session/prompt" ) {
152185 const userContent = extractUserContent ( msg . params ) ;
153186
154- current = {
187+ currentTurn = {
188+ type : "turn" ,
155189 id : `turn-${ msg . id } ` ,
156190 promptId : msg . id ,
157191 userContent,
@@ -160,10 +194,10 @@ function buildTurns(events: AcpMessage[]): Turn[] {
160194 durationMs : 0 ,
161195 toolCalls : new Map ( ) ,
162196 } ;
163- current . durationMs = - event . ts ; // Will add end timestamp later
197+ currentTurn . durationMs = - event . ts ; // Will add end timestamp later
164198
165- pendingPrompts . set ( msg . id , current ) ;
166- turns . push ( current ) ;
199+ pendingPrompts . set ( msg . id , currentTurn ) ;
200+ items . push ( currentTurn ) ;
167201 continue ;
168202 }
169203
@@ -182,24 +216,24 @@ function buildTurns(events: AcpMessage[]): Turn[] {
182216 if (
183217 isJsonRpcNotification ( msg ) &&
184218 msg . method === "session/update" &&
185- current
219+ currentTurn
186220 ) {
187221 const update = ( msg . params as SessionNotification ) ?. update ;
188222 if ( ! update ) continue ;
189223
190- processSessionUpdate ( current , update ) ;
224+ processSessionUpdate ( currentTurn , update ) ;
191225 continue ;
192226 }
193227
194228 // PostHog console messages
195229 if (
196230 isJsonRpcNotification ( msg ) &&
197231 msg . method === "_posthog/console" &&
198- current
232+ currentTurn
199233 ) {
200234 const params = msg . params as { level ?: string ; message ?: string } ;
201235 if ( params ?. message ) {
202- current . items . push ( {
236+ currentTurn . items . push ( {
203237 sessionUpdate : "console" ,
204238 level : params . level ?? "info" ,
205239 message : params . message ,
@@ -209,16 +243,25 @@ function buildTurns(events: AcpMessage[]): Turn[] {
209243 }
210244 }
211245
212- return turns ;
246+ return items ;
247+ }
248+
249+ interface TextBlockWithMeta {
250+ type : "text" ;
251+ text : string ;
252+ _meta ?: { ui ?: { hidden ?: boolean } } ;
213253}
214254
215255function extractUserContent ( params : unknown ) : string {
216256 const p = params as { prompt ?: ContentBlock [ ] } ;
217257 if ( ! p ?. prompt ?. length ) return "" ;
218258
219- const textBlock = p . prompt . find (
220- ( b ) : b is { type : "text" ; text : string } => b . type === "text" ,
221- ) ;
259+ // Find first visible text block (skip hidden context blocks)
260+ const textBlock = p . prompt . find ( ( b ) : b is TextBlockWithMeta => {
261+ if ( b . type !== "text" ) return false ;
262+ const meta = ( b as TextBlockWithMeta ) . _meta ;
263+ return ! meta ?. ui ?. hidden ;
264+ } ) ;
222265 return textBlock ?. text ?? "" ;
223266}
224267
0 commit comments