diff --git a/cmd/root.go b/cmd/root.go index 9ae26b993dd1be7374907305ae4cc90036cb05d6..8e1f0839323c29489031ef48567e31f105612ef3 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -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 }, } diff --git a/internal/app/app.go b/internal/app/app.go index c3dae3d88a2be7c4cd5491e089b97695b08a7a23..c3469c8cceb1bb91ff5bb1566738ea02079053a1 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -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 { diff --git a/internal/llm/agent/agent.go b/internal/llm/agent/agent.go index fbb5b4fd8c6390ff0dfad0e072af35342355ba41..56fb431b3b705a656cdfbf9df426b8ce8c7298c4 100644 --- a/internal/llm/agent/agent.go +++ b/internal/llm/agent/agent.go @@ -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 { diff --git a/internal/message/content.go b/internal/message/content.go index 3ab53e381aaf7755c141985ebe740dbc44356471..b8d2c1aa370559977f4c8eb80803ab5fbfe83cf9 100644 --- a/internal/message/content.go +++ b/internal/message/content.go @@ -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) { diff --git a/internal/tui/components/chat/chat.go b/internal/tui/components/chat/chat.go index 0e6a95937476de9f33b1c5c0dd15e0489c645c43..71f6e1e66ed7d6d1ad80486c1017d02af14b11f4 100644 --- a/internal/tui/components/chat/chat.go +++ b/internal/tui/components/chat/chat.go @@ -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. diff --git a/internal/tui/components/chat/editor/editor.go b/internal/tui/components/chat/editor/editor.go index 67ba67f5e6c40f16a89f7bc4fe1b6932c9989754..9a0f5d3d191d722f4be2e48a40b730b255bf01d1 100644 --- a/internal/tui/components/chat/editor/editor.go +++ b/internal/tui/components/chat/editor/editor.go @@ -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...") } diff --git a/internal/tui/components/chat/messages/messages.go b/internal/tui/components/chat/messages/messages.go index e8ae97056728a0377ddcad179ecae1246f2da662..bfb8af47b6bd13eb2e1e9fb844b1935a6fccbd4d 100644 --- a/internal/tui/components/chat/messages/messages.go +++ b/internal/tui/components/chat/messages/messages.go @@ -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) diff --git a/internal/tui/components/chat/splash/splash.go b/internal/tui/components/chat/splash/splash.go index 5b343e6c5538cc17b476e521e6f2bfaf6b3490cb..c05e2d9947222298dd141c9e762d33f99883b467 100644 --- a/internal/tui/components/chat/splash/splash.go +++ b/internal/tui/components/chat/splash/splash.go @@ -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) diff --git a/internal/tui/components/core/status_test.go b/internal/tui/components/core/status_test.go new file mode 100644 index 0000000000000000000000000000000000000000..0b24dc321d8863c8bad2bc4fc38e38020230a7f5 --- /dev/null +++ b/internal/tui/components/core/status_test.go @@ -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)) + }) + } +} diff --git a/internal/tui/components/core/testdata/TestStatus/AllFieldsWithExtraContent.golden b/internal/tui/components/core/testdata/TestStatus/AllFieldsWithExtraContent.golden new file mode 100644 index 0000000000000000000000000000000000000000..e6f7fb0be25997b79c3d39bddedee2f2d7b11b72 --- /dev/null +++ b/internal/tui/components/core/testdata/TestStatus/AllFieldsWithExtraContent.golden @@ -0,0 +1 @@ +🚀 Deployment Deploying to production environment v1.2.3 \ No newline at end of file diff --git a/internal/tui/components/core/testdata/TestStatus/Default.golden b/internal/tui/components/core/testdata/TestStatus/Default.golden new file mode 100644 index 0000000000000000000000000000000000000000..a0066dedd418dafe54757dc3159b3a6b11d106ca --- /dev/null +++ b/internal/tui/components/core/testdata/TestStatus/Default.golden @@ -0,0 +1 @@ +● Status Everything is working fine \ No newline at end of file diff --git a/internal/tui/components/core/testdata/TestStatus/EmptyDescription.golden b/internal/tui/components/core/testdata/TestStatus/EmptyDescription.golden new file mode 100644 index 0000000000000000000000000000000000000000..f9c4d759b50d02598791a6462f8e9cab2e0a0b6d --- /dev/null +++ b/internal/tui/components/core/testdata/TestStatus/EmptyDescription.golden @@ -0,0 +1 @@ +● Title Only  \ No newline at end of file diff --git a/internal/tui/components/core/testdata/TestStatus/LongDescription.golden b/internal/tui/components/core/testdata/TestStatus/LongDescription.golden new file mode 100644 index 0000000000000000000000000000000000000000..f008176649f7941b9f1ee6276f6e65fea36d4c52 --- /dev/null +++ b/internal/tui/components/core/testdata/TestStatus/LongDescription.golden @@ -0,0 +1 @@ +● Processing This is a very long description that should be… \ No newline at end of file diff --git a/internal/tui/components/core/testdata/TestStatus/NarrowWidth.golden b/internal/tui/components/core/testdata/TestStatus/NarrowWidth.golden new file mode 100644 index 0000000000000000000000000000000000000000..5b9efd7dbb74dcf56344567c1918b470f90eace7 --- /dev/null +++ b/internal/tui/components/core/testdata/TestStatus/NarrowWidth.golden @@ -0,0 +1 @@ +● Status Short message \ No newline at end of file diff --git a/internal/tui/components/core/testdata/TestStatus/NoIcon.golden b/internal/tui/components/core/testdata/TestStatus/NoIcon.golden new file mode 100644 index 0000000000000000000000000000000000000000..09e14574c853264a4b18dfafcfac256b38045a02 --- /dev/null +++ b/internal/tui/components/core/testdata/TestStatus/NoIcon.golden @@ -0,0 +1 @@ +Info This status has no icon \ No newline at end of file diff --git a/internal/tui/components/core/testdata/TestStatus/VeryNarrowWidth.golden b/internal/tui/components/core/testdata/TestStatus/VeryNarrowWidth.golden new file mode 100644 index 0000000000000000000000000000000000000000..26628ae3bc28acd49e8f30e60f65912fe563c0e6 --- /dev/null +++ b/internal/tui/components/core/testdata/TestStatus/VeryNarrowWidth.golden @@ -0,0 +1 @@ +● Test This will be… \ No newline at end of file diff --git a/internal/tui/components/core/testdata/TestStatus/WithColors.golden b/internal/tui/components/core/testdata/TestStatus/WithColors.golden new file mode 100644 index 0000000000000000000000000000000000000000..ff0e3a6ec4847c4786387d26c9752f664d78cd51 --- /dev/null +++ b/internal/tui/components/core/testdata/TestStatus/WithColors.golden @@ -0,0 +1 @@ +⚠ Warning This is a warning message \ No newline at end of file diff --git a/internal/tui/components/core/testdata/TestStatus/WithCustomIcon.golden b/internal/tui/components/core/testdata/TestStatus/WithCustomIcon.golden new file mode 100644 index 0000000000000000000000000000000000000000..6857f0d29dd58886308e15ea50c7e0822834f2ee --- /dev/null +++ b/internal/tui/components/core/testdata/TestStatus/WithCustomIcon.golden @@ -0,0 +1 @@ +✓ Success Operation completed successfully \ No newline at end of file diff --git a/internal/tui/components/core/testdata/TestStatus/WithExtraContent.golden b/internal/tui/components/core/testdata/TestStatus/WithExtraContent.golden new file mode 100644 index 0000000000000000000000000000000000000000..47b02e81b5ec4fc0d0c5dd54545d9634811b1636 --- /dev/null +++ b/internal/tui/components/core/testdata/TestStatus/WithExtraContent.golden @@ -0,0 +1 @@ +● Build Building project [2/5] \ No newline at end of file diff --git a/internal/tui/components/core/testdata/TestStatusTruncation/Width20.golden b/internal/tui/components/core/testdata/TestStatusTruncation/Width20.golden new file mode 100644 index 0000000000000000000000000000000000000000..4437cba67aa068c2597e558000b9b3005478b378 --- /dev/null +++ b/internal/tui/components/core/testdata/TestStatusTruncation/Width20.golden @@ -0,0 +1 @@ +● Very Long Title  [extra] \ No newline at end of file diff --git a/internal/tui/components/core/testdata/TestStatusTruncation/Width30.golden b/internal/tui/components/core/testdata/TestStatusTruncation/Width30.golden new file mode 100644 index 0000000000000000000000000000000000000000..b09cc983c97382e4d92719bb5606d22f9dc2301f --- /dev/null +++ b/internal/tui/components/core/testdata/TestStatusTruncation/Width30.golden @@ -0,0 +1 @@ +● Very Long Title Thi… [extra] \ No newline at end of file diff --git a/internal/tui/components/core/testdata/TestStatusTruncation/Width40.golden b/internal/tui/components/core/testdata/TestStatusTruncation/Width40.golden new file mode 100644 index 0000000000000000000000000000000000000000..5113ce07a0b07d1cfddbcbae0c14046546308f2a --- /dev/null +++ b/internal/tui/components/core/testdata/TestStatusTruncation/Width40.golden @@ -0,0 +1 @@ +● Very Long Title This is an ex… [extra] \ No newline at end of file diff --git a/internal/tui/components/core/testdata/TestStatusTruncation/Width50.golden b/internal/tui/components/core/testdata/TestStatusTruncation/Width50.golden new file mode 100644 index 0000000000000000000000000000000000000000..25bd8723b0cd461311364ecaac92a2b93f00ecd9 --- /dev/null +++ b/internal/tui/components/core/testdata/TestStatusTruncation/Width50.golden @@ -0,0 +1 @@ +● Very Long Title This is an extremely lo… [extra] \ No newline at end of file diff --git a/internal/tui/components/core/testdata/TestStatusTruncation/Width60.golden b/internal/tui/components/core/testdata/TestStatusTruncation/Width60.golden new file mode 100644 index 0000000000000000000000000000000000000000..0152f1c2d0ac9e011d744e0cd02283c18edc8d03 --- /dev/null +++ b/internal/tui/components/core/testdata/TestStatusTruncation/Width60.golden @@ -0,0 +1 @@ +● Very Long Title This is an extremely long descrip… [extra] \ No newline at end of file diff --git a/internal/tui/components/dialogs/permissions/permissions.go b/internal/tui/components/dialogs/permissions/permissions.go index 6bac6e58b37a99b376ad936bbf19f541b999eb4b..0bbaa034ed2357cc4643ad92c0a680bb01cf61ff 100644 --- a/internal/tui/components/dialogs/permissions/permissions.go +++ b/internal/tui/components/dialogs/permissions/permissions.go @@ -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 } diff --git a/internal/tui/tui.go b/internal/tui/tui.go index 365db72299865897feb94879f837baa93bff5e43..0b10b74792c5cc6c91dc285d42a7d9a6736c2b90 100644 --- a/internal/tui/tui.go +++ b/internal/tui/tui.go @@ -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