From 35ad3e7c4e57fc298b674737e1a16eb267e280d4 Mon Sep 17 00:00:00 2001 From: ml Date: Sun, 19 Jul 2026 15:17:44 +0200 Subject: [PATCH 1/2] feat: models selection --- cmd/late/main.go | 53 ++++++++-- internal/config/config.go | 44 +++++++++ internal/config/config_test.go | 37 +++++++ internal/tui/model.go | 4 +- internal/tui/paste_test.go | 6 +- internal/tui/state.go | 12 +++ internal/tui/update.go | 139 ++++++++++++++++++++++++++ internal/tui/view.go | 175 +++++++++++++++++++++++++++++++++ 8 files changed, 458 insertions(+), 12 deletions(-) diff --git a/cmd/late/main.go b/cmd/late/main.go index 82baee3..c633d50 100644 --- a/cmd/late/main.go +++ b/cmd/late/main.go @@ -216,6 +216,13 @@ func main() { Model: resolvedOpenAIConfig.Model, EnableImages: *enableImagesReq, } + if appConfig != nil { + if setting, ok := appConfig.GetModelForAgent("orchestrator"); ok { + resolvedClientConfig.BaseURL = setting.URL + resolvedClientConfig.APIKey = setting.Key + resolvedClientConfig.Model = setting.Model + } + } c := client.NewClient(resolvedClientConfig) c.DiscoverBackend(context.Background()) @@ -286,16 +293,30 @@ func main() { // We'll add middlewares later once the program is started rootAgent := orchestrator.NewBaseOrchestrator("main", sess, nil, 0) - model := tui.NewModel(rootAgent, renderer) - model.ModelName = resolvedOpenAIConfig.Model - model.ShowCWD = *showCWDReq + model := tui.NewModel(rootAgent, renderer, appConfig) + if appConfig != nil { + if orchestratorModel, ok := appConfig.AgentModels["orchestrator"]; ok { + model.ModelName = orchestratorModel + } else { + model.ModelName = resolvedOpenAIConfig.Model + } - // Detect if subagents use a different model/backend - if resolvedSubagentConfig.BaseURL != resolvedOpenAIConfig.BaseURL || - resolvedSubagentConfig.APIKey != resolvedOpenAIConfig.APIKey || - resolvedSubagentConfig.Model != resolvedOpenAIConfig.Model { + var subagentInfos []string + for _, sub := range assets.GetSubagents() { + if m, ok := appConfig.AgentModels[sub.Name]; ok { + subagentInfos = append(subagentInfos, fmt.Sprintf("%s:%s", sub.Name, m)) + } + } + if len(subagentInfos) > 0 { + model.SubagentInfo = strings.Join(subagentInfos, ", ") + } else { + model.SubagentInfo = resolvedSubagentConfig.Model + } + } else { + model.ModelName = resolvedOpenAIConfig.Model model.SubagentInfo = resolvedSubagentConfig.Model } + model.ShowCWD = *showCWDReq p := tea.NewProgram(model) @@ -322,7 +343,23 @@ func main() { if *enableSubagentsReq { runner := func(ctx context.Context, goal string, ctxFiles []string, agentType string) (string, error) { - child, err := agent.NewSubagentOrchestrator(subagentClient, goal, ctxFiles, agentType, enabledTools, *injectCWDReq, *gemmaThinkingReq, *subagentMaxTurns, rootAgent, p) + var currentSubagentClient *client.Client + if appConfig != nil { + if setting, ok := appConfig.GetModelForAgent(agentType); ok { + currentSubagentClient = client.NewClient(client.Config{ + BaseURL: setting.URL, + APIKey: setting.Key, + Model: setting.Model, + EnableImages: *enableImagesReq, + }) + currentSubagentClient.DiscoverBackend(ctx) + } + } + if currentSubagentClient == nil { + currentSubagentClient = subagentClient + } + + child, err := agent.NewSubagentOrchestrator(currentSubagentClient, goal, ctxFiles, agentType, enabledTools, *injectCWDReq, *gemmaThinkingReq, *subagentMaxTurns, rootAgent, p) if err != nil { return "", err } diff --git a/internal/config/config.go b/internal/config/config.go index f8fd087..d4c6382 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -25,6 +25,12 @@ type SubagentSettings struct { Model string } +type ModelSetting struct { + URL string `json:"url"` + Key string `json:"key"` + Model string `json:"model"` +} + const ( configDirPerm os.FileMode = 0o700 configFilePerm os.FileMode = 0o600 @@ -46,6 +52,9 @@ type Config struct { SubagentModel string `json:"subagent_model,omitempty"` SkillsDir string `json:"skills_dir,omitempty"` + + Models []ModelSetting `json:"models,omitempty"` + AgentModels map[string]string `json:"agent_models,omitempty"` } func defaultConfig() Config { @@ -240,3 +249,38 @@ func tightenPermission(path string, required os.FileMode) error { return os.Chmod(path, required) } + +// GetModelForAgent returns the ModelSetting for a given agent type. +// If not found, it returns false. +func (cfg *Config) GetModelForAgent(agentType string) (ModelSetting, bool) { + if cfg == nil || cfg.AgentModels == nil || cfg.Models == nil { + return ModelSetting{}, false + } + modelName, exists := cfg.AgentModels[agentType] + if !exists { + return ModelSetting{}, false + } + for _, m := range cfg.Models { + if m.Model == modelName { + return m, true + } + } + return ModelSetting{}, false +} + +// SaveConfig writes the configuration back to config.json. +func SaveConfig(cfg *Config) error { + lateConfigDir, err := pathutil.LateConfigDir() + if err != nil { + return err + } + configPath := filepath.Join(lateConfigDir, "config.json") + data, err := json.MarshalIndent(cfg, "", " ") + if err != nil { + return err + } + if err := os.WriteFile(configPath, data, configFilePerm); err != nil { + return err + } + return ensureSecureConfigPermissions(lateConfigDir, configPath) +} diff --git a/internal/config/config_test.go b/internal/config/config_test.go index e1589c2..8782d75 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -527,3 +527,40 @@ func TestResolveSubagentSettings(t *testing.T) { }) } } + +func TestConfig_GetModelForAgent(t *testing.T) { + cfg := &Config{ + Models: []ModelSetting{ + {URL: "http://localhost:8080", Key: "key-1", Model: "model-1"}, + {URL: "http://localhost:9090", Key: "key-2", Model: "model-2"}, + }, + AgentModels: map[string]string{ + "orchestrator": "model-1", + "coder": "model-2", + "unknown": "model-3", + }, + } + + tests := []struct { + agentType string + wantModel string + wantOk bool + }{ + {"orchestrator", "model-1", true}, + {"coder", "model-2", true}, + {"unknown", "", false}, + {"missing", "", false}, + } + + for _, tt := range tests { + t.Run(tt.agentType, func(t *testing.T) { + got, ok := cfg.GetModelForAgent(tt.agentType) + if ok != tt.wantOk { + t.Errorf("GetModelForAgent(%q) ok = %v, want %v", tt.agentType, ok, tt.wantOk) + } + if ok && got.Model != tt.wantModel { + t.Errorf("GetModelForAgent(%q) got model = %q, want %q", tt.agentType, got.Model, tt.wantModel) + } + }) + } +} diff --git a/internal/tui/model.go b/internal/tui/model.go index 56d21b5..906b1bc 100644 --- a/internal/tui/model.go +++ b/internal/tui/model.go @@ -2,6 +2,7 @@ package tui import ( "late/internal/common" + "late/internal/config" "os" "charm.land/bubbles/v2/filepicker" @@ -13,7 +14,7 @@ import ( "charm.land/lipgloss/v2" ) -func NewModel(root common.Orchestrator, renderer *glamour.TermRenderer) Model { +func NewModel(root common.Orchestrator, renderer *glamour.TermRenderer, cfg *config.Config) Model { ti := textarea.New() ti.Placeholder = "Ask Late anything..." ti.Focus() @@ -84,6 +85,7 @@ func NewModel(root common.Orchestrator, renderer *glamour.TermRenderer) Model { ShowCWD: true, cachedRendererWidth: -1, // Force first creation Pastes: make(map[string]string), + AppConfig: cfg, } fp := filepicker.New() diff --git a/internal/tui/paste_test.go b/internal/tui/paste_test.go index 60e8343..8baba72 100644 --- a/internal/tui/paste_test.go +++ b/internal/tui/paste_test.go @@ -52,7 +52,7 @@ func (k mockKey) Key() tea.Key { func TestPastePlaceholderReplacement(t *testing.T) { orch := &mockOrchestrator{} - model := NewModel(orch, nil) + model := NewModel(orch, nil, nil) // Simulate PasteMsg of 5 lines pasteText := "line1\nline2\nline3\nline4\nline5" @@ -99,7 +99,7 @@ func TestPastePlaceholderReplacement(t *testing.T) { func TestPasteBinaryIgnored(t *testing.T) { orch := &mockOrchestrator{} - model := NewModel(orch, nil) + model := NewModel(orch, nil, nil) // Set initial state model.Input.SetValue("> hello") @@ -123,7 +123,7 @@ func TestPasteBinaryIgnored(t *testing.T) { func TestPastePlaceholderSubmitNoCollision(t *testing.T) { orch := &mockOrchestrator{} - model := NewModel(orch, nil) + model := NewModel(orch, nil, nil) // Two multi-line pastes. The second paste's CONTENT contains a string // that looks exactly like a placeholder; it must survive submission diff --git a/internal/tui/state.go b/internal/tui/state.go index d965a56..cfd7e38 100644 --- a/internal/tui/state.go +++ b/internal/tui/state.go @@ -3,6 +3,7 @@ package tui import ( "late/internal/client" "late/internal/common" + "late/internal/config" "late/internal/git" "charm.land/bubbles/v2/filepicker" @@ -36,6 +37,7 @@ const ( ViewFilePicker ViewCommitLog ViewRewind + ViewModelPicker ) // Fixed layout heights (crush-style) @@ -51,6 +53,7 @@ var AvailableCommands = []string{ "/compose", "/help", "/log", + "/model", "/quit", "/rewind", } @@ -145,6 +148,15 @@ type Model struct { CWD string // Current working directory, shown in status bar ShowCWD bool // Whether to show current working directory in status bar + // Configuration + AppConfig *config.Config + + // Model picker fields + ModelPickerAgents []string + ModelPickerModels []string + ModelPickerAgentIndex int + ModelPickerAgentSelections map[string]int + // Esc confirmation EscConfirmPending bool // Show "are you sure?" when Esc pressed at main view escBgContent string // Saved viewport content to show underneath the dialog diff --git a/internal/tui/update.go b/internal/tui/update.go index 3063a4d..8d695fd 100644 --- a/internal/tui/update.go +++ b/internal/tui/update.go @@ -2,7 +2,9 @@ package tui import ( "fmt" + "late/internal/assets" "late/internal/common" + "late/internal/config" "late/internal/git" "math/rand/v2" "net/http" @@ -160,6 +162,10 @@ func (m Model) updateInternal(msg tea.Msg) (Model, tea.Cmd) { } } + if m.Mode == ViewModelPicker || m.Mode == ViewRewind || m.Mode == ViewCommitLog || m.Mode == ViewHelp { + forwardToInput = false + } + // Update Sub-models if forwardToInput { m.Input, tiCmd = m.Input.Update(msg) @@ -346,6 +352,93 @@ func (m Model) updateChat(msg tea.Msg) (Model, tea.Cmd) { case tea.KeyMsg: focusedState := m.GetAgentState(m.Focused.ID()) + // Model picker view key handling + if m.Mode == ViewModelPicker { + switch msg.String() { + case "up": + m.ModelPickerAgentIndex = max(0, m.ModelPickerAgentIndex-1) + m.updateViewport() + return m, nil + case "down": + m.ModelPickerAgentIndex = min(len(m.ModelPickerAgents)-1, m.ModelPickerAgentIndex+1) + m.updateViewport() + return m, nil + case "left": + if len(m.ModelPickerAgents) > 0 && len(m.ModelPickerModels) > 0 { + activeAgent := m.ModelPickerAgents[m.ModelPickerAgentIndex] + currentSel := m.ModelPickerAgentSelections[activeAgent] + newSel := max(0, currentSel-1) + m.ModelPickerAgentSelections[activeAgent] = newSel + m.updateViewport() + } + return m, nil + case "right": + if len(m.ModelPickerAgents) > 0 && len(m.ModelPickerModels) > 0 { + activeAgent := m.ModelPickerAgents[m.ModelPickerAgentIndex] + currentSel := m.ModelPickerAgentSelections[activeAgent] + newSel := min(len(m.ModelPickerModels)-1, currentSel+1) + m.ModelPickerAgentSelections[activeAgent] = newSel + m.updateViewport() + } + return m, nil + case "enter", "esc": + // Save choices to AppConfig + if m.AppConfig != nil { + if m.AppConfig.AgentModels == nil { + m.AppConfig.AgentModels = make(map[string]string) + } + for _, agent := range m.ModelPickerAgents { + selIdx := m.ModelPickerAgentSelections[agent] + modelName := m.ModelPickerModels[selIdx] + if modelName == "default" { + delete(m.AppConfig.AgentModels, agent) + } else { + m.AppConfig.AgentModels[agent] = modelName + } + } + // Write config to disk + if err := config.SaveConfig(m.AppConfig); err != nil { + m.Err = fmt.Errorf("failed to save config: %w", err) + return m, nil + } + + // Update ModelName and SubagentInfo dynamically + if model, ok := m.AppConfig.AgentModels["orchestrator"]; ok { + m.ModelName = model + } else { + resolvedOpenAIConfig := config.ResolveOpenAISettings(m.AppConfig) + m.ModelName = resolvedOpenAIConfig.Model + } + + var subagentInfos []string + for _, sub := range assets.GetSubagents() { + if model, ok := m.AppConfig.AgentModels[sub.Name]; ok { + subagentInfos = append(subagentInfos, fmt.Sprintf("%s:%s", sub.Name, model)) + } + } + if len(subagentInfos) > 0 { + m.SubagentInfo = strings.Join(subagentInfos, ", ") + } else { + resolvedSubagentConfig := config.ResolveSubagentSettings(m.AppConfig, config.ResolveOpenAISettings(m.AppConfig)) + m.SubagentInfo = resolvedSubagentConfig.Model + } + } + + m.ToastMessage = "agent models updated" + m.ToastExpireTime = time.Now().UnixMilli() + 3000 + clearCmd := tea.Tick(3*time.Second, func(t time.Time) tea.Msg { + return clearToastMsg{} + }) + + m.Mode = ViewChat + focusedState.RenderedHistory = nil + m.updateLayout() + m.updateViewport() + return m, clearCmd + } + return m, nil + } + // Rewind view key handling if m.Mode == ViewRewind { switch msg.String() { @@ -615,6 +708,49 @@ func (m Model) updateChat(msg tea.Msg) (Model, tea.Cmd) { m.updateLayout() return m, nil } + if cmd == "/model" { + m.Input.Reset() + m.Input.SetValue("> ") + m.Mode = ViewModelPicker + + // Populate agent types + m.ModelPickerAgents = []string{"orchestrator"} + for _, sub := range assets.GetSubagents() { + m.ModelPickerAgents = append(m.ModelPickerAgents, sub.Name) + } + + // Populate model names + m.ModelPickerModels = []string{"default"} + if m.AppConfig != nil { + for _, model := range m.AppConfig.Models { + m.ModelPickerModels = append(m.ModelPickerModels, model.Model) + } + } + + m.ModelPickerAgentIndex = 0 + m.ModelPickerAgentSelections = make(map[string]int) + + // Load current selections + for _, agentName := range m.ModelPickerAgents { + selectedModel := "" + if m.AppConfig != nil && m.AppConfig.AgentModels != nil { + selectedModel = m.AppConfig.AgentModels[agentName] + } + // Find in ModelPickerModels + foundIdx := 0 // default to 0 ("default") + for idx, modelName := range m.ModelPickerModels { + if modelName == selectedModel { + foundIdx = idx + break + } + } + m.ModelPickerAgentSelections[agentName] = foundIdx + } + + focusedState.RenderedHistory = nil + m.updateLayout() + return m, nil + } if cmd == "/clear" { m.Input.Reset() m.Input.SetValue("> ") @@ -961,6 +1097,9 @@ func (m *Model) updateLayout() { m.Viewport.SetWidth(availableWidth) vHeight := m.Height - (m.Input.Height() + 1) - StatusBarHeight - AppPadding + if m.Mode == ViewModelPicker { + vHeight = m.Height - 3 - StatusBarHeight - AppPadding + } // Reserve space for autocomplete dropdown if m.ShowAutocomplete && len(m.AutocompleteItems) > 0 { diff --git a/internal/tui/view.go b/internal/tui/view.go index 02dccd7..6d2a875 100644 --- a/internal/tui/view.go +++ b/internal/tui/view.go @@ -51,6 +51,21 @@ func (m Model) View() tea.View { Width(m.Width). Render(pickerHints) } + if m.Mode == ViewModelPicker { + hUpDn := lipgloss.JoinHorizontal(lipgloss.Left, statusKeyStyle.Render("↑/↓"), statusTextStyle.Render(" Select Agent ")) + hLfRt := lipgloss.JoinHorizontal(lipgloss.Left, statusKeyStyle.Render("←/→"), statusTextStyle.Render(" Choose Model ")) + hEnter := lipgloss.JoinHorizontal(lipgloss.Left, statusKeyStyle.Render("Enter/Esc"), statusTextStyle.Render(" Save & Close ")) + pickerHints := lipgloss.JoinHorizontal(lipgloss.Left, hUpDn, statusBg(" "), hLfRt, statusBg(" "), hEnter) + + iStr = lipgloss.NewStyle(). + Border(lipgloss.NormalBorder(), true, false, false, false). + BorderForeground(lipgloss.Color("#232329")). + BorderBackground(appBgColor). + Background(appBgColor). + Width(m.Width). + Padding(1, 2). + Render(pickerHints) + } aStr := m.autocompleteView() sStr := m.statusBarView() @@ -296,6 +311,36 @@ func (m *Model) statusBarView() string { return "" } + if m.Mode == ViewModelPicker { + bullet := lipgloss.NewStyle().Foreground(primaryColor).Background(appBgColor).Render("◆") + label := lipgloss.NewStyle().Foreground(primaryColor).Background(appBgColor).Bold(true).Render("models") + leftSection := bullet + statusBg(" ") + label + + status := lipgloss.NewStyle().Foreground(subtextColor).Background(appBgColor).Render("Configuring active agent models") + hasToast := m.ToastMessage != "" && time.Now().UnixMilli() < m.ToastExpireTime + if hasToast { + status = lipgloss.NewStyle().Foreground(primaryColor).Background(appBgColor).Bold(true).Render("✓ " + m.ToastMessage) + } + + rightSection := lipgloss.NewStyle().Foreground(subtextColor).Background(appBgColor).Render("esc/enter Save & Close") + + usableW := w - 2 + leftWidth := lipgloss.Width(leftSection) + rightWidth := lipgloss.Width(rightSection) + statusWidth := lipgloss.Width(status) + + spaceWidth := usableW - leftWidth - rightWidth - statusWidth - 3 + if spaceWidth < 0 { + spaceWidth = 0 + } + space := statusBg(strings.Repeat(" ", spaceWidth)) + + parts := []string{leftSection, statusBg(" "), status, space, rightSection} + content := lipgloss.JoinHorizontal(lipgloss.Left, parts...) + paddedContent := statusBg(" ") + content + statusBg(" ") + return statusBarBaseStyle.Width(w).Render(paddedContent) + } + s := m.GetAgentState(m.Focused.ID()) var leftSection string @@ -474,6 +519,11 @@ func (m *Model) updateViewport() { return } + if m.Mode == ViewModelPicker { + m.renderModelPickerView() + return + } + if m.Mode == ViewCommitLog { m.renderCommitLogView() return @@ -1308,3 +1358,128 @@ func splitMarkdownChunks(content string) (complete []string, tail string) { tail = content[lastSplit:] return } + +// renderModelPickerView renders the active agent models configuring list in the viewport. +func (m *Model) renderModelPickerView() { + s := m.GetAgentState(m.Focused.ID()) + s.LastTotalContent = "" + + msgWidth := m.Viewport.Width() - 2 + if msgWidth < 1 { + msgWidth = 80 + } + + var lines []string + header := lipgloss.NewStyle(). + Foreground(primaryColor). + Bold(true). + Background(appBgColor). + PaddingLeft(1). + Render("── Configure Agent Models ──────────────────────────") + lines = append(lines, header, "") + + if len(m.ModelPickerModels) <= 1 && (m.AppConfig == nil || len(m.AppConfig.Models) == 0) { + lines = append(lines, lipgloss.NewStyle(). + Foreground(warningColor). + Background(appBgColor). + PaddingLeft(2). + Render("No models configured in ~/.config/late/config.json")) + lines = append(lines, "", lipgloss.NewStyle(). + Foreground(subtextColor). + Background(appBgColor). + PaddingLeft(2). + Render("Please add a 'models' array to your config file first.")) + } else { + // Instructions + lines = append(lines, lipgloss.NewStyle(). + Foreground(subtextColor). + Background(appBgColor). + PaddingLeft(2). + Render("Use ↑/↓ to choose an agent, and ←/→ to select a model."), "") + + // Print agents and their models + for aIdx, agentName := range m.ModelPickerAgents { + agentLabel := agentName + if agentName == "orchestrator" { + agentLabel = "main/orchestrator" + } + + // Highlight the active row/agent + agentStyle := lipgloss.NewStyle().Foreground(textColor) + prefix := " " + if aIdx == m.ModelPickerAgentIndex { + prefix = "▸ " + agentStyle = lipgloss.NewStyle().Foreground(primaryColor).Bold(true) + } + + // Render agent name, right-padded + agentNamePart := prefix + agentLabel + agentNameStr := fmt.Sprintf("%-22s", agentNamePart) + agentNameRendered := agentStyle.Background(appBgColor).Render(agentNameStr) + + // Build the model choices list for this agent + var modelChoices []string + selectedIdx := m.ModelPickerAgentSelections[agentName] + + for mIdx, modelName := range m.ModelPickerModels { + modelLabel := modelName + + var optStr string + if mIdx == selectedIdx { + // This option is selected + if aIdx == m.ModelPickerAgentIndex { + // Row is active: highlight selected model with primary color + optStr = lipgloss.NewStyle(). + Foreground(appBgColor). + Background(primaryColor). + Bold(true). + Padding(0, 1). + Render(modelLabel) + } else { + // Row is inactive: highlight selected model with secondary color + optStr = lipgloss.NewStyle(). + Foreground(appBgColor). + Background(secondaryColor). + Bold(true). + Padding(0, 1). + Render(modelLabel) + } + } else { + // Not selected + optStr = lipgloss.NewStyle(). + Foreground(subtextColor). + Background(appBgColor). + Padding(0, 1). + Render(modelLabel) + } + modelChoices = append(modelChoices, optStr) + } + + rowContent := agentNameRendered + strings.Join(modelChoices, " ") + + // Wrap row in a box style if it's active for extra pop + rowStyle := lipgloss.NewStyle().Background(appBgColor) + if aIdx == m.ModelPickerAgentIndex { + rowStyle = lipgloss.NewStyle().Background(thoughtBgColor) + } + + lines = append(lines, rowStyle.Render(rowContent)) + } + } + + lines = append(lines, "", "") + + // Footer hints + footer := lipgloss.NewStyle(). + Foreground(subtextColor). + Background(appBgColor). + PaddingLeft(2). + Render("Press [Enter] or [Esc] to save and exit.") + lines = append(lines, footer) + + paddedContent := lipgloss.NewStyle(). + Width(m.Viewport.Width()). + Background(appBgColor). + Render(strings.Join(lines, "\n")) + m.Viewport.SetContent(paddedContent) +} From 3f6cb4b108983e6420dce8ca17c25f6dc6df4565 Mon Sep 17 00:00:00 2001 From: ml Date: Sun, 19 Jul 2026 15:21:54 +0200 Subject: [PATCH 2/2] fix: split enter,esc --- internal/tui/update.go | 9 ++++++++- internal/tui/view.go | 9 +++++---- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/internal/tui/update.go b/internal/tui/update.go index 8d695fd..0c3b295 100644 --- a/internal/tui/update.go +++ b/internal/tui/update.go @@ -381,7 +381,7 @@ func (m Model) updateChat(msg tea.Msg) (Model, tea.Cmd) { m.updateViewport() } return m, nil - case "enter", "esc": + case "enter": // Save choices to AppConfig if m.AppConfig != nil { if m.AppConfig.AgentModels == nil { @@ -435,6 +435,13 @@ func (m Model) updateChat(msg tea.Msg) (Model, tea.Cmd) { m.updateLayout() m.updateViewport() return m, clearCmd + + case "esc": + m.Mode = ViewChat + focusedState.RenderedHistory = nil + m.updateLayout() + m.updateViewport() + return m, nil } return m, nil } diff --git a/internal/tui/view.go b/internal/tui/view.go index 6d2a875..2e6cbcb 100644 --- a/internal/tui/view.go +++ b/internal/tui/view.go @@ -54,8 +54,9 @@ func (m Model) View() tea.View { if m.Mode == ViewModelPicker { hUpDn := lipgloss.JoinHorizontal(lipgloss.Left, statusKeyStyle.Render("↑/↓"), statusTextStyle.Render(" Select Agent ")) hLfRt := lipgloss.JoinHorizontal(lipgloss.Left, statusKeyStyle.Render("←/→"), statusTextStyle.Render(" Choose Model ")) - hEnter := lipgloss.JoinHorizontal(lipgloss.Left, statusKeyStyle.Render("Enter/Esc"), statusTextStyle.Render(" Save & Close ")) - pickerHints := lipgloss.JoinHorizontal(lipgloss.Left, hUpDn, statusBg(" "), hLfRt, statusBg(" "), hEnter) + hEnter := lipgloss.JoinHorizontal(lipgloss.Left, statusKeyStyle.Render("Enter"), statusTextStyle.Render(" Save ")) + hEsc := lipgloss.JoinHorizontal(lipgloss.Left, statusKeyStyle.Render("Esc"), statusTextStyle.Render(" Cancel ")) + pickerHints := lipgloss.JoinHorizontal(lipgloss.Left, hUpDn, statusBg(" "), hLfRt, statusBg(" "), hEnter, statusBg(" "), hEsc) iStr = lipgloss.NewStyle(). Border(lipgloss.NormalBorder(), true, false, false, false). @@ -322,7 +323,7 @@ func (m *Model) statusBarView() string { status = lipgloss.NewStyle().Foreground(primaryColor).Background(appBgColor).Bold(true).Render("✓ " + m.ToastMessage) } - rightSection := lipgloss.NewStyle().Foreground(subtextColor).Background(appBgColor).Render("esc/enter Save & Close") + rightSection := lipgloss.NewStyle().Foreground(subtextColor).Background(appBgColor).Render("enter Save • esc Cancel") usableW := w - 2 leftWidth := lipgloss.Width(leftSection) @@ -1474,7 +1475,7 @@ func (m *Model) renderModelPickerView() { Foreground(subtextColor). Background(appBgColor). PaddingLeft(2). - Render("Press [Enter] or [Esc] to save and exit.") + Render("Press [Enter] to save, [Esc] to cancel.") lines = append(lines, footer) paddedContent := lipgloss.NewStyle().