on main help sign in
Merge pull request #170 from charmbracelet/small-fixes
6895ea3Kujtim Hoxha
cmd/root.go+1−2
@@ -85,7 +85,6 @@ to assist developers in writing, debugging, and understanding code directly from
85 slog.Error(fmt.Sprintf("Failed to create app instance: %v", err))
85 slog.Error(fmt.Sprintf("Failed to create app instance: %v", err))
86 return err
86 return err
87 }
87 }
88− // Defer shutdown here so it runs for both interactive and non-interactive modes
89 defer app.Shutdown()
88 defer app.Shutdown()
90
89
91 // Initialize MCP tools early for both modes
90 // Initialize MCP tools early for both modes
@@ -109,6 +108,7 @@ to assist developers in writing, debugging, and understanding code directly from
109 tea.WithAltScreen(),
108 tea.WithAltScreen(),
110 tea.WithKeyReleases(),
109 tea.WithKeyReleases(),
111 tea.WithUniformKeyLayout(),
110 tea.WithUniformKeyLayout(),
111+ tea.WithContext(ctx),
112 tea.WithMouseCellMotion(), // Use cell motion instead of all motion to reduce event flooding
112 tea.WithMouseCellMotion(), // Use cell motion instead of all motion to reduce event flooding
113 tea.WithFilter(tui.MouseEventFilter), // Filter mouse events based on focus state
113 tea.WithFilter(tui.MouseEventFilter), // Filter mouse events based on focus state
114 )
114 )
@@ -119,7 +119,6 @@ to assist developers in writing, debugging, and understanding code directly from
119 slog.Error(fmt.Sprintf("TUI run error: %v", err))
119 slog.Error(fmt.Sprintf("TUI run error: %v", err))
120 return fmt.Errorf("TUI error: %v", err)
120 return fmt.Errorf("TUI error: %v", err)
121 }
121 }
122− app.Shutdown()
123 return nil
122 return nil
124 },
123 },
125}
124}
internal/app/app.go+3−3
@@ -282,6 +282,9 @@ func (app *App) Subscribe(program *tea.Program) {
282
282
283// Shutdown performs a clean shutdown of the application
283// Shutdown performs a clean shutdown of the application
284func (app *App) Shutdown() {
284func (app *App) Shutdown() {
285+ if app.CoderAgent != nil {
286+ app.CoderAgent.CancelAll()
287+ }
285 app.cancelFuncsMutex.Lock()
288 app.cancelFuncsMutex.Lock()
286 for _, cancel := range app.watcherCancelFuncs {
289 for _, cancel := range app.watcherCancelFuncs {
287 cancel()
290 cancel()
@@ -301,9 +304,6 @@ func (app *App) Shutdown() {
301 }
304 }
302 cancel()
305 cancel()
303 }
306 }
304− if app.CoderAgent != nil {
305− app.CoderAgent.CancelAll()
306− }
307
307
308 for _, cleanup := range app.cleanupFuncs {
308 for _, cleanup := range app.cleanupFuncs {
309 if cleanup != nil {
309 if cleanup != nil {
internal/llm/agent/agent.go+22−13
@@ -403,7 +403,7 @@ func (a *agent) processGeneration(ctx context.Context, sessionID, content string
403 agentMessage, toolResults, err := a.streamAndHandleEvents(ctx, sessionID, msgHistory)
403 agentMessage, toolResults, err := a.streamAndHandleEvents(ctx, sessionID, msgHistory)
404 if err != nil {
404 if err != nil {
405 if errors.Is(err, context.Canceled) {
405 if errors.Is(err, context.Canceled) {
406− agentMessage.AddFinish(message.FinishReasonCanceled)
406+ agentMessage.AddFinish(message.FinishReasonCanceled, "Request cancelled", "")
407 a.messages.Update(context.Background(), agentMessage)
407 a.messages.Update(context.Background(), agentMessage)
408 return a.err(ErrRequestCancelled)
408 return a.err(ErrRequestCancelled)
409 }
409 }
@@ -454,11 +454,15 @@ func (a *agent) streamAndHandleEvents(ctx context.Context, sessionID string, msg
454 // Process each event in the stream.
454 // Process each event in the stream.
455 for event := range eventChan {
455 for event := range eventChan {
456 if processErr := a.processEvent(ctx, sessionID, &assistantMsg, event); processErr != nil {
456 if processErr := a.processEvent(ctx, sessionID, &assistantMsg, event); processErr != nil {
457+ if errors.Is(processErr, context.Canceled) {
458+ a.finishMessage(context.Background(), &assistantMsg, message.FinishReasonCanceled, "Request cancelled", "")
459+ } else {
457− a.finishMessage(ctx, &assistantMsg, message.FinishReasonCanceled)
460+ a.finishMessage(ctx, &assistantMsg, message.FinishReasonError, "API Error", processErr.Error())
461+ }
458 return assistantMsg, nil, processErr
462 return assistantMsg, nil, processErr
459 }
463 }
460 if ctx.Err() != nil {
464 if ctx.Err() != nil {
461− a.finishMessage(context.Background(), &assistantMsg, message.FinishReasonCanceled)
465+ a.finishMessage(context.Background(), &assistantMsg, message.FinishReasonCanceled, "Request cancelled", "")
462 return assistantMsg, nil, ctx.Err()
466 return assistantMsg, nil, ctx.Err()
463 }
467 }
464 }
468 }
@@ -468,7 +472,7 @@ func (a *agent) streamAndHandleEvents(ctx context.Context, sessionID string, msg
468 for i, toolCall := range toolCalls {
472 for i, toolCall := range toolCalls {
469 select {
473 select {
470 case <-ctx.Done():
474 case <-ctx.Done():
471− a.finishMessage(context.Background(), &assistantMsg, message.FinishReasonCanceled)
475+ a.finishMessage(context.Background(), &assistantMsg, message.FinishReasonCanceled, "Request cancelled", "")
472 // Make all future tool calls cancelled
476 // Make all future tool calls cancelled
473 for j := i; j < len(toolCalls); j++ {
477 for j := i; j < len(toolCalls); j++ {
474 toolResults[j] = message.ToolResult{
478 toolResults[j] = message.ToolResult{
@@ -516,7 +520,7 @@ func (a *agent) streamAndHandleEvents(ctx context.Context, sessionID string, msg
516 IsError: true,
520 IsError: true,
517 }
521 }
518 }
522 }
519− a.finishMessage(ctx, &assistantMsg, message.FinishReasonPermissionDenied)
523+ a.finishMessage(ctx, &assistantMsg, message.FinishReasonPermissionDenied, "Permission denied", "")
520 break
524 break
521 }
525 }
522 }
526 }
@@ -548,8 +552,8 @@ out:
548 return assistantMsg, &msg, err
552 return assistantMsg, &msg, err
549}
553}
550
554
551−func (a *agent) finishMessage(ctx context.Context, msg *message.Message, finishReson message.FinishReason) {
555+func (a *agent) finishMessage(ctx context.Context, msg *message.Message, finishReson message.FinishReason, message, details string) {
552− msg.AddFinish(finishReson)
556+ msg.AddFinish(finishReson, message, details)
553 _ = a.messages.Update(ctx, *msg)
557 _ = a.messages.Update(ctx, *msg)
554}
558}
555
559
@@ -580,15 +584,10 @@ func (a *agent) processEvent(ctx context.Context, sessionID string, assistantMsg
580 assistantMsg.FinishToolCall(event.ToolCall.ID)
584 assistantMsg.FinishToolCall(event.ToolCall.ID)
581 return a.messages.Update(ctx, *assistantMsg)
585 return a.messages.Update(ctx, *assistantMsg)
582 case provider.EventError:
586 case provider.EventError:
583− if errors.Is(event.Error, context.Canceled) {
584− slog.Info(fmt.Sprintf("Event processing canceled for session: %s", sessionID))
585− return context.Canceled
586− }
587− slog.Error(event.Error.Error())
588 return event.Error
587 return event.Error
589 case provider.EventComplete:
588 case provider.EventComplete:
590 assistantMsg.SetToolCalls(event.Response.ToolCalls)
589 assistantMsg.SetToolCalls(event.Response.ToolCalls)
591− assistantMsg.AddFinish(event.Response.FinishReason)
590+ assistantMsg.AddFinish(event.Response.FinishReason, "", "")
592 if err := a.messages.Update(ctx, *assistantMsg); err != nil {
591 if err := a.messages.Update(ctx, *assistantMsg); err != nil {
593 return fmt.Errorf("failed to update message: %w", err)
592 return fmt.Errorf("failed to update message: %w", err)
594 }
593 }
@@ -801,6 +800,16 @@ func (a *agent) CancelAll() {
801 a.Cancel(key.(string)) // key is sessionID
800 a.Cancel(key.(string)) // key is sessionID
802 return true
801 return true
803 })
802 })
803+
804+ timeout := time.After(5 * time.Second)
805+ for a.IsBusy() {
806+ select {
807+ case <-timeout:
808+ return
809+ default:
810+ time.Sleep(200 * time.Millisecond)
811+ }
812+ }
804}
813}
805
814
806func (a *agent) UpdateModel() error {
815func (a *agent) UpdateModel() error {
internal/message/content.go+6−4
@@ -102,8 +102,10 @@ type ToolResult struct {
102func (ToolResult) isPart() {}
102func (ToolResult) isPart() {}
103
103
104type Finish struct {
104type Finish struct {
105− Reason FinishReason `json:"reason"`
105+ Reason FinishReason `json:"reason"`
106− Time int64 `json:"time"`
106+ Time int64 `json:"time"`
107+ Message string `json:"message,omitempty"`
108+ Details string `json:"details,omitempty"`
107}
109}
108
110
109func (Finish) isPart() {}
111func (Finish) isPart() {}
@@ -308,7 +310,7 @@ func (m *Message) SetToolResults(tr []ToolResult) {
308 }
310 }
309}
311}
310
312
311−func (m *Message) AddFinish(reason FinishReason) {
313+func (m *Message) AddFinish(reason FinishReason, message, details string) {
312 // remove any existing finish part
314 // remove any existing finish part
313 for i, part := range m.Parts {
315 for i, part := range m.Parts {
314 if _, ok := part.(Finish); ok {
316 if _, ok := part.(Finish); ok {
@@ -316,7 +318,7 @@ func (m *Message) AddFinish(reason FinishReason) {
316 break
318 break
317 }
319 }
318 }
320 }
319− m.Parts = append(m.Parts, Finish{Reason: reason, Time: time.Now().Unix()})
321+ m.Parts = append(m.Parts, Finish{Reason: reason, Time: time.Now().Unix(), Message: message, Details: details})
320}
322}
321
323
322func (m *Message) AddImageURL(url, detail string) {
324func (m *Message) AddImageURL(url, detail string) {
internal/tui/components/chat/chat.go+6−3
@@ -337,7 +337,7 @@ func (m *messageListCmp) updateToolCalls(msg message.Message, existingToolCalls
337 var cmds []tea.Cmd
337 var cmds []tea.Cmd
338
338
339 for _, tc := range msg.ToolCalls() {
339 for _, tc := range msg.ToolCalls() {
340− if cmd := m.updateOrAddToolCall(tc, existingToolCalls, msg.ID); cmd != nil {
340+ if cmd := m.updateOrAddToolCall(msg, tc, existingToolCalls); cmd != nil {
341 cmds = append(cmds, cmd)
341 cmds = append(cmds, cmd)
342 }
342 }
343 }
343 }
@@ -346,18 +346,21 @@ func (m *messageListCmp) updateToolCalls(msg message.Message, existingToolCalls
346}
346}
347
347
348// updateOrAddToolCall updates an existing tool call or adds a new one.
348// updateOrAddToolCall updates an existing tool call or adds a new one.
349−func (m *messageListCmp) updateOrAddToolCall(tc message.ToolCall, existingToolCalls map[int]messages.ToolCallCmp, messageID string) tea.Cmd {
349+func (m *messageListCmp) updateOrAddToolCall(msg message.Message, tc message.ToolCall, existingToolCalls map[int]messages.ToolCallCmp) tea.Cmd {
350 // Try to find existing tool call
350 // Try to find existing tool call
351 for index, existingTC := range existingToolCalls {
351 for index, existingTC := range existingToolCalls {
352 if tc.ID == existingTC.GetToolCall().ID {
352 if tc.ID == existingTC.GetToolCall().ID {
353 existingTC.SetToolCall(tc)
353 existingTC.SetToolCall(tc)
354+ if msg.FinishPart() != nil && msg.FinishPart().Reason == message.FinishReasonCanceled {
355+ existingTC.SetCancelled()
356+ }
354 m.listCmp.UpdateItem(index, existingTC)
357 m.listCmp.UpdateItem(index, existingTC)
355 return nil
358 return nil
356 }
359 }
357 }
360 }
358
361
359 // Add new tool call if not found
362 // Add new tool call if not found
360− return m.listCmp.AppendItem(messages.NewToolCallCmp(messageID, tc))
363+ return m.listCmp.AppendItem(messages.NewToolCallCmp(msg.ID, tc))
361}
364}
362
365
363// handleNewAssistantMessage processes new assistant messages and their tool calls.
366// handleNewAssistantMessage processes new assistant messages and their tool calls.
internal/tui/components/chat/editor/editor.go+3
@@ -130,6 +130,9 @@ func (m *editorCmp) Init() tea.Cmd {
130}
130}
131
131
132func (m *editorCmp) send() tea.Cmd {
132func (m *editorCmp) send() tea.Cmd {
133+ if m.app.CoderAgent == nil {
134+ return util.ReportError(fmt.Errorf("coder agent is not initialized"))
135+ }
133 if m.app.CoderAgent.IsSessionBusy(m.session.ID) {
136 if m.app.CoderAgent.IsSessionBusy(m.session.ID) {
134 return util.ReportWarn("Agent is working, please wait...")
137 return util.ReportWarn("Agent is working, please wait...")
135 }
138 }
internal/tui/components/chat/messages/messages.go+8
@@ -8,6 +8,7 @@ import (
8
8
9 tea "github.com/charmbracelet/bubbletea/v2"
9 tea "github.com/charmbracelet/bubbletea/v2"
10 "github.com/charmbracelet/lipgloss/v2"
10 "github.com/charmbracelet/lipgloss/v2"
11+ "github.com/charmbracelet/x/ansi"
11
12
12 "github.com/charmbracelet/crush/internal/config"
13 "github.com/charmbracelet/crush/internal/config"
13 "github.com/charmbracelet/crush/internal/fur/provider"
14 "github.com/charmbracelet/crush/internal/fur/provider"
@@ -184,6 +185,7 @@ func (m *messageCmp) toMarkdown(content string) string {
184// markdownContent processes the message content and handles special states.
185// markdownContent processes the message content and handles special states.
185// Returns appropriate content for thinking, finished, and error states.
186// Returns appropriate content for thinking, finished, and error states.
186func (m *messageCmp) markdownContent() string {
187func (m *messageCmp) markdownContent() string {
188+ t := styles.CurrentTheme()
187 content := m.message.Content().String()
189 content := m.message.Content().String()
188 if m.message.Role == message.Assistant {
190 if m.message.Role == message.Assistant {
189 thinking := m.message.IsThinking()
191 thinking := m.message.IsThinking()
@@ -199,6 +201,12 @@ func (m *messageCmp) markdownContent() string {
199 content = ""
201 content = ""
200 } else if finished && content == "" && finishedData.Reason == message.FinishReasonCanceled {
202 } else if finished && content == "" && finishedData.Reason == message.FinishReasonCanceled {
201 content = "*Canceled*"
203 content = "*Canceled*"
204+ } else if finished && content == "" && finishedData.Reason == message.FinishReasonError {
205+ errTag := t.S().Base.Padding(0, 1).Background(t.Red).Foreground(t.White).Render("ERROR")
206+ truncated := ansi.Truncate(finishedData.Message, m.textWidth()-2-lipgloss.Width(errTag), "...")
207+ title := fmt.Sprintf("%s %s", errTag, t.S().Base.Foreground(t.FgHalfMuted).Render(truncated))
208+ details := t.S().Base.Foreground(t.FgSubtle).Width(m.textWidth() - 2).Render(finishedData.Details)
209+ return fmt.Sprintf("%s\n\n%s", title, details)
202 }
210 }
203 }
211 }
204 return m.toMarkdown(content)
212 return m.toMarkdown(content)
internal/tui/components/chat/splash/splash.go+2−4
@@ -192,10 +192,8 @@ func (s *splashCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
192 return s, s.initializeProject()
192 return s, s.initializeProject()
193 }
193 }
194 case key.Matches(msg, s.keyMap.No):
194 case key.Matches(msg, s.keyMap.No):
195− if s.needsProjectInit {
195+ s.selectedNo = true
196− s.needsProjectInit = false
196+ return s, s.initializeProject()
197− return s, util.CmdHandler(OnboardingCompleteMsg{})
198− }
199 default:
197 default:
200 if s.needsAPIKey {
198 if s.needsAPIKey {
201 u, cmd := s.apiKeyInput.Update(msg)
199 u, cmd := s.apiKeyInput.Update(msg)
internal/tui/components/core/status_test.gonew file+147
@@ -0,0 +1,147 @@
1+package core_test
2+
3+import (
4+ "fmt"
5+ "image/color"
6+ "testing"
7+
8+ "github.com/charmbracelet/crush/internal/tui/components/core"
9+ "github.com/charmbracelet/x/exp/golden"
10+)
11+
12+func TestStatus(t *testing.T) {
13+ t.Parallel()
14+
15+ tests := []struct {
16+ name string
17+ opts core.StatusOpts
18+ width int
19+ }{
20+ {
21+ name: "Default",
22+ opts: core.StatusOpts{
23+ Title: "Status",
24+ Description: "Everything is working fine",
25+ },
26+ width: 80,
27+ },
28+ {
29+ name: "WithCustomIcon",
30+ opts: core.StatusOpts{
31+ Icon: "✓",
32+ Title: "Success",
33+ Description: "Operation completed successfully",
34+ },
35+ width: 80,
36+ },
37+ {
38+ name: "NoIcon",
39+ opts: core.StatusOpts{
40+ NoIcon: true,
41+ Title: "Info",
42+ Description: "This status has no icon",
43+ },
44+ width: 80,
45+ },
46+ {
47+ name: "WithColors",
48+ opts: core.StatusOpts{
49+ Icon: "⚠",
50+ IconColor: color.RGBA{255, 165, 0, 255}, // Orange
51+ Title: "Warning",
52+ TitleColor: color.RGBA{255, 255, 0, 255}, // Yellow
53+ Description: "This is a warning message",
54+ DescriptionColor: color.RGBA{255, 0, 0, 255}, // Red
55+ },
56+ width: 80,
57+ },
58+ {
59+ name: "WithExtraContent",
60+ opts: core.StatusOpts{
61+ Title: "Build",
62+ Description: "Building project",
63+ ExtraContent: "[2/5]",
64+ },
65+ width: 80,
66+ },
67+ {
68+ name: "LongDescription",
69+ opts: core.StatusOpts{
70+ Title: "Processing",
71+ Description: "This is a very long description that should be truncated when the width is too small to display it completely without wrapping",
72+ },
73+ width: 60,
74+ },
75+ {
76+ name: "NarrowWidth",
77+ opts: core.StatusOpts{
78+ Icon: "●",
79+ Title: "Status",
80+ Description: "Short message",
81+ },
82+ width: 30,
83+ },
84+ {
85+ name: "VeryNarrowWidth",
86+ opts: core.StatusOpts{
87+ Icon: "●",
88+ Title: "Test",
89+ Description: "This will be truncated",
90+ },
91+ width: 20,
92+ },
93+ {
94+ name: "EmptyDescription",
95+ opts: core.StatusOpts{
96+ Icon: "●",
97+ Title: "Title Only",
98+ },
99+ width: 80,
100+ },
101+ {
102+ name: "AllFieldsWithExtraContent",
103+ opts: core.StatusOpts{
104+ Icon: "🚀",
105+ IconColor: color.RGBA{0, 255, 0, 255}, // Green
106+ Title: "Deployment",
107+ TitleColor: color.RGBA{0, 0, 255, 255}, // Blue
108+ Description: "Deploying to production environment",
109+ DescriptionColor: color.RGBA{128, 128, 128, 255}, // Gray
110+ ExtraContent: "v1.2.3",
111+ },
112+ width: 80,
113+ },
114+ }
115+
116+ for _, tt := range tests {
117+ t.Run(tt.name, func(t *testing.T) {
118+ t.Parallel()
119+
120+ output := core.Status(tt.opts, tt.width)
121+ golden.RequireEqual(t, []byte(output))
122+ })
123+ }
124+}
125+
126+func TestStatusTruncation(t *testing.T) {
127+ t.Parallel()
128+
129+ opts := core.StatusOpts{
130+ Icon: "●",
131+ Title: "Very Long Title",
132+ Description: "This is an extremely long description that definitely needs to be truncated",
133+ ExtraContent: "[extra]",
134+ }
135+
136+ // Test different widths to ensure truncation works correctly
137+ widths := []int{20, 30, 40, 50, 60}
138+
139+ for _, width := range widths {
140+ t.Run(fmt.Sprintf("Width%d", width), func(t *testing.T) {
141+ t.Parallel()
142+
143+ output := core.Status(opts, width)
144+ golden.RequireEqual(t, []byte(output))
145+ })
146+ }
147+}
internal/tui/components/core/testdata/TestStatus/AllFieldsWithExtraContent.goldennew file+1
@@ -0,0 +1 @@
1+🚀 Deployment Deploying to production environment v1.2.3
internal/tui/components/core/testdata/TestStatus/Default.goldennew file+1
@@ -0,0 +1 @@
1+● Status Everything is working fine
internal/tui/components/core/testdata/TestStatus/EmptyDescription.goldennew file+1
@@ -0,0 +1 @@
1+● Title Only 
internal/tui/components/core/testdata/TestStatus/LongDescription.goldennew file+1
@@ -0,0 +1 @@
1+● Processing This is a very long description that should be…
internal/tui/components/core/testdata/TestStatus/NarrowWidth.goldennew file+1
@@ -0,0 +1 @@
1+● Status Short message
internal/tui/components/core/testdata/TestStatus/NoIcon.goldennew file+1
@@ -0,0 +1 @@
1+Info This status has no icon
internal/tui/components/core/testdata/TestStatus/VeryNarrowWidth.goldennew file+1
@@ -0,0 +1 @@
1+● Test This will be…
internal/tui/components/core/testdata/TestStatus/WithColors.goldennew file+1
@@ -0,0 +1 @@
1+⚠ Warning This is a warning message
internal/tui/components/core/testdata/TestStatus/WithCustomIcon.goldennew file+1
@@ -0,0 +1 @@
1+✓ Success Operation completed successfully
internal/tui/components/core/testdata/TestStatus/WithExtraContent.goldennew file+1
@@ -0,0 +1 @@
1+● Build Building project [2/5]
internal/tui/components/core/testdata/TestStatusTruncation/Width20.goldennew file+1
@@ -0,0 +1 @@
1+● Very Long Title  [extra]
internal/tui/components/core/testdata/TestStatusTruncation/Width30.goldennew file+1
@@ -0,0 +1 @@
1+● Very Long Title Thi… [extra]
internal/tui/components/core/testdata/TestStatusTruncation/Width40.goldennew file+1
@@ -0,0 +1 @@
1+● Very Long Title This is an ex… [extra]
internal/tui/components/core/testdata/TestStatusTruncation/Width50.goldennew file+1
@@ -0,0 +1 @@
1+● Very Long Title This is an extremely lo… [extra]
internal/tui/components/core/testdata/TestStatusTruncation/Width60.goldennew file+1
@@ -0,0 +1 @@
1+● Very Long Title This is an extremely long descrip… [extra]
internal/tui/components/dialogs/permissions/permissions.go+13−6
@@ -60,6 +60,9 @@ type permissionDialogCmp struct {
60 cachedContent string
60 cachedContent string
61 contentDirty bool
61 contentDirty bool
62
62
63+ positionRow int // Row position for dialog
64+ positionCol int // Column position for dialog
65+
63 keyMap KeyMap
66 keyMap KeyMap
64}
67}
65
68
@@ -446,6 +449,10 @@ func (p *permissionDialogCmp) render() string {
446 p.contentViewPort.SetHeight(contentHeight)
449 p.contentViewPort.SetHeight(contentHeight)
447 p.contentViewPort.SetContent(contentFinal)
450 p.contentViewPort.SetContent(contentFinal)
448
451
452+ p.positionRow = p.wHeight / 2
453+ p.positionRow -= (contentHeight + 9) / 2
454+ p.positionRow -= 3 // Move dialog slightly higher than middle
455+
449 var contentHelp string
456 var contentHelp string
450 if p.supportsDiffView() {
457 if p.supportsDiffView() {
451 contentHelp = help.New().View(p.keyMap)
458 contentHelp = help.New().View(p.keyMap)
@@ -509,7 +516,11 @@ func (p *permissionDialogCmp) SetSize() tea.Cmd {
509 if oldWidth != p.width || oldHeight != p.height {
516 if oldWidth != p.width || oldHeight != p.height {
510 p.contentDirty = true
517 p.contentDirty = true
511 }
518 }
512−
519+ p.positionRow = p.wHeight / 2
520+ p.positionRow -= p.height / 2
521+ p.positionRow -= 3 // Move dialog slightly higher than middle
522+ p.positionCol = p.wWidth / 2
523+ p.positionCol -= p.width / 2
513 return nil
524 return nil
514}
525}
515
526
@@ -529,9 +540,5 @@ func (p *permissionDialogCmp) ID() dialogs.DialogID {
529
540
530// Position implements PermissionDialogCmp.
541// Position implements PermissionDialogCmp.
531func (p *permissionDialogCmp) Position() (int, int) {
542func (p *permissionDialogCmp) Position() (int, int) {
532− row := (p.wHeight / 2) - 2 // Just a bit above the center
543+ return p.positionRow, p.positionCol
533− row -= p.height / 2
534− col := p.wWidth / 2
535− col -= p.width / 2
536− return row, col
537}
544}
internal/tui/tui.go+4−4
@@ -315,7 +315,6 @@ func (a *appModel) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd {
315 // dialogs
315 // dialogs
316 case key.Matches(msg, a.keyMap.Quit):
316 case key.Matches(msg, a.keyMap.Quit):
317 if a.dialog.ActiveDialogID() == quit.QuitDialogID {
317 if a.dialog.ActiveDialogID() == quit.QuitDialogID {
318− // if the quit dialog is already open, close the app
319 return tea.Quit
318 return tea.Quit
320 }
319 }
321 return util.CmdHandler(dialogs.OpenDialogMsg{
320 return util.CmdHandler(dialogs.OpenDialogMsg{
@@ -324,20 +323,21 @@ func (a *appModel) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd {
324
323
325 case key.Matches(msg, a.keyMap.Commands):
324 case key.Matches(msg, a.keyMap.Commands):
326 if a.dialog.ActiveDialogID() == commands.CommandsDialogID {
325 if a.dialog.ActiveDialogID() == commands.CommandsDialogID {
327− // If the commands dialog is already open, close it
328 return util.CmdHandler(dialogs.CloseDialogMsg{})
326 return util.CmdHandler(dialogs.CloseDialogMsg{})
329 }
327 }
330 if a.dialog.HasDialogs() {
328 if a.dialog.HasDialogs() {
331− return nil // Don't open commands dialog if another dialog is active
329+ return nil
332 }
330 }
333 return util.CmdHandler(dialogs.OpenDialogMsg{
331 return util.CmdHandler(dialogs.OpenDialogMsg{
334 Model: commands.NewCommandDialog(a.selectedSessionID),
332 Model: commands.NewCommandDialog(a.selectedSessionID),
335 })
333 })
336 case key.Matches(msg, a.keyMap.Sessions):
334 case key.Matches(msg, a.keyMap.Sessions):
337 if a.dialog.ActiveDialogID() == sessions.SessionsDialogID {
335 if a.dialog.ActiveDialogID() == sessions.SessionsDialogID {
338− // If the sessions dialog is already open, close it
339 return util.CmdHandler(dialogs.CloseDialogMsg{})
336 return util.CmdHandler(dialogs.CloseDialogMsg{})
340 }
337 }
338+ if a.dialog.HasDialogs() && a.dialog.ActiveDialogID() != commands.CommandsDialogID {
339+ return nil
340+ }
341 var cmds []tea.Cmd
341 var cmds []tea.Cmd
342 if a.dialog.ActiveDialogID() == commands.CommandsDialogID {
342 if a.dialog.ActiveDialogID() == commands.CommandsDialogID {
343 // If the commands dialog is open, close it first
343 // If the commands dialog is open, close it first