@@ -208,9 +208,14 @@ type UI struct {
208208 isCanceling bool
209209
210210 // bangMode tracks whether the editor is in bang (!) shell mode.
211− bangMode bool
212− bangWasEmpty bool // true when bang prompt became empty on last keystroke
213− pendingShellItem *chat.ShellItem
211+ bangMode bool
212+ bangWasEmpty bool // true when bang prompt became empty on last keystroke
213+
214+ // pendingBangCommand holds a shell command that was issued before
215+ // the session finished loading. The loadSessionMsg handler creates
216+ // the pending UI item and starts execution once the chat list is
217+ // stable, eliminating races between session load and shell output.
218+ pendingBangCommand string
214219
215220 // bangCancel cancels a running bang-mode shell command. Nil when no
216221 // bang command is in progress. Set by runShellCommand, cleared by
@@ -622,16 +627,11 @@ func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
622627 if cmd := m.autoExpandPillsIfReasonable(); cmd != nil {
623628 cmds = append(cmds, cmd)
624629 }
625− // Re-append pending shell item if session load wiped it.
626− if m.pendingShellItem != nil {
627− m.chat.AppendMessages(m.pendingShellItem)
628− if cmd := m.chat.ScrollToBottomAndAnimate(); cmd != nil {
629− cmds = append(cmds, cmd)
630− }
631− if cmd := m.pendingShellItem.StartAnimation(); cmd != nil {
632− cmds = append(cmds, cmd)
633− }
634− m.pendingShellItem = nil
630+ // If a bang command was issued before the session finished
631+ // loading, start it now that the chat list is stable.
632+ if m.pendingBangCommand != "" {
633+ cmds = append(cmds, m.runShellCommandInternal(m.pendingBangCommand, true))
634+ m.pendingBangCommand = ""
635635 }
636636 if hasInProgressTodo(m.session.Todos) {
637637 // only start spinner if there is an in-progress todo
@@ -994,8 +994,6 @@ func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
994994 m.bangCancel = nil
995995 }
996996 // Complete the pending shell item if it exists, otherwise create a new one.
997− // During session creation the pending item lives in m.pendingShellItem
998− // rather than the chat list, so check both locations.
999997 completed := false
1000998 if msg.PendingID != "" {
1001999 if item := m.chat.MessageItem(msg.PendingID); item != nil {
@@ -1007,19 +1005,6 @@ func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
10071005 completed = true
10081006 }
10091007 }
1010− if !completed && m.pendingShellItem != nil && m.pendingShellItem.ID() == msg.PendingID {
1011− m.pendingShellItem.Complete(msg.Output, msg.ExitCode)
1012− // The item is still stashed; it will be appended when the
1013− // session load handler fires. Append it now so the result
1014− // is visible immediately and clear the stash to prevent
1015− // a duplicate append later.
1016− m.chat.AppendMessages(m.pendingShellItem)
1017− if cmd := m.chat.ScrollToBottomAndAnimate(); cmd != nil {
1018− cmds = append(cmds, cmd)
1019− }
1020− m.pendingShellItem = nil
1021− completed = true
1022− }
10231008 }
10241009 if !completed {
10251010 item := chat.NewShellItem(m.com.Styles, msg.Command, msg.Output, msg.ExitCode)
@@ -3471,9 +3456,15 @@ func (m *UI) sendMessage(content string, attachments ...message.Attachment) tea.
34713456// runShellCommand executes a shell command server-side without triggering
34723457// the LLM. The result is displayed as a tool-style item in the chat.
34733458func (m *UI) runShellCommand(command string) tea.Cmd {
3459+ return m.runShellCommandInternal(command, false)
3460+}
3461+
3462+// runShellCommandInternal is the shared implementation for bang-mode shell
3463+// execution. isFirstMessage indicates the command is the first user message
3464+// in a newly created session, which triggers title generation.
3465+func (m *UI) runShellCommandInternal(command string, isFirstMessage bool) tea.Cmd {
34743466 var cmds []tea.Cmd
3475− creatingSession := !m.hasSession()
3476− if creatingSession {
3467+ if !m.hasSession() {
34773468 newSession, err := m.com.Workspace.CreateSession(context.Background(), "New Session")
34783469 if err != nil {
34793470 return util.ReportError(err)
@@ -3486,6 +3477,10 @@ func (m *UI) runShellCommand(command string) tea.Cmd {
34863477 cmds = append(cmds, m.loadSession(newSession.ID))
34873478 }
34883479 m.setState(uiChat, m.focus)
3480+ // Defer shell execution until loadSessionMsg fires so the chat
3481+ // list is stable before we add items or start streaming.
3482+ m.pendingBangCommand = command
3483+ return tea.Batch(cmds...)
34893484 }
34903485
34913486 sessionID := m.session.ID
@@ -3493,18 +3488,12 @@ func (m *UI) runShellCommand(command string) tea.Cmd {
34933488
34943489 // Append a pending shell item immediately so the user sees feedback.
34953490 pendingItem := chat.NewPendingShellItem(m.com.Styles, command)
3496− if creatingSession {
3497− // loadSession will wipe the chat list via SetMessages.
3498− // Stash the item and re-append after messages load.
3499− m.pendingShellItem = pendingItem
3500− } else {
3501− m.chat.AppendMessages(pendingItem)
3502− if cmd := m.chat.ScrollToBottomAndAnimate(); cmd != nil {
3503− cmds = append(cmds, cmd)
3504− }
3505− if cmd := pendingItem.StartAnimation(); cmd != nil {
3506− cmds = append(cmds, cmd)
3507− }
3491+ m.chat.AppendMessages(pendingItem)
3492+ if cmd := m.chat.ScrollToBottomAndAnimate(); cmd != nil {
3493+ cmds = append(cmds, cmd)
3494+ }
3495+ if cmd := pendingItem.StartAnimation(); cmd != nil {
3496+ cmds = append(cmds, cmd)
35083497 }
35093498
35103499 // Stream output via channel. The progress callback writes chunks
@@ -3533,7 +3522,7 @@ func (m *UI) runShellCommand(command string) tea.Cmd {
35333522 m.bangCancel = cancel
35343523
35353524 cmds = append(cmds, func() tea.Msg {
3536− resp, err := m.com.Workspace.AgentRunShellCommand(ctx, sessionID, command, contentWidth, onProgress)
3525+ resp, err := m.com.Workspace.AgentRunShellCommand(ctx, sessionID, command, contentWidth, onProgress, isFirstMessage)
35373526 close(streamCh)
35383527 if err != nil && !errors.Is(err, context.Canceled) {
35393528 return util.InfoMsg{