diff --git a/internal/tui/components/chat/editor/editor.go b/internal/tui/components/chat/editor/editor.go index 8590098a59c6f455823c0af3bae5a801a1986c1f..01c99c8f2148d75a0d92758d0f58d50ff7b36695 100644 --- a/internal/tui/components/chat/editor/editor.go +++ b/internal/tui/components/chat/editor/editor.go @@ -27,6 +27,7 @@ import ( "git.secluded.site/crush/internal/tui/components/dialogs/commands" "git.secluded.site/crush/internal/tui/components/dialogs/filepicker" "git.secluded.site/crush/internal/tui/components/dialogs/quit" + tuieditor "git.secluded.site/crush/internal/tui/components/dialogs/tui_editor" "git.secluded.site/crush/internal/tui/styles" "git.secluded.site/crush/internal/tui/util" ) @@ -96,7 +97,7 @@ type OpenEditorMsg struct { func (m *editorCmp) openEditor(value string) tea.Cmd { editor := os.Getenv("EDITOR") if editor == "" { - // Use platform-appropriate default editor + // Use platform-appropriate default editor. if runtime.GOOS == "windows" { editor = "notepad" } else { @@ -112,6 +113,14 @@ func (m *editorCmp) openEditor(value string) tea.Cmd { if _, err := tmpfile.WriteString(value); err != nil { return util.ReportError(err) } + + if tuieditor.IsTUIEditor(editor) { + return util.CmdHandler(commands.OpenEmbeddedEditorMsg{ + FilePath: tmpfile.Name(), + Editor: editor, + }) + } + cmdStr := editor + " " + tmpfile.Name() return util.ExecShell(context.TODO(), cmdStr, func(err error) tea.Msg { if err != nil { @@ -550,10 +559,7 @@ func (c *editorCmp) IsEmpty() bool { func normalPromptFunc(info textarea.PromptInfo) string { t := styles.CurrentTheme() if info.LineNumber == 0 { - if info.Focused { - return " > " - } - return "::: " + return " > " } if info.Focused { return t.S().Base.Foreground(t.GreenDark).Render("::: ") diff --git a/internal/tui/components/dialogs/commands/commands.go b/internal/tui/components/dialogs/commands/commands.go index 8772cfa792601ceeb28a7e610796bad204206e9d..4261a4aa79f2cee7324affb11b1c831e60b004f1 100644 --- a/internal/tui/components/dialogs/commands/commands.go +++ b/internal/tui/components/dialogs/commands/commands.go @@ -85,10 +85,14 @@ type ( ToggleThinkingMsg struct{} OpenReasoningDialogMsg struct{} OpenExternalEditorMsg struct{} - ToggleYoloModeMsg struct{} - OpenLazygitMsg struct{} - OpenGhDashMsg struct{} - CompactMsg struct { + OpenEmbeddedEditorMsg struct { + FilePath string + Editor string + } + ToggleYoloModeMsg struct{} + OpenLazygitMsg struct{} + OpenGhDashMsg struct{} + CompactMsg struct { SessionID string } ) diff --git a/internal/tui/components/dialogs/ghdash/ghdash.go b/internal/tui/components/dialogs/ghdash/ghdash.go index 20066acf01813d01b18d52b20781f3e1e3bf657d..4ed1ae4b15fe0962b18f421a66c9a6e4371a1388 100644 --- a/internal/tui/components/dialogs/ghdash/ghdash.go +++ b/internal/tui/components/dialogs/ghdash/ghdash.go @@ -7,10 +7,12 @@ import ( "image/color" "os" - "github.com/charmbracelet/crush/internal/terminal" - "github.com/charmbracelet/crush/internal/tui/components/dialogs" - "github.com/charmbracelet/crush/internal/tui/components/dialogs/termdialog" - "github.com/charmbracelet/crush/internal/tui/styles" + tea "charm.land/bubbletea/v2" + + "git.secluded.site/crush/internal/terminal" + "git.secluded.site/crush/internal/tui/components/dialogs" + "git.secluded.site/crush/internal/tui/components/dialogs/termdialog" + "git.secluded.site/crush/internal/tui/styles" ) // DialogID is the unique identifier for the gh-dash dialog. @@ -34,10 +36,11 @@ func NewDialog(ctx context.Context, workingDir string) *termdialog.Dialog { Title: "GitHub Dashboard", LoadingMsg: "Starting gh-dash...", Term: terminal.New(terminal.Config{Context: ctx, Cmd: cmd}), - OnClose: func() { + OnClose: func() tea.Cmd { if configFile != "" { _ = os.Remove(configFile) } + return nil }, }) } diff --git a/internal/tui/components/dialogs/lazygit/lazygit.go b/internal/tui/components/dialogs/lazygit/lazygit.go index ec41de721f51f081340b30ba0b0f7a0a8b079a27..8770a1a6f00750a886e3d5dfdaa1f5408a4ad82f 100644 --- a/internal/tui/components/dialogs/lazygit/lazygit.go +++ b/internal/tui/components/dialogs/lazygit/lazygit.go @@ -7,6 +7,8 @@ import ( "image/color" "os" + tea "charm.land/bubbletea/v2" + "git.secluded.site/crush/internal/terminal" "git.secluded.site/crush/internal/tui/components/dialogs" "git.secluded.site/crush/internal/tui/components/dialogs/termdialog" @@ -34,10 +36,11 @@ func NewDialog(ctx context.Context, workingDir string) *termdialog.Dialog { Title: "Lazygit", LoadingMsg: "Starting lazygit...", Term: terminal.New(terminal.Config{Context: ctx, Cmd: cmd}), - OnClose: func() { + OnClose: func() tea.Cmd { if configFile != "" { _ = os.Remove(configFile) } + return nil }, }) } diff --git a/internal/tui/components/dialogs/termdialog/termdialog.go b/internal/tui/components/dialogs/termdialog/termdialog.go index 237be7af462eb3ee71a6097acf39e70720426f2b..fed95b67a2958a81d919ffe55e7656905334b1aa 100644 --- a/internal/tui/components/dialogs/termdialog/termdialog.go +++ b/internal/tui/components/dialogs/termdialog/termdialog.go @@ -32,7 +32,8 @@ type Config struct { // Term is the terminal to embed. Term *terminal.Terminal // OnClose is called when the dialog is closed (optional). - OnClose func() + // Can return a tea.Cmd to emit messages after close. + OnClose func() tea.Cmd } // Dialog is a dialog that embeds a terminal application. @@ -41,7 +42,7 @@ type Dialog struct { title string loadingMsg string term *terminal.Terminal - onClose func() + onClose func() tea.Cmd wWidth int wHeight int @@ -243,7 +244,7 @@ func (d *Dialog) Close() tea.Cmd { _ = d.term.Close() if d.onClose != nil { - d.onClose() + return d.onClose() } return nil diff --git a/internal/tui/components/dialogs/tui_editor/editor.go b/internal/tui/components/dialogs/tui_editor/editor.go new file mode 100644 index 0000000000000000000000000000000000000000..dff8cfdaa2389e8d655f392c0e4a35916bd01ff6 --- /dev/null +++ b/internal/tui/components/dialogs/tui_editor/editor.go @@ -0,0 +1,108 @@ +// Package tuieditor provides a dialog component for embedding terminal-based +// editors (vim, nvim, nano, etc.) in the TUI. +package tuieditor + +import ( + "context" + "os" + "path/filepath" + "slices" + "strings" + + tea "charm.land/bubbletea/v2" + + "git.secluded.site/crush/internal/terminal" + "git.secluded.site/crush/internal/tui/components/dialogs" + "git.secluded.site/crush/internal/tui/components/dialogs/termdialog" + "git.secluded.site/crush/internal/tui/util" +) + +// DialogID is the unique identifier for the embedded editor dialog. +const DialogID dialogs.DialogID = "tui_editor" + +// EditorResultMsg is sent when the embedded editor closes with the file content. +type EditorResultMsg struct { + Content string + Err error +} + +// knownTUIEditors is a list of terminal-based editors that can be embedded. +var knownTUIEditors = []string{ + "vim", + "nvim", + "vi", + "nano", + "helix", + "hx", + "micro", + "emacs", + "joe", + "ne", + "jed", + "kak", + "pico", + "mcedit", + "mg", + "zile", +} + +// IsTUIEditor returns true if the given editor command is a known TUI editor. +func IsTUIEditor(editor string) bool { + base := filepath.Base(editor) + if idx := strings.Index(base, " "); idx != -1 { + base = base[:idx] + } + return slices.Contains(knownTUIEditors, base) +} + +// Config holds configuration for the embedded editor dialog. +type Config struct { + // FilePath is the path to the file to edit. + FilePath string + // Editor is the editor command to use. + Editor string + // WorkingDir is the working directory for the editor. + WorkingDir string +} + +// NewDialog creates a new embedded editor dialog. The context controls the +// lifetime of the editor process - when cancelled, the process will be killed. +// When the editor exits, an EditorResultMsg is emitted with the file content. +func NewDialog(ctx context.Context, cfg Config) *termdialog.Dialog { + editorCmd := cfg.Editor + if editorCmd == "" { + editorCmd = "nvim" + } + + parts := strings.Fields(editorCmd) + cmdName := parts[0] + args := append(parts[1:], cfg.FilePath) + + cmd := terminal.PrepareCmd( + ctx, + cmdName, + args, + cfg.WorkingDir, + nil, + ) + + filePath := cfg.FilePath + + return termdialog.New(termdialog.Config{ + ID: DialogID, + Title: "Editor", + LoadingMsg: "Starting editor...", + Term: terminal.New(terminal.Config{Context: ctx, Cmd: cmd}), + OnClose: func() tea.Cmd { + content, err := os.ReadFile(filePath) + _ = os.Remove(filePath) + + if err != nil { + return util.CmdHandler(EditorResultMsg{Err: err}) + } + return util.CmdHandler(EditorResultMsg{ + Content: strings.TrimSpace(string(content)), + }) + }, + }) +} diff --git a/internal/tui/page/chat/chat.go b/internal/tui/page/chat/chat.go index f8b151745789186096648190a680bcf0f4fa7eba..70ec9e1b9aa5a55cf0792cf9197d42fda418cecd 100644 --- a/internal/tui/page/chat/chat.go +++ b/internal/tui/page/chat/chat.go @@ -34,6 +34,7 @@ import ( "git.secluded.site/crush/internal/tui/components/dialogs/filepicker" "git.secluded.site/crush/internal/tui/components/dialogs/models" "git.secluded.site/crush/internal/tui/components/dialogs/reasoning" + tuieditor "git.secluded.site/crush/internal/tui/components/dialogs/tui_editor" "git.secluded.site/crush/internal/tui/page" "git.secluded.site/crush/internal/tui/styles" "git.secluded.site/crush/internal/tui/util" @@ -57,14 +58,6 @@ const ( PanelTypeSplash PanelType = "splash" ) -// PillSection represents which pill section is focused when in pills panel. -type PillSection int - -const ( - PillSectionTodos PillSection = iota - PillSectionQueue -) - const ( CompactModeWidthBreakpoint = 120 // Width at which the chat page switches to compact mode CompactModeHeightBreakpoint = 30 // Height at which the chat page switches to compact mode @@ -124,18 +117,9 @@ type chatPage struct { splashFullScreen bool isOnboarding bool isProjectInit bool - promptQueue int - - // Pills state - pillsExpanded bool - focusedPillSection PillSection - - // Todo spinner - todoSpinner spinner.Model } func New(app *app.App) ChatPage { - t := styles.CurrentTheme() return &chatPage{ app: app, keyMap: DefaultKeyMap(), @@ -145,10 +129,6 @@ func New(app *app.App) ChatPage { editor: editor.New(app), splash: splash.New(), focusedPane: PanelTypeSplash, - todoSpinner: spinner.New( - spinner.WithSpinner(spinner.MiniDot), - spinner.WithStyle(t.S().Base.Foreground(t.GreenDark)), - ), } } @@ -187,13 +167,6 @@ func (p *chatPage) Init() tea.Cmd { func (p *chatPage) Update(msg tea.Msg) (util.Model, tea.Cmd) { var cmds []tea.Cmd - if p.session.ID != "" && p.app.AgentCoordinator != nil { - queueSize := p.app.AgentCoordinator.QueuedPrompts(p.session.ID) - if queueSize != p.promptQueue { - p.promptQueue = queueSize - cmds = append(cmds, p.SetSize(p.width, p.height)) - } - } switch msg := msg.(type) { case tea.KeyboardEnhancementsMsg: p.keyboardEnhancements = msg @@ -295,20 +268,18 @@ func (p *chatPage) Update(msg tea.Msg) (util.Model, tea.Cmd) { u, cmd := p.editor.Update(msg) p.editor = u.(editor.Editor) return p, cmd - case pubsub.Event[session.Session]: - if msg.Payload.ID == p.session.ID { - prevHasIncompleteTodos := hasIncompleteTodos(p.session.Todos) - prevHasInProgress := p.hasInProgressTodo() - p.session = msg.Payload - newHasIncompleteTodos := hasIncompleteTodos(p.session.Todos) - newHasInProgress := p.hasInProgressTodo() - if prevHasIncompleteTodos != newHasIncompleteTodos { - cmds = append(cmds, p.SetSize(p.width, p.height)) - } - if !prevHasInProgress && newHasInProgress { - cmds = append(cmds, p.todoSpinner.Tick) - } + case tuieditor.EditorResultMsg: + if msg.Err != nil { + return p, util.ReportError(msg.Err) } + if msg.Content == "" { + return p, util.ReportWarn("Message is empty") + } + // Forward the result to the editor as OpenEditorMsg. + u, cmd := p.editor.Update(editor.OpenEditorMsg{Text: msg.Content}) + p.editor = u.(editor.Editor) + return p, cmd + case pubsub.Event[session.Session]: u, cmd := p.header.Update(msg) p.header = u.(header.Header) cmds = append(cmds, cmd) @@ -352,17 +323,6 @@ func (p *chatPage) Update(msg tea.Msg) (util.Model, tea.Cmd) { case pubsub.Event[message.Message], anim.StepMsg, spinner.TickMsg: - // Update todo spinner if agent is busy and we have in-progress todos - agentBusy := p.app.AgentCoordinator != nil && p.app.AgentCoordinator.IsBusy() - if _, ok := msg.(spinner.TickMsg); ok && p.hasInProgressTodo() && agentBusy { - var cmd tea.Cmd - p.todoSpinner, cmd = p.todoSpinner.Update(msg) - cmds = append(cmds, cmd) - } - // Start spinner when agent becomes busy and we have in-progress todos - if _, ok := msg.(pubsub.Event[message.Message]); ok && p.hasInProgressTodo() && agentBusy { - cmds = append(cmds, p.todoSpinner.Tick) - } if p.focusedPane == PanelTypeSplash { u, cmd := p.splash.Update(msg) p.splash = u.(splash.Splash) @@ -453,7 +413,8 @@ func (p *chatPage) Update(msg tea.Msg) (util.Model, tea.Cmd) { p.splash = u.(splash.Splash) return p, cmd } - return p, p.changeFocus() + p.changeFocus() + return p, nil case key.Matches(msg, p.keyMap.Cancel): if p.session.ID != "" && p.app.AgentCoordinator.IsBusy() { return p, p.cancel() @@ -461,18 +422,6 @@ func (p *chatPage) Update(msg tea.Msg) (util.Model, tea.Cmd) { case key.Matches(msg, p.keyMap.Details): p.toggleDetails() return p, nil - case key.Matches(msg, p.keyMap.TogglePills): - if p.session.ID != "" { - return p, p.togglePillsExpanded() - } - case key.Matches(msg, p.keyMap.PillLeft): - if p.session.ID != "" && p.pillsExpanded { - return p, p.switchPillSection(-1) - } - case key.Matches(msg, p.keyMap.PillRight): - if p.session.ID != "" && p.pillsExpanded { - return p, p.switchPillSection(1) - } } switch p.focusedPane { @@ -546,78 +495,19 @@ func (p *chatPage) View() string { } else { messagesView := p.chat.View() editorView := p.editor.View() - - hasIncompleteTodos := hasIncompleteTodos(p.session.Todos) - hasQueue := p.promptQueue > 0 - todosFocused := p.pillsExpanded && p.focusedPillSection == PillSectionTodos - queueFocused := p.pillsExpanded && p.focusedPillSection == PillSectionQueue - - // Use spinner when agent is busy, otherwise show static icon - agentBusy := p.app.AgentCoordinator != nil && p.app.AgentCoordinator.IsBusy() - inProgressIcon := t.S().Base.Foreground(t.GreenDark).Render(styles.CenterSpinnerIcon) - if agentBusy { - inProgressIcon = p.todoSpinner.View() - } - - var pills []string - if hasIncompleteTodos { - pills = append(pills, todoPill(p.session.Todos, inProgressIcon, todosFocused, p.pillsExpanded, t)) - } - if hasQueue { - pills = append(pills, queuePill(p.promptQueue, queueFocused, p.pillsExpanded, t)) - } - - var expandedList string - if p.pillsExpanded { - if todosFocused && hasIncompleteTodos { - expandedList = todoList(p.session.Todos, inProgressIcon, t, p.width-SideBarWidth) - } else if queueFocused && hasQueue { - queueItems := p.app.AgentCoordinator.QueuedPromptsList(p.session.ID) - expandedList = queueList(queueItems, t) - } - } - - var pillsArea string - if len(pills) > 0 { - pillsRow := lipgloss.JoinHorizontal(lipgloss.Top, pills...) - - if expandedList != "" { - pillsArea = lipgloss.JoinVertical( - lipgloss.Left, - pillsRow, - expandedList, - ) - } else { - pillsArea = pillsRow - } - - style := t.S().Base.MarginTop(1).PaddingLeft(3) - pillsArea = style.Render(pillsArea) - } - if p.compact { headerView := p.header.View() - views := []string{headerView, messagesView} - if pillsArea != "" { - views = append(views, pillsArea) - } - views = append(views, editorView) - chatView = lipgloss.JoinVertical(lipgloss.Left, views...) + chatView = lipgloss.JoinVertical( + lipgloss.Left, + headerView, + messagesView, + editorView, + ) } else { sidebarView := p.sidebar.View() - var messagesColumn string - if pillsArea != "" { - messagesColumn = lipgloss.JoinVertical( - lipgloss.Left, - messagesView, - pillsArea, - ) - } else { - messagesColumn = messagesView - } messages := lipgloss.JoinHorizontal( lipgloss.Left, - messagesColumn, + messagesView, sidebarView, ) chatView = lipgloss.JoinVertical( @@ -779,30 +669,14 @@ func (p *chatPage) SetSize(width, height int) tea.Cmd { cmds = append(cmds, p.editor.SetPosition(0, height-EditorHeight)) } } else { - hasIncompleteTodos := hasIncompleteTodos(p.session.Todos) - hasQueue := p.promptQueue > 0 - hasPills := hasIncompleteTodos || hasQueue - - pillsAreaHeight := 0 - if hasPills { - pillsAreaHeight = pillHeightWithBorder + 1 // +1 for padding top - if p.pillsExpanded { - if p.focusedPillSection == PillSectionTodos && hasIncompleteTodos { - pillsAreaHeight += len(p.session.Todos) - } else if p.focusedPillSection == PillSectionQueue && hasQueue { - pillsAreaHeight += p.promptQueue - } - } - } - if p.compact { - cmds = append(cmds, p.chat.SetSize(width, height-EditorHeight-HeaderHeight-pillsAreaHeight)) + cmds = append(cmds, p.chat.SetSize(width, height-EditorHeight-HeaderHeight)) p.detailsWidth = width - DetailsPositioning cmds = append(cmds, p.sidebar.SetSize(p.detailsWidth-LeftRightBorders, p.detailsHeight-TopBottomBorders)) cmds = append(cmds, p.editor.SetSize(width, EditorHeight)) cmds = append(cmds, p.header.SetWidth(width-BorderWidth)) } else { - cmds = append(cmds, p.chat.SetSize(width-SideBarWidth, height-EditorHeight-pillsAreaHeight)) + cmds = append(cmds, p.chat.SetSize(width-SideBarWidth, height-EditorHeight)) cmds = append(cmds, p.editor.SetSize(width, EditorHeight)) cmds = append(cmds, p.sidebar.SetSize(SideBarWidth, height-EditorHeight)) } @@ -827,77 +701,37 @@ func (p *chatPage) newSession() tea.Cmd { ) } -func (p *chatPage) setSession(sess session.Session) tea.Cmd { - if p.session.ID == sess.ID { +func (p *chatPage) setSession(session session.Session) tea.Cmd { + if p.session.ID == session.ID { return nil } var cmds []tea.Cmd - p.session = sess - - if p.hasInProgressTodo() { - cmds = append(cmds, p.todoSpinner.Tick) - } + p.session = session cmds = append(cmds, p.SetSize(p.width, p.height)) - cmds = append(cmds, p.chat.SetSession(sess)) - cmds = append(cmds, p.sidebar.SetSession(sess)) - cmds = append(cmds, p.header.SetSession(sess)) - cmds = append(cmds, p.editor.SetSession(sess)) + cmds = append(cmds, p.chat.SetSession(session)) + cmds = append(cmds, p.sidebar.SetSession(session)) + cmds = append(cmds, p.header.SetSession(session)) + cmds = append(cmds, p.editor.SetSession(session)) return tea.Sequence(cmds...) } -func (p *chatPage) changeFocus() tea.Cmd { +func (p *chatPage) changeFocus() { if p.session.ID == "" { - return nil + return } - switch p.focusedPane { - case PanelTypeEditor: - p.focusedPane = PanelTypeChat - p.chat.Focus() - p.editor.Blur() case PanelTypeChat: p.focusedPane = PanelTypeEditor p.editor.Focus() p.chat.Blur() + case PanelTypeEditor: + p.focusedPane = PanelTypeChat + p.chat.Focus() + p.editor.Blur() } - return nil -} - -func (p *chatPage) togglePillsExpanded() tea.Cmd { - hasPills := hasIncompleteTodos(p.session.Todos) || p.promptQueue > 0 - if !hasPills { - return nil - } - p.pillsExpanded = !p.pillsExpanded - if p.pillsExpanded { - if hasIncompleteTodos(p.session.Todos) { - p.focusedPillSection = PillSectionTodos - } else { - p.focusedPillSection = PillSectionQueue - } - } - return p.SetSize(p.width, p.height) -} - -func (p *chatPage) switchPillSection(dir int) tea.Cmd { - if !p.pillsExpanded { - return nil - } - hasIncompleteTodos := hasIncompleteTodos(p.session.Todos) - hasQueue := p.promptQueue > 0 - - if dir < 0 && p.focusedPillSection == PillSectionQueue && hasIncompleteTodos { - p.focusedPillSection = PillSectionTodos - return p.SetSize(p.width, p.height) - } - if dir > 0 && p.focusedPillSection == PillSectionTodos && hasQueue { - p.focusedPillSection = PillSectionQueue - return p.SetSize(p.width, p.height) - } - return nil } func (p *chatPage) cancel() tea.Cmd { @@ -1183,53 +1017,18 @@ func (p *chatPage) Help() help.KeyMap { globalBindings := []key.Binding{} // we are in a session if p.session.ID != "" { - var tabKey key.Binding - switch p.focusedPane { - case PanelTypeEditor: - tabKey = key.NewBinding( - key.WithKeys("tab"), - key.WithHelp("tab", "focus chat"), - ) - case PanelTypeChat: + tabKey := key.NewBinding( + key.WithKeys("tab"), + key.WithHelp("tab", "focus chat"), + ) + if p.focusedPane == PanelTypeChat { tabKey = key.NewBinding( key.WithKeys("tab"), key.WithHelp("tab", "focus editor"), ) - default: - tabKey = key.NewBinding( - key.WithKeys("tab"), - key.WithHelp("tab", "focus chat"), - ) } shortList = append(shortList, tabKey) globalBindings = append(globalBindings, tabKey) - - // Add toggle pills binding if there are pills - hasTodos := hasIncompleteTodos(p.session.Todos) - hasQueue := p.promptQueue > 0 - if hasTodos || hasQueue { - toggleBinding := p.keyMap.TogglePills - if p.pillsExpanded { - if hasTodos { - toggleBinding.SetHelp("ctrl+space", "hide todos") - } else { - toggleBinding.SetHelp("ctrl+space", "hide queued") - } - } else { - if hasTodos { - toggleBinding.SetHelp("ctrl+space", "show todos") - } else { - toggleBinding.SetHelp("ctrl+space", "show queued") - } - } - shortList = append(shortList, toggleBinding) - globalBindings = append(globalBindings, toggleBinding) - // Show left/right to switch sections when expanded and both exist - if p.pillsExpanded && hasTodos && hasQueue { - shortList = append(shortList, p.keyMap.PillLeft) - globalBindings = append(globalBindings, p.keyMap.PillLeft) - } - } } commandsBinding := key.NewBinding( key.WithKeys("ctrl+p"), @@ -1420,12 +1219,3 @@ func (p *chatPage) isMouseOverChat(x, y int) bool { // Check if mouse coordinates are within chat bounds return x >= chatX && x < chatX+chatWidth && y >= chatY && y < chatY+chatHeight } - -func (p *chatPage) hasInProgressTodo() bool { - for _, todo := range p.session.Todos { - if todo.Status == session.TodoStatusInProgress { - return true - } - } - return false -} diff --git a/internal/tui/tui.go b/internal/tui/tui.go index 0ef05f19f150197564cbcd0e96f63faceb24c6b3..479b48961f0791f1171e993550128239dcf0b506 100644 --- a/internal/tui/tui.go +++ b/internal/tui/tui.go @@ -34,6 +34,7 @@ import ( "git.secluded.site/crush/internal/tui/components/dialogs/permissions" "git.secluded.site/crush/internal/tui/components/dialogs/quit" "git.secluded.site/crush/internal/tui/components/dialogs/sessions" + tuieditor "git.secluded.site/crush/internal/tui/components/dialogs/tui_editor" "git.secluded.site/crush/internal/tui/page" "git.secluded.site/crush/internal/tui/page/chat" "git.secluded.site/crush/internal/tui/styles" @@ -321,6 +322,27 @@ func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return a, util.CmdHandler(dialogs.OpenDialogMsg{ Model: ghdash.NewDialog(a.app.Context(), a.app.Config().WorkingDir()), }) + // Embedded TUI Editor + case commands.OpenEmbeddedEditorMsg: + if a.dialog.ActiveDialogID() == tuieditor.DialogID { + return a, util.CmdHandler(dialogs.CloseDialogMsg{}) + } + return a, util.CmdHandler(dialogs.OpenDialogMsg{ + Model: tuieditor.NewDialog(a.app.Context(), tuieditor.Config{ + FilePath: msg.FilePath, + Editor: msg.Editor, + WorkingDir: a.app.Config().WorkingDir(), + }), + }) + // Editor result - forward to page + case tuieditor.EditorResultMsg: + item, ok := a.pages[a.currentPage] + if !ok { + return a, nil + } + updated, itemCmd := item.Update(msg) + a.pages[a.currentPage] = updated + return a, itemCmd // Permissions case pubsub.Event[permission.PermissionNotification]: item, ok := a.pages[a.currentPage]