Detailed changes
  
  
    
    @@ -85,7 +85,6 @@ to assist developers in writing, debugging, and understanding code directly from
 			slog.Error(fmt.Sprintf("Failed to create app instance: %v", err))
 			return err
 		}
-		// Defer shutdown here so it runs for both interactive and non-interactive modes
 		defer app.Shutdown()
 
 		// Initialize MCP tools early for both modes
@@ -109,6 +108,7 @@ to assist developers in writing, debugging, and understanding code directly from
 			tea.WithAltScreen(),
 			tea.WithKeyReleases(),
 			tea.WithUniformKeyLayout(),
+			tea.WithContext(ctx),
 			tea.WithMouseCellMotion(),            // Use cell motion instead of all motion to reduce event flooding
 			tea.WithFilter(tui.MouseEventFilter), // Filter mouse events based on focus state
 		)
@@ -119,7 +119,6 @@ to assist developers in writing, debugging, and understanding code directly from
 			slog.Error(fmt.Sprintf("TUI run error: %v", err))
 			return fmt.Errorf("TUI error: %v", err)
 		}
-		app.Shutdown()
 		return nil
 	},
 }
  
  
  
    
    @@ -282,6 +282,9 @@ func (app *App) Subscribe(program *tea.Program) {
 
 // Shutdown performs a clean shutdown of the application
 func (app *App) Shutdown() {
+	if app.CoderAgent != nil {
+		app.CoderAgent.CancelAll()
+	}
 	app.cancelFuncsMutex.Lock()
 	for _, cancel := range app.watcherCancelFuncs {
 		cancel()
@@ -301,9 +304,6 @@ func (app *App) Shutdown() {
 		}
 		cancel()
 	}
-	if app.CoderAgent != nil {
-		app.CoderAgent.CancelAll()
-	}
 
 	for _, cleanup := range app.cleanupFuncs {
 		if cleanup != nil {
  
  
  
    
    @@ -403,7 +403,7 @@ func (a *agent) processGeneration(ctx context.Context, sessionID, content string
 		agentMessage, toolResults, err := a.streamAndHandleEvents(ctx, sessionID, msgHistory)
 		if err != nil {
 			if errors.Is(err, context.Canceled) {
-				agentMessage.AddFinish(message.FinishReasonCanceled)
+				agentMessage.AddFinish(message.FinishReasonCanceled, "Request cancelled", "")
 				a.messages.Update(context.Background(), agentMessage)
 				return a.err(ErrRequestCancelled)
 			}
@@ -454,11 +454,15 @@ func (a *agent) streamAndHandleEvents(ctx context.Context, sessionID string, msg
 	// Process each event in the stream.
 	for event := range eventChan {
 		if processErr := a.processEvent(ctx, sessionID, &assistantMsg, event); processErr != nil {
-			a.finishMessage(ctx, &assistantMsg, message.FinishReasonCanceled)
+			if errors.Is(processErr, context.Canceled) {
+				a.finishMessage(context.Background(), &assistantMsg, message.FinishReasonCanceled, "Request cancelled", "")
+			} else {
+				a.finishMessage(ctx, &assistantMsg, message.FinishReasonError, "API Error", processErr.Error())
+			}
 			return assistantMsg, nil, processErr
 		}
 		if ctx.Err() != nil {
-			a.finishMessage(context.Background(), &assistantMsg, message.FinishReasonCanceled)
+			a.finishMessage(context.Background(), &assistantMsg, message.FinishReasonCanceled, "Request cancelled", "")
 			return assistantMsg, nil, ctx.Err()
 		}
 	}
@@ -468,7 +472,7 @@ func (a *agent) streamAndHandleEvents(ctx context.Context, sessionID string, msg
 	for i, toolCall := range toolCalls {
 		select {
 		case <-ctx.Done():
-			a.finishMessage(context.Background(), &assistantMsg, message.FinishReasonCanceled)
+			a.finishMessage(context.Background(), &assistantMsg, message.FinishReasonCanceled, "Request cancelled", "")
 			// Make all future tool calls cancelled
 			for j := i; j < len(toolCalls); j++ {
 				toolResults[j] = message.ToolResult{
@@ -516,7 +520,7 @@ func (a *agent) streamAndHandleEvents(ctx context.Context, sessionID string, msg
 							IsError:    true,
 						}
 					}
-					a.finishMessage(ctx, &assistantMsg, message.FinishReasonPermissionDenied)
+					a.finishMessage(ctx, &assistantMsg, message.FinishReasonPermissionDenied, "Permission denied", "")
 					break
 				}
 			}
@@ -548,8 +552,8 @@ out:
 	return assistantMsg, &msg, err
 }
 
-func (a *agent) finishMessage(ctx context.Context, msg *message.Message, finishReson message.FinishReason) {
-	msg.AddFinish(finishReson)
+func (a *agent) finishMessage(ctx context.Context, msg *message.Message, finishReson message.FinishReason, message, details string) {
+	msg.AddFinish(finishReson, message, details)
 	_ = a.messages.Update(ctx, *msg)
 }
 
@@ -580,15 +584,10 @@ func (a *agent) processEvent(ctx context.Context, sessionID string, assistantMsg
 		assistantMsg.FinishToolCall(event.ToolCall.ID)
 		return a.messages.Update(ctx, *assistantMsg)
 	case provider.EventError:
-		if errors.Is(event.Error, context.Canceled) {
-			slog.Info(fmt.Sprintf("Event processing canceled for session: %s", sessionID))
-			return context.Canceled
-		}
-		slog.Error(event.Error.Error())
 		return event.Error
 	case provider.EventComplete:
 		assistantMsg.SetToolCalls(event.Response.ToolCalls)
-		assistantMsg.AddFinish(event.Response.FinishReason)
+		assistantMsg.AddFinish(event.Response.FinishReason, "", "")
 		if err := a.messages.Update(ctx, *assistantMsg); err != nil {
 			return fmt.Errorf("failed to update message: %w", err)
 		}
@@ -801,6 +800,16 @@ func (a *agent) CancelAll() {
 		a.Cancel(key.(string)) // key is sessionID
 		return true
 	})
+
+	timeout := time.After(5 * time.Second)
+	for a.IsBusy() {
+		select {
+		case <-timeout:
+			return
+		default:
+			time.Sleep(200 * time.Millisecond)
+		}
+	}
 }
 
 func (a *agent) UpdateModel() error {
  
  
  
    
    @@ -102,8 +102,10 @@ type ToolResult struct {
 func (ToolResult) isPart() {}
 
 type Finish struct {
-	Reason FinishReason `json:"reason"`
-	Time   int64        `json:"time"`
+	Reason  FinishReason `json:"reason"`
+	Time    int64        `json:"time"`
+	Message string       `json:"message,omitempty"`
+	Details string       `json:"details,omitempty"`
 }
 
 func (Finish) isPart() {}
@@ -308,7 +310,7 @@ func (m *Message) SetToolResults(tr []ToolResult) {
 	}
 }
 
-func (m *Message) AddFinish(reason FinishReason) {
+func (m *Message) AddFinish(reason FinishReason, message, details string) {
 	// remove any existing finish part
 	for i, part := range m.Parts {
 		if _, ok := part.(Finish); ok {
@@ -316,7 +318,7 @@ func (m *Message) AddFinish(reason FinishReason) {
 			break
 		}
 	}
-	m.Parts = append(m.Parts, Finish{Reason: reason, Time: time.Now().Unix()})
+	m.Parts = append(m.Parts, Finish{Reason: reason, Time: time.Now().Unix(), Message: message, Details: details})
 }
 
 func (m *Message) AddImageURL(url, detail string) {
  
  
  
    
    @@ -337,7 +337,7 @@ func (m *messageListCmp) updateToolCalls(msg message.Message, existingToolCalls
 	var cmds []tea.Cmd
 
 	for _, tc := range msg.ToolCalls() {
-		if cmd := m.updateOrAddToolCall(tc, existingToolCalls, msg.ID); cmd != nil {
+		if cmd := m.updateOrAddToolCall(msg, tc, existingToolCalls); cmd != nil {
 			cmds = append(cmds, cmd)
 		}
 	}
@@ -346,18 +346,21 @@ func (m *messageListCmp) updateToolCalls(msg message.Message, existingToolCalls
 }
 
 // updateOrAddToolCall updates an existing tool call or adds a new one.
-func (m *messageListCmp) updateOrAddToolCall(tc message.ToolCall, existingToolCalls map[int]messages.ToolCallCmp, messageID string) tea.Cmd {
+func (m *messageListCmp) updateOrAddToolCall(msg message.Message, tc message.ToolCall, existingToolCalls map[int]messages.ToolCallCmp) tea.Cmd {
 	// Try to find existing tool call
 	for index, existingTC := range existingToolCalls {
 		if tc.ID == existingTC.GetToolCall().ID {
 			existingTC.SetToolCall(tc)
+			if msg.FinishPart() != nil && msg.FinishPart().Reason == message.FinishReasonCanceled {
+				existingTC.SetCancelled()
+			}
 			m.listCmp.UpdateItem(index, existingTC)
 			return nil
 		}
 	}
 
 	// Add new tool call if not found
-	return m.listCmp.AppendItem(messages.NewToolCallCmp(messageID, tc))
+	return m.listCmp.AppendItem(messages.NewToolCallCmp(msg.ID, tc))
 }
 
 // handleNewAssistantMessage processes new assistant messages and their tool calls.
  
  
  
    
    @@ -130,6 +130,9 @@ func (m *editorCmp) Init() tea.Cmd {
 }
 
 func (m *editorCmp) send() tea.Cmd {
+	if m.app.CoderAgent == nil {
+		return util.ReportError(fmt.Errorf("coder agent is not initialized"))
+	}
 	if m.app.CoderAgent.IsSessionBusy(m.session.ID) {
 		return util.ReportWarn("Agent is working, please wait...")
 	}
  
  
  
    
    @@ -8,6 +8,7 @@ import (
 
 	tea "github.com/charmbracelet/bubbletea/v2"
 	"github.com/charmbracelet/lipgloss/v2"
+	"github.com/charmbracelet/x/ansi"
 
 	"github.com/charmbracelet/crush/internal/config"
 	"github.com/charmbracelet/crush/internal/fur/provider"
@@ -184,6 +185,7 @@ func (m *messageCmp) toMarkdown(content string) string {
 // markdownContent processes the message content and handles special states.
 // Returns appropriate content for thinking, finished, and error states.
 func (m *messageCmp) markdownContent() string {
+	t := styles.CurrentTheme()
 	content := m.message.Content().String()
 	if m.message.Role == message.Assistant {
 		thinking := m.message.IsThinking()
@@ -199,6 +201,12 @@ func (m *messageCmp) markdownContent() string {
 			content = ""
 		} else if finished && content == "" && finishedData.Reason == message.FinishReasonCanceled {
 			content = "*Canceled*"
+		} else if finished && content == "" && finishedData.Reason == message.FinishReasonError {
+			errTag := t.S().Base.Padding(0, 1).Background(t.Red).Foreground(t.White).Render("ERROR")
+			truncated := ansi.Truncate(finishedData.Message, m.textWidth()-2-lipgloss.Width(errTag), "...")
+			title := fmt.Sprintf("%s %s", errTag, t.S().Base.Foreground(t.FgHalfMuted).Render(truncated))
+			details := t.S().Base.Foreground(t.FgSubtle).Width(m.textWidth() - 2).Render(finishedData.Details)
+			return fmt.Sprintf("%s\n\n%s", title, details)
 		}
 	}
 	return m.toMarkdown(content)
  
  
  
    
    @@ -192,10 +192,8 @@ func (s *splashCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 				return s, s.initializeProject()
 			}
 		case key.Matches(msg, s.keyMap.No):
-			if s.needsProjectInit {
-				s.needsProjectInit = false
-				return s, util.CmdHandler(OnboardingCompleteMsg{})
-			}
+			s.selectedNo = true
+			return s, s.initializeProject()
 		default:
 			if s.needsAPIKey {
 				u, cmd := s.apiKeyInput.Update(msg)
  
  
  
    
    @@ -0,0 +1,147 @@
+package core_test
+
+import (
+	"fmt"
+	"image/color"
+	"testing"
+
+	"github.com/charmbracelet/crush/internal/tui/components/core"
+	"github.com/charmbracelet/x/exp/golden"
+)
+
+func TestStatus(t *testing.T) {
+	t.Parallel()
+
+	tests := []struct {
+		name  string
+		opts  core.StatusOpts
+		width int
+	}{
+		{
+			name: "Default",
+			opts: core.StatusOpts{
+				Title:       "Status",
+				Description: "Everything is working fine",
+			},
+			width: 80,
+		},
+		{
+			name: "WithCustomIcon",
+			opts: core.StatusOpts{
+				Icon:        "β",
+				Title:       "Success",
+				Description: "Operation completed successfully",
+			},
+			width: 80,
+		},
+		{
+			name: "NoIcon",
+			opts: core.StatusOpts{
+				NoIcon:      true,
+				Title:       "Info",
+				Description: "This status has no icon",
+			},
+			width: 80,
+		},
+		{
+			name: "WithColors",
+			opts: core.StatusOpts{
+				Icon:             "β ",
+				IconColor:        color.RGBA{255, 165, 0, 255}, // Orange
+				Title:            "Warning",
+				TitleColor:       color.RGBA{255, 255, 0, 255}, // Yellow
+				Description:      "This is a warning message",
+				DescriptionColor: color.RGBA{255, 0, 0, 255}, // Red
+			},
+			width: 80,
+		},
+		{
+			name: "WithExtraContent",
+			opts: core.StatusOpts{
+				Title:        "Build",
+				Description:  "Building project",
+				ExtraContent: "[2/5]",
+			},
+			width: 80,
+		},
+		{
+			name: "LongDescription",
+			opts: core.StatusOpts{
+				Title:       "Processing",
+				Description: "This is a very long description that should be truncated when the width is too small to display it completely without wrapping",
+			},
+			width: 60,
+		},
+		{
+			name: "NarrowWidth",
+			opts: core.StatusOpts{
+				Icon:        "β",
+				Title:       "Status",
+				Description: "Short message",
+			},
+			width: 30,
+		},
+		{
+			name: "VeryNarrowWidth",
+			opts: core.StatusOpts{
+				Icon:        "β",
+				Title:       "Test",
+				Description: "This will be truncated",
+			},
+			width: 20,
+		},
+		{
+			name: "EmptyDescription",
+			opts: core.StatusOpts{
+				Icon:  "β",
+				Title: "Title Only",
+			},
+			width: 80,
+		},
+		{
+			name: "AllFieldsWithExtraContent",
+			opts: core.StatusOpts{
+				Icon:             "π",
+				IconColor:        color.RGBA{0, 255, 0, 255}, // Green
+				Title:            "Deployment",
+				TitleColor:       color.RGBA{0, 0, 255, 255}, // Blue
+				Description:      "Deploying to production environment",
+				DescriptionColor: color.RGBA{128, 128, 128, 255}, // Gray
+				ExtraContent:     "v1.2.3",
+			},
+			width: 80,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			t.Parallel()
+
+			output := core.Status(tt.opts, tt.width)
+			golden.RequireEqual(t, []byte(output))
+		})
+	}
+}
+
+func TestStatusTruncation(t *testing.T) {
+	t.Parallel()
+
+	opts := core.StatusOpts{
+		Icon:         "β",
+		Title:        "Very Long Title",
+		Description:  "This is an extremely long description that definitely needs to be truncated",
+		ExtraContent: "[extra]",
+	}
+
+	// Test different widths to ensure truncation works correctly
+	widths := []int{20, 30, 40, 50, 60}
+
+	for _, width := range widths {
+		t.Run(fmt.Sprintf("Width%d", width), func(t *testing.T) {
+			t.Parallel()
+
+			output := core.Status(opts, width)
+			golden.RequireEqual(t, []byte(output))
+		})
+	}
+}
  
  
  
    
    @@ -0,0 +1 @@
+[38;2;0;255;0mπ[m [38;2;0;0;255mDeployment[m [38;2;128;128;128mDeploying to production environment[m v1.2.3
  
  
  
    
    @@ -0,0 +1 @@
+[38;2;18;199;143mβ[m [38;2;133;131;146mStatus[m [38;2;96;95;107mEverything is working fine[m
  
  
  
    
    @@ -0,0 +1 @@
+[38;2;18;199;143mβ[m [38;2;133;131;146mTitle Only[m [38;2;96;95;107m[m
  
  
  
    
    @@ -0,0 +1 @@
+[38;2;18;199;143mβ[m [38;2;133;131;146mProcessing[m [38;2;96;95;107mThis is a very long description that should beβ¦[m
  
  
  
    
    @@ -0,0 +1 @@
+[38;2;18;199;143mβ[m [38;2;133;131;146mStatus[m [38;2;96;95;107mShort message[m
  
  
  
    
    @@ -0,0 +1 @@
+[38;2;133;131;146mInfo[m [38;2;96;95;107mThis status has no icon[m
  
  
  
    
    @@ -0,0 +1 @@
+[38;2;18;199;143mβ[m [38;2;133;131;146mTest[m [38;2;96;95;107mThis will beβ¦[m
  
  
  
    
    @@ -0,0 +1 @@
+[38;2;255;165;0mβ [m [38;2;255;255;0mWarning[m [38;2;255;0;0mThis is a warning message[m
  
  
  
    
    @@ -0,0 +1 @@
+[38;2;18;199;143mβ[m [38;2;133;131;146mSuccess[m [38;2;96;95;107mOperation completed successfully[m
  
  
  
    
    @@ -0,0 +1 @@
+[38;2;18;199;143mβ[m [38;2;133;131;146mBuild[m [38;2;96;95;107mBuilding project[m [2/5]
  
  
  
    
    @@ -0,0 +1 @@
+[38;2;18;199;143mβ[m [38;2;133;131;146mVery Long Title[m [38;2;96;95;107m[m [extra]
  
  
  
    
    @@ -0,0 +1 @@
+[38;2;18;199;143mβ[m [38;2;133;131;146mVery Long Title[m [38;2;96;95;107mThiβ¦[m [extra]
  
  
  
    
    @@ -0,0 +1 @@
+[38;2;18;199;143mβ[m [38;2;133;131;146mVery Long Title[m [38;2;96;95;107mThis is an exβ¦[m [extra]
  
  
  
    
    @@ -0,0 +1 @@
+[38;2;18;199;143mβ[m [38;2;133;131;146mVery Long Title[m [38;2;96;95;107mThis is an extremely loβ¦[m [extra]
  
  
  
    
    @@ -0,0 +1 @@
+[38;2;18;199;143mβ[m [38;2;133;131;146mVery Long Title[m [38;2;96;95;107mThis is an extremely long descripβ¦[m [extra]
  
  
  
    
    @@ -60,6 +60,9 @@ type permissionDialogCmp struct {
 	cachedContent string
 	contentDirty  bool
 
+	positionRow int // Row position for dialog
+	positionCol int // Column position for dialog
+
 	keyMap KeyMap
 }
 
@@ -446,6 +449,10 @@ func (p *permissionDialogCmp) render() string {
 	p.contentViewPort.SetHeight(contentHeight)
 	p.contentViewPort.SetContent(contentFinal)
 
+	p.positionRow = p.wHeight / 2
+	p.positionRow -= (contentHeight + 9) / 2
+	p.positionRow -= 3 // Move dialog slightly higher than middle
+
 	var contentHelp string
 	if p.supportsDiffView() {
 		contentHelp = help.New().View(p.keyMap)
@@ -509,7 +516,11 @@ func (p *permissionDialogCmp) SetSize() tea.Cmd {
 	if oldWidth != p.width || oldHeight != p.height {
 		p.contentDirty = true
 	}
-
+	p.positionRow = p.wHeight / 2
+	p.positionRow -= p.height / 2
+	p.positionRow -= 3 // Move dialog slightly higher than middle
+	p.positionCol = p.wWidth / 2
+	p.positionCol -= p.width / 2
 	return nil
 }
 
@@ -529,9 +540,5 @@ func (p *permissionDialogCmp) ID() dialogs.DialogID {
 
 // Position implements PermissionDialogCmp.
 func (p *permissionDialogCmp) Position() (int, int) {
-	row := (p.wHeight / 2) - 2 // Just a bit above the center
-	row -= p.height / 2
-	col := p.wWidth / 2
-	col -= p.width / 2
-	return row, col
+	return p.positionRow, p.positionCol
 }
  
  
  
    
    @@ -315,7 +315,6 @@ func (a *appModel) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd {
 	// dialogs
 	case key.Matches(msg, a.keyMap.Quit):
 		if a.dialog.ActiveDialogID() == quit.QuitDialogID {
-			// if the quit dialog is already open, close the app
 			return tea.Quit
 		}
 		return util.CmdHandler(dialogs.OpenDialogMsg{
@@ -324,20 +323,21 @@ func (a *appModel) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd {
 
 	case key.Matches(msg, a.keyMap.Commands):
 		if a.dialog.ActiveDialogID() == commands.CommandsDialogID {
-			// If the commands dialog is already open, close it
 			return util.CmdHandler(dialogs.CloseDialogMsg{})
 		}
 		if a.dialog.HasDialogs() {
-			return nil // Don't open commands dialog if another dialog is active
+			return nil
 		}
 		return util.CmdHandler(dialogs.OpenDialogMsg{
 			Model: commands.NewCommandDialog(a.selectedSessionID),
 		})
 	case key.Matches(msg, a.keyMap.Sessions):
 		if a.dialog.ActiveDialogID() == sessions.SessionsDialogID {
-			// If the sessions dialog is already open, close it
 			return util.CmdHandler(dialogs.CloseDialogMsg{})
 		}
+		if a.dialog.HasDialogs() && a.dialog.ActiveDialogID() != commands.CommandsDialogID {
+			return nil
+		}
 		var cmds []tea.Cmd
 		if a.dialog.ActiveDialogID() == commands.CommandsDialogID {
 			// If the commands dialog is open, close it first