Detailed changes
@@ -8,7 +8,8 @@ import (
"github.com/atotto/clipboard"
"github.com/charmbracelet/bubbles/v2/key"
tea "github.com/charmbracelet/bubbletea/v2"
- "github.com/charmbracelet/crush/internal/app"
+ "github.com/charmbracelet/crush/internal/client"
+ "github.com/charmbracelet/crush/internal/config"
"github.com/charmbracelet/crush/internal/llm/agent"
"github.com/charmbracelet/crush/internal/message"
"github.com/charmbracelet/crush/internal/permission"
@@ -58,7 +59,8 @@ type MessageListCmp interface {
// of chat messages with support for tool calls, real-time updates, and
// session switching.
type messageListCmp struct {
- app *app.App
+ client *client.Client
+ cfg *config.Config
width, height int
session session.Session
listCmp list.List[list.Item]
@@ -77,7 +79,7 @@ type messageListCmp struct {
// New creates a new message list component with custom keybindings
// and reverse ordering (newest messages at bottom).
-func New(app *app.App) MessageListCmp {
+func New(app *client.Client, cfg *config.Config) MessageListCmp {
defaultListKeyMap := list.DefaultKeyMap()
listCmp := list.New(
[]list.Item{},
@@ -88,7 +90,8 @@ func New(app *app.App) MessageListCmp {
list.WithEnableMouse(),
)
return &messageListCmp{
- app: app,
+ client: app,
+ cfg: cfg,
listCmp: listCmp,
previousSelected: "",
defaultListKeyMap: defaultListKeyMap,
@@ -103,8 +106,9 @@ func (m *messageListCmp) Init() tea.Cmd {
// Update handles incoming messages and updates the component state.
func (m *messageListCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmds []tea.Cmd
- if m.session.ID != "" && m.app.CoderAgent != nil {
- queueSize := m.app.CoderAgent.QueuedPrompts(m.session.ID)
+ info, err := m.client.GetAgentInfo(context.TODO())
+ if m.session.ID != "" && err == nil && !info.IsZero() {
+ queueSize, _ := m.client.GetAgentSessionQueuedPrompts(context.TODO(), m.session.ID)
if queueSize != m.promptQueue {
m.promptQueue = queueSize
cmds = append(cmds, m.SetSize(m.width, m.height))
@@ -235,7 +239,8 @@ func (m *messageListCmp) View() string {
m.listCmp.View(),
),
}
- if m.app.CoderAgent != nil && m.promptQueue > 0 {
+ info, err := m.client.GetAgentInfo(context.TODO())
+ if err == nil && !info.IsZero() && m.promptQueue > 0 {
queuePill := queuePill(m.promptQueue, t)
view = append(view, t.S().Base.PaddingLeft(4).PaddingTop(1).Render(queuePill))
}
@@ -289,7 +294,6 @@ func (m *messageListCmp) handleChildSession(event pubsub.Event[message.Message])
nestedCall := messages.NewToolCallCmp(
event.Payload.ID,
tc,
- m.app.Permissions,
messages.WithToolCallNested(true),
)
cmds = append(cmds, nestedCall.Init())
@@ -369,7 +373,7 @@ func (m *messageListCmp) handleNewMessage(msg message.Message) tea.Cmd {
// handleNewUserMessage adds a new user message to the list and updates the timestamp.
func (m *messageListCmp) handleNewUserMessage(msg message.Message) tea.Cmd {
m.lastUserMessageTime = msg.CreatedAt
- return m.listCmp.AppendItem(messages.NewMessageCmp(msg))
+ return m.listCmp.AppendItem(messages.NewMessageCmp(m.cfg, msg))
}
// handleToolMessage updates existing tool calls with their results.
@@ -462,6 +466,7 @@ func (m *messageListCmp) updateAssistantMessageContent(msg message.Message, assi
if msg.FinishPart() != nil && msg.FinishPart().Reason == message.FinishReasonEndTurn {
m.listCmp.AppendItem(
messages.NewAssistantSection(
+ m.cfg,
msg,
time.Unix(m.lastUserMessageTime, 0),
),
@@ -508,7 +513,7 @@ func (m *messageListCmp) updateOrAddToolCall(msg message.Message, tc message.Too
}
// Add new tool call if not found
- return m.listCmp.AppendItem(messages.NewToolCallCmp(msg.ID, tc, m.app.Permissions))
+ return m.listCmp.AppendItem(messages.NewToolCallCmp(msg.ID, tc))
}
// handleNewAssistantMessage processes new assistant messages and their tool calls.
@@ -519,6 +524,7 @@ func (m *messageListCmp) handleNewAssistantMessage(msg message.Message) tea.Cmd
if m.shouldShowAssistantMessage(msg) {
cmd := m.listCmp.AppendItem(
messages.NewMessageCmp(
+ m.cfg,
msg,
),
)
@@ -527,7 +533,7 @@ func (m *messageListCmp) handleNewAssistantMessage(msg message.Message) tea.Cmd
// Add tool calls
for _, tc := range msg.ToolCalls() {
- cmd := m.listCmp.AppendItem(messages.NewToolCallCmp(msg.ID, tc, m.app.Permissions))
+ cmd := m.listCmp.AppendItem(messages.NewToolCallCmp(msg.ID, tc))
cmds = append(cmds, cmd)
}
@@ -541,7 +547,7 @@ func (m *messageListCmp) SetSession(session session.Session) tea.Cmd {
}
m.session = session
- sessionMessages, err := m.app.Messages.List(context.Background(), session.ID)
+ sessionMessages, err := m.client.ListMessages(context.Background(), session.ID)
if err != nil {
return util.ReportError(err)
}
@@ -581,11 +587,11 @@ func (m *messageListCmp) convertMessagesToUI(sessionMessages []message.Message,
switch msg.Role {
case message.User:
m.lastUserMessageTime = msg.CreatedAt
- uiMessages = append(uiMessages, messages.NewMessageCmp(msg))
+ uiMessages = append(uiMessages, messages.NewMessageCmp(m.cfg, msg))
case message.Assistant:
uiMessages = append(uiMessages, m.convertAssistantMessage(msg, toolResultMap)...)
if msg.FinishPart() != nil && msg.FinishPart().Reason == message.FinishReasonEndTurn {
- uiMessages = append(uiMessages, messages.NewAssistantSection(msg, time.Unix(m.lastUserMessageTime, 0)))
+ uiMessages = append(uiMessages, messages.NewAssistantSection(m.cfg, msg, time.Unix(m.lastUserMessageTime, 0)))
}
}
}
@@ -602,7 +608,7 @@ func (m *messageListCmp) convertAssistantMessage(msg message.Message, toolResult
uiMessages = append(
uiMessages,
messages.NewMessageCmp(
- msg,
+ m.cfg, msg,
),
)
}
@@ -610,10 +616,10 @@ func (m *messageListCmp) convertAssistantMessage(msg message.Message, toolResult
// Add tool calls with their results and status
for _, tc := range msg.ToolCalls() {
options := m.buildToolCallOptions(tc, msg, toolResultMap)
- uiMessages = append(uiMessages, messages.NewToolCallCmp(msg.ID, tc, m.app.Permissions, options...))
+ uiMessages = append(uiMessages, messages.NewToolCallCmp(msg.ID, tc, options...))
// If this tool call is the agent tool, fetch nested tool calls
if tc.Name == agent.AgentToolName {
- nestedMessages, _ := m.app.Messages.List(context.Background(), tc.ID)
+ nestedMessages, _ := m.client.ListMessages(context.Background(), tc.ID)
nestedToolResultMap := m.buildToolResultMap(nestedMessages)
nestedUIMessages := m.convertMessagesToUI(nestedMessages, nestedToolResultMap)
nestedToolCalls := make([]messages.ToolCallCmp, 0, len(nestedUIMessages))
@@ -16,7 +16,7 @@ import (
"github.com/charmbracelet/bubbles/v2/key"
"github.com/charmbracelet/bubbles/v2/textarea"
tea "github.com/charmbracelet/bubbletea/v2"
- "github.com/charmbracelet/crush/internal/app"
+ "github.com/charmbracelet/crush/internal/client"
"github.com/charmbracelet/crush/internal/fsext"
"github.com/charmbracelet/crush/internal/message"
"github.com/charmbracelet/crush/internal/session"
@@ -53,7 +53,7 @@ type editorCmp struct {
width int
height int
x, y int
- app *app.App
+ app *client.Client
session session.Session
textarea *textarea.Model
attachments []message.Attachment
@@ -211,7 +211,8 @@ func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
case commands.OpenExternalEditorMsg:
- if m.app.CoderAgent.IsSessionBusy(m.session.ID) {
+ info, err := m.app.GetAgentSessionInfo(context.TODO(), m.session.ID)
+ if err == nil && info.IsBusy {
return m, util.ReportWarn("Agent is working, please wait...")
}
return m, m.openEditor(m.textarea.Value())
@@ -297,7 +298,8 @@ func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
}
if key.Matches(msg, m.keyMap.OpenEditor) {
- if m.app.CoderAgent.IsSessionBusy(m.session.ID) {
+ info, err := m.app.GetAgentSessionInfo(context.TODO(), m.session.ID)
+ if err == nil && info.IsBusy {
return m, util.ReportWarn("Agent is working, please wait...")
}
return m, m.openEditor(m.textarea.Value())
@@ -365,7 +367,8 @@ func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
func (m *editorCmp) setEditorPrompt() {
- if m.app.Permissions.SkipRequests() {
+ skip, err := m.app.GetPermissionsSkipRequests(context.TODO())
+ if err == nil && skip {
m.textarea.SetPromptFunc(4, yoloPromptFunc)
return
}
@@ -415,12 +418,14 @@ func (m *editorCmp) randomizePlaceholders() {
func (m *editorCmp) View() string {
t := styles.CurrentTheme()
// Update placeholder
- if m.app.CoderAgent != nil && m.app.CoderAgent.IsBusy() {
+ info, err := m.app.GetAgentInfo(context.TODO())
+ if err == nil && info.IsBusy {
m.textarea.Placeholder = m.workingPlaceholder
} else {
m.textarea.Placeholder = m.readyPlaceholder
}
- if m.app.Permissions.SkipRequests() {
+ skip, err := m.app.GetPermissionsSkipRequests(context.TODO())
+ if err == nil && skip {
m.textarea.Placeholder = "Yolo mode!"
}
if len(m.attachments) == 0 {
@@ -563,7 +568,7 @@ func yoloPromptFunc(info textarea.PromptInfo) string {
return fmt.Sprintf("%s ", t.YoloDotsBlurred)
}
-func New(app *app.App) Editor {
+func New(app *client.Client) Editor {
t := styles.CurrentTheme()
ta := textarea.New()
ta.SetStyles(t.S().TextArea)
@@ -1,14 +1,14 @@
package header
import (
+ "context"
"fmt"
"strings"
tea "github.com/charmbracelet/bubbletea/v2"
+ "github.com/charmbracelet/crush/internal/client"
"github.com/charmbracelet/crush/internal/config"
- "github.com/charmbracelet/crush/internal/csync"
"github.com/charmbracelet/crush/internal/fsext"
- "github.com/charmbracelet/crush/internal/lsp"
"github.com/charmbracelet/crush/internal/pubsub"
"github.com/charmbracelet/crush/internal/session"
"github.com/charmbracelet/crush/internal/tui/styles"
@@ -29,14 +29,16 @@ type Header interface {
type header struct {
width int
session session.Session
- lspClients *csync.Map[string, *lsp.Client]
+ client *client.Client
+ cfg *config.Config
detailsOpen bool
}
-func New(lspClients *csync.Map[string, *lsp.Client]) Header {
+func New(lspClients *client.Client, cfg *config.Config) Header {
return &header{
- lspClients: lspClients,
- width: 0,
+ client: lspClients,
+ cfg: cfg,
+ width: 0,
}
}
@@ -105,8 +107,19 @@ func (h *header) details(availWidth int) string {
var parts []string
errorCount := 0
- for l := range h.lspClients.Seq() {
- for _, diagnostics := range l.GetDiagnostics() {
+ // TODO: Move this to update?
+ lsps, err := h.client.GetLSPs(context.TODO())
+ if err != nil {
+ return ""
+ }
+
+ for l := range lsps {
+ // TODO: Same here, move to update?
+ diags, err := h.client.GetLSPDiagnostics(context.TODO(), l)
+ if err != nil {
+ return ""
+ }
+ for _, diagnostics := range diags {
for _, diagnostic := range diagnostics {
if diagnostic.Severity == protocol.SeverityError {
errorCount++
@@ -119,8 +132,11 @@ func (h *header) details(availWidth int) string {
parts = append(parts, s.Error.Render(fmt.Sprintf("%s%d", styles.ErrorIcon, errorCount)))
}
- agentCfg := config.Get().Agents["coder"]
- model := config.Get().GetModelByType(agentCfg.Model)
+ agentCfg := h.cfg.Agents["coder"]
+ model := h.cfg.GetModelByType(agentCfg.Model)
+ if model == nil {
+ return "No model"
+ }
percentage := (float64(h.session.CompletionTokens+h.session.PromptTokens) / float64(model.ContextWindow)) * 100
formattedPercentage := s.Muted.Render(fmt.Sprintf("%d%%", int(percentage)))
parts = append(parts, formattedPercentage)
@@ -138,7 +154,7 @@ func (h *header) details(availWidth int) string {
// Truncate cwd if necessary, and insert it at the beginning.
const dirTrimLimit = 4
- cwd := fsext.DirTrim(fsext.PrettyPath(config.Get().WorkingDir()), dirTrimLimit)
+ cwd := fsext.DirTrim(fsext.PrettyPath(h.cfg.WorkingDir()), dirTrimLimit)
cwd = ansi.Truncate(cwd, max(0, availWidth-lipgloss.Width(metadata)), "…")
cwd = s.Muted.Render(cwd)
@@ -57,6 +57,8 @@ type messageCmp struct {
// Thinking viewport for displaying reasoning content
thinkingViewport viewport.Model
+
+ cfg *config.Config
}
var focusedMessageBorder = lipgloss.Border{
@@ -64,7 +66,7 @@ var focusedMessageBorder = lipgloss.Border{
}
// NewMessageCmp creates a new message component with the given message and options
-func NewMessageCmp(msg message.Message) MessageCmp {
+func NewMessageCmp(cfg *config.Config, msg message.Message) MessageCmp {
t := styles.CurrentTheme()
thinkingViewport := viewport.New()
@@ -72,6 +74,7 @@ func NewMessageCmp(msg message.Message) MessageCmp {
thinkingViewport.KeyMap = viewport.KeyMap{}
m := &messageCmp{
+ cfg: cfg,
message: msg,
anim: anim.New(anim.Settings{
Size: 15,
@@ -363,6 +366,7 @@ type assistantSectionModel struct {
id string
message message.Message
lastUserMessageTime time.Time
+ cfg *config.Config
}
// ID implements AssistantSection.
@@ -370,12 +374,13 @@ func (m *assistantSectionModel) ID() string {
return m.id
}
-func NewAssistantSection(message message.Message, lastUserMessageTime time.Time) AssistantSection {
+func NewAssistantSection(cfg *config.Config, message message.Message, lastUserMessageTime time.Time) AssistantSection {
return &assistantSectionModel{
width: 0,
id: uuid.NewString(),
message: message,
lastUserMessageTime: lastUserMessageTime,
+ cfg: cfg,
}
}
@@ -394,7 +399,7 @@ func (m *assistantSectionModel) View() string {
duration := finishTime.Sub(m.lastUserMessageTime)
infoMsg := t.S().Subtle.Render(duration.String())
icon := t.S().Subtle.Render(styles.ModelIcon)
- model := config.Get().GetModel(m.message.Provider, m.message.Model)
+ model := m.cfg.GetModel(m.message.Provider, m.message.Model)
if model == nil {
// This means the model is not configured anymore
model = &catwalk.Model{
@@ -15,7 +15,6 @@ import (
"github.com/charmbracelet/crush/internal/llm/agent"
"github.com/charmbracelet/crush/internal/llm/tools"
"github.com/charmbracelet/crush/internal/message"
- "github.com/charmbracelet/crush/internal/permission"
"github.com/charmbracelet/crush/internal/tui/components/anim"
"github.com/charmbracelet/crush/internal/tui/components/core/layout"
"github.com/charmbracelet/crush/internal/tui/styles"
@@ -110,7 +109,7 @@ func WithToolPermissionGranted() ToolCallOption {
// NewToolCallCmp creates a new tool call component with the given parent message ID,
// tool call, and optional configuration
-func NewToolCallCmp(parentMessageID string, tc message.ToolCall, permissions permission.Service, opts ...ToolCallOption) ToolCallCmp {
+func NewToolCallCmp(parentMessageID string, tc message.ToolCall, opts ...ToolCallOption) ToolCallCmp {
m := &toolCallCmp{
call: tc,
parentMessageID: parentMessageID,
@@ -8,13 +8,13 @@ import (
tea "github.com/charmbracelet/bubbletea/v2"
"github.com/charmbracelet/catwalk/pkg/catwalk"
+ "github.com/charmbracelet/crush/internal/client"
"github.com/charmbracelet/crush/internal/config"
"github.com/charmbracelet/crush/internal/csync"
"github.com/charmbracelet/crush/internal/diff"
"github.com/charmbracelet/crush/internal/fsext"
"github.com/charmbracelet/crush/internal/history"
"github.com/charmbracelet/crush/internal/home"
- "github.com/charmbracelet/crush/internal/lsp"
"github.com/charmbracelet/crush/internal/pubsub"
"github.com/charmbracelet/crush/internal/session"
"github.com/charmbracelet/crush/internal/tui/components/chat"
@@ -69,16 +69,16 @@ type sidebarCmp struct {
session session.Session
logo string
cwd string
- lspClients *csync.Map[string, *lsp.Client]
+ client *client.Client
compactMode bool
- history history.Service
files *csync.Map[string, SessionFile]
+ cfg *config.Config
}
-func New(history history.Service, lspClients *csync.Map[string, *lsp.Client], compact bool) Sidebar {
+func New(c *client.Client, cfg *config.Config, compact bool) Sidebar {
return &sidebarCmp{
- lspClients: lspClients,
- history: history,
+ client: c,
+ cfg: cfg,
compactMode: compact,
files: csync.NewMap[string, SessionFile](),
}
@@ -194,7 +194,7 @@ func (m *sidebarCmp) handleFileHistoryEvent(event pubsub.Event[history.File]) te
before, _ := fsext.ToUnixLineEndings(existing.History.initialVersion.Content)
after, _ := fsext.ToUnixLineEndings(existing.History.latestVersion.Content)
path := existing.History.initialVersion.Path
- cwd := config.Get().WorkingDir()
+ cwd := m.cfg.WorkingDir()
path = strings.TrimPrefix(path, cwd)
_, additions, deletions := diff.GenerateDiff(before, after, path)
existing.Additions = additions
@@ -221,7 +221,7 @@ func (m *sidebarCmp) handleFileHistoryEvent(event pubsub.Event[history.File]) te
}
func (m *sidebarCmp) loadSessionFiles() tea.Msg {
- files, err := m.history.ListBySession(context.Background(), m.session.ID)
+ files, err := m.client.ListSessionHistoryFiles(context.Background(), m.session.ID)
if err != nil {
return util.InfoMsg{
Type: util.InfoTypeError,
@@ -247,7 +247,7 @@ func (m *sidebarCmp) loadSessionFiles() tea.Msg {
sessionFiles := make([]SessionFile, 0, len(fileMap))
for path, fh := range fileMap {
- cwd := config.Get().WorkingDir()
+ cwd := m.cfg.WorkingDir()
path = strings.TrimPrefix(path, cwd)
before, _ := fsext.ToUnixLineEndings(fh.initialVersion.Content)
after, _ := fsext.ToUnixLineEndings(fh.latestVersion.Content)
@@ -267,7 +267,7 @@ func (m *sidebarCmp) loadSessionFiles() tea.Msg {
func (m *sidebarCmp) SetSize(width, height int) tea.Cmd {
m.logo = m.logoBlock()
- m.cwd = cwd()
+ m.cwd = cwd(m.cfg)
m.width = width
m.height = height
return nil
@@ -406,7 +406,7 @@ func (m *sidebarCmp) filesBlockCompact(maxWidth int) string {
maxItems = min(maxItems, availableHeight)
}
- return files.RenderFileBlock(fileSlice, files.RenderOptions{
+ return files.RenderFileBlock(m.cfg, fileSlice, files.RenderOptions{
MaxWidth: maxWidth,
MaxItems: maxItems,
ShowSection: true,
@@ -417,14 +417,14 @@ func (m *sidebarCmp) filesBlockCompact(maxWidth int) string {
// lspBlockCompact renders the LSP block with limited width and height for horizontal layout
func (m *sidebarCmp) lspBlockCompact(maxWidth int) string {
// Limit items for horizontal layout
- lspConfigs := config.Get().LSP.Sorted()
+ lspConfigs := m.cfg.LSP.Sorted()
maxItems := min(5, len(lspConfigs))
availableHeight := m.height - 8
if availableHeight > 0 {
maxItems = min(maxItems, availableHeight)
}
- return lspcomponent.RenderLSPBlock(m.lspClients, lspcomponent.RenderOptions{
+ return lspcomponent.RenderLSPBlock(m.client, m.cfg, lspcomponent.RenderOptions{
MaxWidth: maxWidth,
MaxItems: maxItems,
ShowSection: true,
@@ -435,13 +435,13 @@ func (m *sidebarCmp) lspBlockCompact(maxWidth int) string {
// mcpBlockCompact renders the MCP block with limited width and height for horizontal layout
func (m *sidebarCmp) mcpBlockCompact(maxWidth int) string {
// Limit items for horizontal layout
- maxItems := min(5, len(config.Get().MCP.Sorted()))
+ maxItems := min(5, len(m.cfg.MCP.Sorted()))
availableHeight := m.height - 8
if availableHeight > 0 {
maxItems = min(maxItems, availableHeight)
}
- return mcp.RenderMCPBlock(mcp.RenderOptions{
+ return mcp.RenderMCPBlock(m.cfg, mcp.RenderOptions{
MaxWidth: maxWidth,
MaxItems: maxItems,
ShowSection: true,
@@ -469,7 +469,7 @@ func (m *sidebarCmp) filesBlock() string {
maxFiles, _, _ := m.getDynamicLimits()
maxFiles = min(len(fileSlice), maxFiles)
- return files.RenderFileBlock(fileSlice, files.RenderOptions{
+ return files.RenderFileBlock(m.cfg, fileSlice, files.RenderOptions{
MaxWidth: m.getMaxWidth(),
MaxItems: maxFiles,
ShowSection: true,
@@ -480,10 +480,10 @@ func (m *sidebarCmp) filesBlock() string {
func (m *sidebarCmp) lspBlock() string {
// Limit the number of LSPs shown
_, maxLSPs, _ := m.getDynamicLimits()
- lspConfigs := config.Get().LSP.Sorted()
+ lspConfigs := m.cfg.LSP.Sorted()
maxLSPs = min(len(lspConfigs), maxLSPs)
- return lspcomponent.RenderLSPBlock(m.lspClients, lspcomponent.RenderOptions{
+ return lspcomponent.RenderLSPBlock(m.client, m.cfg, lspcomponent.RenderOptions{
MaxWidth: m.getMaxWidth(),
MaxItems: maxLSPs,
ShowSection: true,
@@ -494,10 +494,10 @@ func (m *sidebarCmp) lspBlock() string {
func (m *sidebarCmp) mcpBlock() string {
// Limit the number of MCPs shown
_, _, maxMCPs := m.getDynamicLimits()
- mcps := config.Get().MCP.Sorted()
+ mcps := m.cfg.MCP.Sorted()
maxMCPs = min(len(mcps), maxMCPs)
- return mcp.RenderMCPBlock(mcp.RenderOptions{
+ return mcp.RenderMCPBlock(m.cfg, mcp.RenderOptions{
MaxWidth: m.getMaxWidth(),
MaxItems: maxMCPs,
ShowSection: true,
@@ -544,13 +544,15 @@ func formatTokensAndCost(tokens, contextWindow int64, cost float64) string {
}
func (s *sidebarCmp) currentModelBlock() string {
- cfg := config.Get()
- agentCfg := cfg.Agents["coder"]
+ agentCfg := s.cfg.Agents["coder"]
- selectedModel := cfg.Models[agentCfg.Model]
+ selectedModel := s.cfg.Models[agentCfg.Model]
- model := config.Get().GetModelByType(agentCfg.Model)
- modelProvider := config.Get().GetProviderForModel(agentCfg.Model)
+ model := s.cfg.GetModelByType(agentCfg.Model)
+ if model == nil {
+ return "No model found"
+ }
+ modelProvider := s.cfg.GetProviderForModel(agentCfg.Model)
t := styles.CurrentTheme()
@@ -606,8 +608,8 @@ func (m *sidebarCmp) SetCompactMode(compact bool) {
m.compactMode = compact
}
-func cwd() string {
- cwd := config.Get().WorkingDir()
+func cwd(cfg *config.Config) string {
+ cwd := cfg.WorkingDir()
t := styles.CurrentTheme()
return t.S().Muted.Render(home.Short(cwd))
}
@@ -9,6 +9,7 @@ import (
"github.com/charmbracelet/bubbles/v2/spinner"
tea "github.com/charmbracelet/bubbletea/v2"
"github.com/charmbracelet/catwalk/pkg/catwalk"
+ "github.com/charmbracelet/crush/internal/client"
"github.com/charmbracelet/crush/internal/config"
"github.com/charmbracelet/crush/internal/home"
"github.com/charmbracelet/crush/internal/llm/prompt"
@@ -72,9 +73,12 @@ type splashCmp struct {
selectedModel *models.ModelOption
isAPIKeyValid bool
apiKeyValue string
+
+ client *client.Client
+ cfg *config.Config
}
-func New() Splash {
+func New(c *client.Client, cfg *config.Config) Splash {
keyMap := DefaultKeyMap()
listKeyMap := list.DefaultKeyMap()
listKeyMap.Down.SetEnabled(false)
@@ -86,10 +90,12 @@ func New() Splash {
listKeyMap.DownOneItem = keyMap.Next
listKeyMap.UpOneItem = keyMap.Previous
- modelList := models.NewModelListComponent(listKeyMap, "Find your fave", false)
+ modelList := models.NewModelListComponent(cfg, listKeyMap, "Find your fave", false)
apiKeyInput := models.NewAPIKeyInput()
return &splashCmp{
+ client: c,
+ cfg: cfg,
width: 0,
height: 0,
keyMap: keyMap,
@@ -211,7 +217,7 @@ func (s *splashCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}),
func() tea.Msg {
start := time.Now()
- err := providerConfig.TestConnection(config.Get().Resolver())
+ err := providerConfig.TestConnection(s.cfg.Resolver())
// intentionally wait for at least 750ms to make sure the user sees the spinner
elapsed := time.Since(start)
if elapsed < 750*time.Millisecond {
@@ -305,8 +311,7 @@ func (s *splashCmp) saveAPIKeyAndContinue(apiKey string) tea.Cmd {
return nil
}
- cfg := config.Get()
- err := cfg.SetProviderAPIKey(string(s.selectedModel.Provider.ID), apiKey)
+ err := s.cfg.SetProviderAPIKey(string(s.selectedModel.Provider.ID), apiKey)
if err != nil {
return util.ReportError(fmt.Errorf("failed to save API key: %w", err))
}
@@ -324,7 +329,7 @@ func (s *splashCmp) saveAPIKeyAndContinue(apiKey string) tea.Cmd {
func (s *splashCmp) initializeProject() tea.Cmd {
s.needsProjectInit = false
- if err := config.MarkProjectInitialized(); err != nil {
+ if err := config.MarkProjectInitialized(s.cfg); err != nil {
return util.ReportError(err)
}
var cmds []tea.Cmd
@@ -342,8 +347,7 @@ func (s *splashCmp) initializeProject() tea.Cmd {
}
func (s *splashCmp) setPreferredModel(selectedItem models.ModelOption) tea.Cmd {
- cfg := config.Get()
- model := cfg.GetModel(string(selectedItem.Provider.ID), selectedItem.Model.ID)
+ model := s.cfg.GetModel(string(selectedItem.Provider.ID), selectedItem.Model.ID)
if model == nil {
return util.ReportError(fmt.Errorf("model %s not found for provider %s", selectedItem.Model.ID, selectedItem.Provider.ID))
}
@@ -355,7 +359,7 @@ func (s *splashCmp) setPreferredModel(selectedItem models.ModelOption) tea.Cmd {
MaxTokens: model.DefaultMaxTokens,
}
- err := cfg.UpdatePreferredModel(config.SelectedModelTypeLarge, selectedModel)
+ err := s.cfg.UpdatePreferredModel(config.SelectedModelTypeLarge, selectedModel)
if err != nil {
return util.ReportError(err)
}
@@ -367,16 +371,16 @@ func (s *splashCmp) setPreferredModel(selectedItem models.ModelOption) tea.Cmd {
}
if knownProvider == nil {
// for local provider we just use the same model
- err = cfg.UpdatePreferredModel(config.SelectedModelTypeSmall, selectedModel)
+ err = s.cfg.UpdatePreferredModel(config.SelectedModelTypeSmall, selectedModel)
if err != nil {
return util.ReportError(err)
}
} else {
smallModel := knownProvider.DefaultSmallModelID
- model := cfg.GetModel(string(selectedItem.Provider.ID), smallModel)
+ model := s.cfg.GetModel(string(selectedItem.Provider.ID), smallModel)
// should never happen
if model == nil {
- err = cfg.UpdatePreferredModel(config.SelectedModelTypeSmall, selectedModel)
+ err = s.cfg.UpdatePreferredModel(config.SelectedModelTypeSmall, selectedModel)
if err != nil {
return util.ReportError(err)
}
@@ -388,18 +392,17 @@ func (s *splashCmp) setPreferredModel(selectedItem models.ModelOption) tea.Cmd {
ReasoningEffort: model.DefaultReasoningEffort,
MaxTokens: model.DefaultMaxTokens,
}
- err = cfg.UpdatePreferredModel(config.SelectedModelTypeSmall, smallSelectedModel)
+ err = s.cfg.UpdatePreferredModel(config.SelectedModelTypeSmall, smallSelectedModel)
if err != nil {
return util.ReportError(err)
}
}
- cfg.SetupAgents()
+ s.cfg.SetupAgents()
return nil
}
func (s *splashCmp) getProvider(providerID catwalk.InferenceProvider) (*catwalk.Provider, error) {
- cfg := config.Get()
- providers, err := config.Providers(cfg)
+ providers, err := config.Providers(s.cfg)
if err != nil {
return nil, err
}
@@ -412,8 +415,7 @@ func (s *splashCmp) getProvider(providerID catwalk.InferenceProvider) (*catwalk.
}
func (s *splashCmp) isProviderConfigured(providerID string) bool {
- cfg := config.Get()
- if _, ok := cfg.Providers.Get(providerID); ok {
+ if _, ok := s.cfg.Providers.Get(providerID); ok {
return true
}
return false
@@ -650,11 +652,11 @@ func (s *splashCmp) cwdPart() string {
}
func (s *splashCmp) cwd() string {
- return home.Short(config.Get().WorkingDir())
+ return home.Short(s.cfg.WorkingDir())
}
-func LSPList(maxWidth int) []string {
- return lspcomponent.RenderLSPList(nil, lspcomponent.RenderOptions{
+func LSPList(c *client.Client, cfg *config.Config, maxWidth int) []string {
+ return lspcomponent.RenderLSPList(c, cfg, lspcomponent.RenderOptions{
MaxWidth: maxWidth,
ShowSection: false,
})
@@ -664,7 +666,7 @@ func (s *splashCmp) lspBlock() string {
t := styles.CurrentTheme()
maxWidth := s.getMaxInfoWidth() / 2
section := t.S().Subtle.Render("LSPs")
- lspList := append([]string{section, ""}, LSPList(maxWidth-1)...)
+ lspList := append([]string{section, ""}, LSPList(s.client, s.cfg, maxWidth-1)...)
return t.S().Base.Width(maxWidth).PaddingRight(1).Render(
lipgloss.JoinVertical(
lipgloss.Left,
@@ -673,8 +675,8 @@ func (s *splashCmp) lspBlock() string {
)
}
-func MCPList(maxWidth int) []string {
- return mcp.RenderMCPList(mcp.RenderOptions{
+func MCPList(cfg *config.Config, maxWidth int) []string {
+ return mcp.RenderMCPList(cfg, mcp.RenderOptions{
MaxWidth: maxWidth,
ShowSection: false,
})
@@ -684,7 +686,7 @@ func (s *splashCmp) mcpBlock() string {
t := styles.CurrentTheme()
maxWidth := s.getMaxInfoWidth() / 2
section := t.S().Subtle.Render("MCPs")
- mcpList := append([]string{section, ""}, MCPList(maxWidth-1)...)
+ mcpList := append([]string{section, ""}, MCPList(s.cfg, maxWidth-1)...)
return t.S().Base.Width(maxWidth).PaddingRight(1).Render(
lipgloss.JoinVertical(
lipgloss.Left,
@@ -694,9 +696,8 @@ func (s *splashCmp) mcpBlock() string {
}
func (s *splashCmp) currentModelBlock() string {
- cfg := config.Get()
- agentCfg := cfg.Agents["coder"]
- model := config.Get().GetModelByType(agentCfg.Model)
+ agentCfg := s.cfg.Agents["coder"]
+ model := s.cfg.GetModelByType(agentCfg.Model)
if model == nil {
return ""
}
@@ -57,6 +57,8 @@ type commandDialogCmp struct {
commandType int // SystemCommands or UserCommands
userCommands []Command // User-defined commands
sessionID string // Current session ID
+
+ cfg *config.Config
}
type (
@@ -76,7 +78,7 @@ type (
}
)
-func NewCommandDialog(sessionID string) CommandsDialog {
+func NewCommandDialog(cfg *config.Config, sessionID string) CommandsDialog {
keyMap := DefaultCommandsDialogKeyMap()
listKeyMap := list.DefaultKeyMap()
listKeyMap.Down.SetEnabled(false)
@@ -104,11 +106,12 @@ func NewCommandDialog(sessionID string) CommandsDialog {
help: help,
commandType: SystemCommands,
sessionID: sessionID,
+ cfg: cfg,
}
}
func (c *commandDialogCmp) Init() tea.Cmd {
- commands, err := LoadCustomCommands()
+ commands, err := LoadCustomCommands(c.cfg)
if err != nil {
return util.ReportError(err)
}
@@ -302,12 +305,11 @@ func (c *commandDialogCmp) defaultCommands() []Command {
}
// Add reasoning toggle for models that support it
- cfg := config.Get()
- if agentCfg, ok := cfg.Agents["coder"]; ok {
- providerCfg := cfg.GetProviderForModel(agentCfg.Model)
- model := cfg.GetModelByType(agentCfg.Model)
+ if agentCfg, ok := c.cfg.Agents["coder"]; ok {
+ providerCfg := c.cfg.GetProviderForModel(agentCfg.Model)
+ model := c.cfg.GetModelByType(agentCfg.Model)
if providerCfg != nil && model != nil && model.CanReason {
- selectedModel := cfg.Models[agentCfg.Model]
+ selectedModel := c.cfg.Models[agentCfg.Model]
// Anthropic models: thinking toggle
if providerCfg.Type == catwalk.TypeAnthropic {
@@ -350,8 +352,8 @@ func (c *commandDialogCmp) defaultCommands() []Command {
})
}
if c.sessionID != "" {
- agentCfg := config.Get().Agents["coder"]
- model := config.Get().GetModelByType(agentCfg.Model)
+ agentCfg := c.cfg.Agents["coder"]
+ model := c.cfg.GetModelByType(agentCfg.Model)
if model.SupportsImages {
commands = append(commands, Command{
ID: "file_picker",
@@ -30,8 +30,7 @@ type commandSource struct {
prefix string
}
-func LoadCustomCommands() ([]Command, error) {
- cfg := config.Get()
+func LoadCustomCommands(cfg *config.Config) ([]Command, error) {
if cfg == nil {
return nil, fmt.Errorf("config not loaded")
}
@@ -7,6 +7,7 @@ import (
tea "github.com/charmbracelet/bubbletea/v2"
"github.com/charmbracelet/lipgloss/v2"
+ "github.com/charmbracelet/crush/internal/client"
"github.com/charmbracelet/crush/internal/llm/agent"
"github.com/charmbracelet/crush/internal/tui/components/core"
"github.com/charmbracelet/crush/internal/tui/components/dialogs"
@@ -29,7 +30,7 @@ type compactDialogCmp struct {
sessionID string
state compactState
progress string
- agent agent.Service
+ client *client.Client
noAsk bool // If true, skip confirmation dialog
}
@@ -42,13 +43,13 @@ const (
)
// NewCompactDialogCmp creates a new session compact dialog
-func NewCompactDialogCmp(agent agent.Service, sessionID string, noAsk bool) CompactDialog {
+func NewCompactDialogCmp(c *client.Client, sessionID string, noAsk bool) CompactDialog {
return &compactDialogCmp{
sessionID: sessionID,
keyMap: DefaultKeyMap(),
state: stateConfirm,
selected: 0,
- agent: agent,
+ client: c,
noAsk: noAsk,
}
}
@@ -133,7 +134,7 @@ func (c *compactDialogCmp) startCompaction() tea.Cmd {
c.state = stateCompacting
c.progress = "Starting summarization..."
return func() tea.Msg {
- err := c.agent.Summarize(context.Background(), c.sessionID)
+ err := c.client.AgentSummarizeSession(context.Background(), c.sessionID)
if err != nil {
c.state = stateError
c.progress = "Error: " + err.Error()
@@ -19,9 +19,10 @@ type ModelListComponent struct {
list listModel
modelType int
providers []catwalk.Provider
+ cfg *config.Config
}
-func NewModelListComponent(keyMap list.KeyMap, inputPlaceholder string, shouldResize bool) *ModelListComponent {
+func NewModelListComponent(cfg *config.Config, keyMap list.KeyMap, inputPlaceholder string, shouldResize bool) *ModelListComponent {
t := styles.CurrentTheme()
inputStyle := t.S().Base.PaddingLeft(1).PaddingBottom(1)
options := []list.ListOption{
@@ -43,14 +44,14 @@ func NewModelListComponent(keyMap list.KeyMap, inputPlaceholder string, shouldRe
return &ModelListComponent{
list: modelList,
modelType: LargeModelType,
+ cfg: cfg,
}
}
func (m *ModelListComponent) Init() tea.Cmd {
var cmds []tea.Cmd
if len(m.providers) == 0 {
- cfg := config.Get()
- providers, err := config.Providers(cfg)
+ providers, err := config.Providers(m.cfg)
filteredProviders := []catwalk.Provider{}
for _, p := range providers {
hasAPIKeyEnv := strings.HasPrefix(p.APIKey, "$")
@@ -104,12 +105,11 @@ func (m *ModelListComponent) SetModelType(modelType int) tea.Cmd {
// first none section
selectedItemID := ""
- cfg := config.Get()
var currentModel config.SelectedModel
if m.modelType == LargeModelType {
- currentModel = cfg.Models[config.SelectedModelTypeLarge]
+ currentModel = m.cfg.Models[config.SelectedModelTypeLarge]
} else {
- currentModel = cfg.Models[config.SelectedModelTypeSmall]
+ currentModel = m.cfg.Models[config.SelectedModelTypeSmall]
}
configuredIcon := t.S().Base.Foreground(t.Success).Render(styles.CheckIcon)
@@ -120,11 +120,11 @@ func (m *ModelListComponent) SetModelType(modelType int) tea.Cmd {
// First, add any configured providers that are not in the known providers list
// These should appear at the top of the list
- knownProviders, err := config.Providers(cfg)
+ knownProviders, err := config.Providers(m.cfg)
if err != nil {
return util.ReportError(err)
}
- for providerID, providerConfig := range cfg.Providers.Seq2() {
+ for providerID, providerConfig := range m.cfg.Providers.Seq2() {
if providerConfig.Disable {
continue
}
@@ -196,7 +196,7 @@ func (m *ModelListComponent) SetModelType(modelType int) tea.Cmd {
}
// Check if this provider is configured and not disabled
- if providerConfig, exists := cfg.Providers.Get(string(provider.ID)); exists && providerConfig.Disable {
+ if providerConfig, exists := m.cfg.Providers.Get(string(provider.ID)); exists && providerConfig.Disable {
continue
}
@@ -206,7 +206,7 @@ func (m *ModelListComponent) SetModelType(modelType int) tea.Cmd {
}
section := list.NewItemSection(name)
- if _, ok := cfg.Providers.Get(string(provider.ID)); ok {
+ if _, ok := m.cfg.Providers.Get(string(provider.ID)); ok {
section.SetInfo(configured)
}
group := list.Group[list.CompletionItem[ModelOption]]{
@@ -67,9 +67,11 @@ type modelDialogCmp struct {
selectedModelType config.SelectedModelType
isAPIKeyValid bool
apiKeyValue string
+
+ cfg *config.Config
}
-func NewModelDialogCmp() ModelDialog {
+func NewModelDialogCmp(cfg *config.Config) ModelDialog {
keyMap := DefaultKeyMap()
listKeyMap := list.DefaultKeyMap()
@@ -79,7 +81,7 @@ func NewModelDialogCmp() ModelDialog {
listKeyMap.UpOneItem = keyMap.Previous
t := styles.CurrentTheme()
- modelList := NewModelListComponent(listKeyMap, largeModelInputPlaceholder, true)
+ modelList := NewModelListComponent(cfg, listKeyMap, largeModelInputPlaceholder, true)
apiKeyInput := NewAPIKeyInput()
apiKeyInput.SetShowTitle(false)
help := help.New()
@@ -91,6 +93,7 @@ func NewModelDialogCmp() ModelDialog {
width: defaultWidth,
keyMap: DefaultKeyMap(),
help: help,
+ cfg: cfg,
}
}
@@ -136,7 +139,7 @@ func (m *modelDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}),
func() tea.Msg {
start := time.Now()
- err := providerConfig.TestConnection(config.Get().Resolver())
+ err := providerConfig.TestConnection(m.cfg.Resolver())
// intentionally wait for at least 750ms to make sure the user sees the spinner
elapsed := time.Since(start)
if elapsed < 750*time.Millisecond {
@@ -344,16 +347,14 @@ func (m *modelDialogCmp) modelTypeRadio() string {
}
func (m *modelDialogCmp) isProviderConfigured(providerID string) bool {
- cfg := config.Get()
- if _, ok := cfg.Providers.Get(providerID); ok {
+ if _, ok := m.cfg.Providers.Get(providerID); ok {
return true
}
return false
}
func (m *modelDialogCmp) getProvider(providerID catwalk.InferenceProvider) (*catwalk.Provider, error) {
- cfg := config.Get()
- providers, err := config.Providers(cfg)
+ providers, err := config.Providers(m.cfg)
if err != nil {
return nil, err
}
@@ -370,8 +371,7 @@ func (m *modelDialogCmp) saveAPIKeyAndContinue(apiKey string) tea.Cmd {
return util.ReportError(fmt.Errorf("no model selected"))
}
- cfg := config.Get()
- err := cfg.SetProviderAPIKey(string(m.selectedModel.Provider.ID), apiKey)
+ err := m.cfg.SetProviderAPIKey(string(m.selectedModel.Provider.ID), apiKey)
if err != nil {
return util.ReportError(fmt.Errorf("failed to save API key: %w", err))
}
@@ -11,6 +11,7 @@ import (
"github.com/charmbracelet/crush/internal/fsext"
"github.com/charmbracelet/crush/internal/llm/tools"
"github.com/charmbracelet/crush/internal/permission"
+ "github.com/charmbracelet/crush/internal/proto"
"github.com/charmbracelet/crush/internal/tui/components/core"
"github.com/charmbracelet/crush/internal/tui/components/dialogs"
"github.com/charmbracelet/crush/internal/tui/styles"
@@ -19,13 +20,13 @@ import (
"github.com/charmbracelet/x/ansi"
)
-type PermissionAction string
+type PermissionAction = proto.PermissionAction
// Permission responses
const (
- PermissionAllow PermissionAction = "allow"
- PermissionAllowForSession PermissionAction = "allow_session"
- PermissionDeny PermissionAction = "deny"
+ PermissionAllow = proto.PermissionAllow
+ PermissionAllowForSession = proto.PermissionAllowForSession
+ PermissionDeny = proto.PermissionDeny
PermissionsDialogID dialogs.DialogID = "permissions"
)
@@ -39,6 +39,8 @@ type reasoningDialogCmp struct {
effortList listModel
keyMap ReasoningDialogKeyMap
help help.Model
+
+ cfg *config.Config
}
type ReasoningEffortSelectedMsg struct {
@@ -84,7 +86,7 @@ func (k ReasoningDialogKeyMap) FullHelp() [][]key.Binding {
}
}
-func NewReasoningDialog() ReasoningDialog {
+func NewReasoningDialog(cfg *config.Config) ReasoningDialog {
keyMap := DefaultReasoningDialogKeyMap()
listKeyMap := list.DefaultKeyMap()
listKeyMap.Down.SetEnabled(false)
@@ -111,6 +113,7 @@ func NewReasoningDialog() ReasoningDialog {
width: defaultWidth,
keyMap: keyMap,
help: help,
+ cfg: cfg,
}
}
@@ -119,10 +122,9 @@ func (r *reasoningDialogCmp) Init() tea.Cmd {
}
func (r *reasoningDialogCmp) populateEffortOptions() tea.Cmd {
- cfg := config.Get()
- if agentCfg, ok := cfg.Agents["coder"]; ok {
- selectedModel := cfg.Models[agentCfg.Model]
- model := cfg.GetModelByType(agentCfg.Model)
+ if agentCfg, ok := r.cfg.Agents["coder"]; ok {
+ selectedModel := r.cfg.Models[agentCfg.Model]
+ model := r.cfg.GetModelByType(agentCfg.Model)
// Get current reasoning effort
currentEffort := selectedModel.ReasoningEffort
@@ -40,7 +40,7 @@ type RenderOptions struct {
}
// RenderFileList renders a list of file status items with the given options.
-func RenderFileList(fileSlice []SessionFile, opts RenderOptions) []string {
+func RenderFileList(cfg *config.Config, fileSlice []SessionFile, opts RenderOptions) []string {
t := styles.CurrentTheme()
fileList := []string{}
@@ -90,7 +90,7 @@ func RenderFileList(fileSlice []SessionFile, opts RenderOptions) []string {
}
extraContent := strings.Join(statusParts, " ")
- cwd := config.Get().WorkingDir() + string(os.PathSeparator)
+ cwd := cfg.WorkingDir() + string(os.PathSeparator)
filePath := file.FilePath
if rel, err := filepath.Rel(cwd, filePath); err == nil {
filePath = rel
@@ -114,9 +114,9 @@ func RenderFileList(fileSlice []SessionFile, opts RenderOptions) []string {
}
// RenderFileBlock renders a complete file block with optional truncation indicator.
-func RenderFileBlock(fileSlice []SessionFile, opts RenderOptions, showTruncationIndicator bool) string {
+func RenderFileBlock(cfg *config.Config, fileSlice []SessionFile, opts RenderOptions, showTruncationIndicator bool) string {
t := styles.CurrentTheme()
- fileList := RenderFileList(fileSlice, opts)
+ fileList := RenderFileList(cfg, fileSlice, opts)
// Add truncation indicator if needed
if showTruncationIndicator && opts.MaxItems > 0 {
@@ -1,12 +1,13 @@
package lsp
import (
+ "context"
"fmt"
+ "log/slog"
"strings"
- "github.com/charmbracelet/crush/internal/app"
+ "github.com/charmbracelet/crush/internal/client"
"github.com/charmbracelet/crush/internal/config"
- "github.com/charmbracelet/crush/internal/csync"
"github.com/charmbracelet/crush/internal/lsp"
"github.com/charmbracelet/crush/internal/tui/components/core"
"github.com/charmbracelet/crush/internal/tui/styles"
@@ -23,7 +24,7 @@ type RenderOptions struct {
}
// RenderLSPList renders a list of LSP status items with the given options.
-func RenderLSPList(lspClients *csync.Map[string, *lsp.Client], opts RenderOptions) []string {
+func RenderLSPList(c *client.Client, cfg *config.Config, opts RenderOptions) []string {
t := styles.CurrentTheme()
lspList := []string{}
@@ -36,14 +37,18 @@ func RenderLSPList(lspClients *csync.Map[string, *lsp.Client], opts RenderOption
lspList = append(lspList, section, "")
}
- lspConfigs := config.Get().LSP.Sorted()
+ lspConfigs := cfg.LSP.Sorted()
if len(lspConfigs) == 0 {
lspList = append(lspList, t.S().Base.Foreground(t.Border).Render("None"))
return lspList
}
// Get LSP states
- lspStates := app.GetLSPStates()
+ lspStates, err := c.GetLSPs(context.TODO())
+ if err != nil {
+ slog.Error("failed to get lsp clients")
+ return nil
+ }
// Determine how many items to show
maxItems := len(lspConfigs)
@@ -85,15 +90,20 @@ func RenderLSPList(lspClients *csync.Map[string, *lsp.Client], opts RenderOption
// Calculate diagnostic counts if we have LSP clients
var extraContent string
- if lspClients != nil {
+ if c != nil {
lspErrs := map[protocol.DiagnosticSeverity]int{
protocol.SeverityError: 0,
protocol.SeverityWarning: 0,
protocol.SeverityHint: 0,
protocol.SeverityInformation: 0,
}
- if client, ok := lspClients.Get(l.Name); ok {
- for _, diagnostics := range client.GetDiagnostics() {
+ if _, ok := lspStates[l.Name]; ok {
+ diags, err := c.GetLSPDiagnostics(context.TODO(), l.Name)
+ if err != nil {
+ slog.Error("couldn't get lsp diagnostics", "lsp", l.Name)
+ return nil
+ }
+ for _, diagnostics := range diags {
for _, diagnostic := range diagnostics {
if severity, ok := lspErrs[diagnostic.Severity]; ok {
lspErrs[diagnostic.Severity] = severity + 1
@@ -135,13 +145,18 @@ func RenderLSPList(lspClients *csync.Map[string, *lsp.Client], opts RenderOption
}
// RenderLSPBlock renders a complete LSP block with optional truncation indicator.
-func RenderLSPBlock(lspClients *csync.Map[string, *lsp.Client], opts RenderOptions, showTruncationIndicator bool) string {
+func RenderLSPBlock(c *client.Client, cfg *config.Config, opts RenderOptions, showTruncationIndicator bool) string {
t := styles.CurrentTheme()
- lspList := RenderLSPList(lspClients, opts)
+ lspList := RenderLSPList(c, cfg, opts)
+ cfg, err := c.GetConfig(context.TODO())
+ if err != nil {
+ slog.Error("failed to get config for lsp block rendering", "error", err)
+ return ""
+ }
// Add truncation indicator if needed
if showTruncationIndicator && opts.MaxItems > 0 {
- lspConfigs := config.Get().LSP.Sorted()
+ lspConfigs := cfg.LSP.Sorted()
if len(lspConfigs) > opts.MaxItems {
remaining := len(lspConfigs) - opts.MaxItems
if remaining == 1 {
@@ -20,7 +20,7 @@ type RenderOptions struct {
}
// RenderMCPList renders a list of MCP status items with the given options.
-func RenderMCPList(opts RenderOptions) []string {
+func RenderMCPList(cfg *config.Config, opts RenderOptions) []string {
t := styles.CurrentTheme()
mcpList := []string{}
@@ -33,7 +33,7 @@ func RenderMCPList(opts RenderOptions) []string {
mcpList = append(mcpList, section, "")
}
- mcps := config.Get().MCP.Sorted()
+ mcps := cfg.MCP.Sorted()
if len(mcps) == 0 {
mcpList = append(mcpList, t.S().Base.Foreground(t.Border).Render("None"))
return mcpList
@@ -99,13 +99,13 @@ func RenderMCPList(opts RenderOptions) []string {
}
// RenderMCPBlock renders a complete MCP block with optional truncation indicator.
-func RenderMCPBlock(opts RenderOptions, showTruncationIndicator bool) string {
+func RenderMCPBlock(cfg *config.Config, opts RenderOptions, showTruncationIndicator bool) string {
t := styles.CurrentTheme()
- mcpList := RenderMCPList(opts)
+ mcpList := RenderMCPList(cfg, opts)
// Add truncation indicator if needed
if showTruncationIndicator && opts.MaxItems > 0 {
- mcps := config.Get().MCP.Sorted()
+ mcps := cfg.MCP.Sorted()
if len(mcps) > opts.MaxItems {
remaining := len(mcps) - opts.MaxItems
if remaining == 1 {
@@ -10,7 +10,7 @@ import (
"github.com/charmbracelet/bubbles/v2/spinner"
tea "github.com/charmbracelet/bubbletea/v2"
"github.com/charmbracelet/catwalk/pkg/catwalk"
- "github.com/charmbracelet/crush/internal/app"
+ "github.com/charmbracelet/crush/internal/client"
"github.com/charmbracelet/crush/internal/config"
"github.com/charmbracelet/crush/internal/history"
"github.com/charmbracelet/crush/internal/message"
@@ -90,7 +90,8 @@ func cancelTimerCmd() tea.Cmd {
type chatPage struct {
width, height int
detailsWidth, detailsHeight int
- app *app.App
+ app *client.Client
+ cfg *config.Config
keyboardEnhancements tea.KeyboardEnhancementsMsg
// Layout state
@@ -117,33 +118,33 @@ type chatPage struct {
isProjectInit bool
}
-func New(app *app.App) ChatPage {
+func New(app *client.Client, cfg *config.Config) ChatPage {
return &chatPage{
app: app,
+ cfg: cfg,
keyMap: DefaultKeyMap(),
- header: header.New(app.LSPClients),
- sidebar: sidebar.New(app.History, app.LSPClients, false),
- chat: chat.New(app),
+ header: header.New(app, cfg),
+ sidebar: sidebar.New(app, cfg, false),
+ chat: chat.New(app, cfg),
editor: editor.New(app),
- splash: splash.New(),
+ splash: splash.New(app, cfg),
focusedPane: PanelTypeSplash,
}
}
func (p *chatPage) Init() tea.Cmd {
- cfg := config.Get()
- compact := cfg.Options.TUI.CompactMode
+ compact := p.cfg.Options.TUI.CompactMode
p.compact = compact
p.forceCompact = compact
p.sidebar.SetCompactMode(p.compact)
// Set splash state based on config
- if !config.HasInitialDataConfig() {
+ if !config.HasInitialDataConfig(p.cfg) {
// First-time setup: show model selection
p.splash.SetOnboarding(true)
p.isOnboarding = true
p.splashFullScreen = true
- } else if b, _ := config.ProjectNeedsInitialization(); b {
+ } else if b, _ := config.ProjectNeedsInitialization(p.cfg); b {
// Project needs CRUSH.md initialization
p.splash.SetProjectInit(true)
p.isProjectInit = true
@@ -331,7 +332,11 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return p, tea.Batch(cmds...)
case commands.CommandRunCustomMsg:
- if p.app.CoderAgent.IsBusy() {
+ info, err := p.app.GetAgentInfo(context.TODO())
+ if err != nil {
+ return p, util.ReportError(fmt.Errorf("failed to get agent info: %w", err))
+ }
+ if info.IsBusy {
return p, util.ReportWarn("Agent is busy, please wait before executing a command...")
}
@@ -341,12 +346,12 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
case splash.OnboardingCompleteMsg:
p.splashFullScreen = false
- if b, _ := config.ProjectNeedsInitialization(); b {
+ if b, _ := config.ProjectNeedsInitialization(p.cfg); b {
p.splash.SetProjectInit(true)
p.splashFullScreen = true
return p, p.SetSize(p.width, p.height)
}
- err := p.app.InitCoderAgent()
+ err := p.app.InitiateAgentProcessing(context.TODO())
if err != nil {
return p, util.ReportError(err)
}
@@ -355,7 +360,11 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
p.focusedPane = PanelTypeEditor
return p, p.SetSize(p.width, p.height)
case commands.NewSessionsMsg:
- if p.app.CoderAgent.IsBusy() {
+ info, err := p.app.GetAgentInfo(context.TODO())
+ if err != nil {
+ return p, util.ReportError(fmt.Errorf("failed to get agent info: %w", err))
+ }
+ if info.IsBusy {
return p, util.ReportWarn("Agent is busy, please wait before starting a new session...")
}
return p, p.newSession()
@@ -363,16 +372,17 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch {
case key.Matches(msg, p.keyMap.NewSession):
// if we have no agent do nothing
- if p.app.CoderAgent == nil {
+ info, err := p.app.GetAgentInfo(context.TODO())
+ if err != nil || info.IsZero() {
return p, nil
}
- if p.app.CoderAgent.IsBusy() {
+ if info.IsBusy {
return p, util.ReportWarn("Agent is busy, please wait before starting a new session...")
}
return p, p.newSession()
case key.Matches(msg, p.keyMap.AddAttachment):
- agentCfg := config.Get().Agents["coder"]
- model := config.Get().GetModelByType(agentCfg.Model)
+ agentCfg := p.cfg.Agents["coder"]
+ model := p.cfg.GetModelByType(agentCfg.Model)
if model.SupportsImages {
return p, util.CmdHandler(commands.OpenFilePickerMsg{})
} else {
@@ -387,7 +397,11 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
p.changeFocus()
return p, nil
case key.Matches(msg, p.keyMap.Cancel):
- if p.session.ID != "" && p.app.CoderAgent.IsBusy() {
+ info, err := p.app.GetAgentInfo(context.TODO())
+ if err != nil {
+ return p, util.ReportError(fmt.Errorf("failed to get agent info: %w", err))
+ }
+ if p.session.ID != "" && info.IsBusy {
return p, p.cancel()
}
case key.Matches(msg, p.keyMap.Details):
@@ -516,7 +530,7 @@ func (p *chatPage) View() string {
func (p *chatPage) updateCompactConfig(compact bool) tea.Cmd {
return func() tea.Msg {
- err := config.Get().SetCompactMode(compact)
+ err := p.cfg.SetCompactMode(compact)
if err != nil {
return util.InfoMsg{
Type: util.InfoTypeError,
@@ -529,16 +543,15 @@ func (p *chatPage) updateCompactConfig(compact bool) tea.Cmd {
func (p *chatPage) toggleThinking() tea.Cmd {
return func() tea.Msg {
- cfg := config.Get()
- agentCfg := cfg.Agents["coder"]
- currentModel := cfg.Models[agentCfg.Model]
+ agentCfg := p.cfg.Agents["coder"]
+ currentModel := p.cfg.Models[agentCfg.Model]
// Toggle the thinking mode
currentModel.Think = !currentModel.Think
- cfg.Models[agentCfg.Model] = currentModel
+ p.cfg.Models[agentCfg.Model] = currentModel
// Update the agent with the new configuration
- if err := p.app.UpdateAgentModel(); err != nil {
+ if err := p.app.UpdateAgent(context.TODO()); err != nil {
return util.InfoMsg{
Type: util.InfoTypeError,
Msg: "Failed to update thinking mode: " + err.Error(),
@@ -558,16 +571,15 @@ func (p *chatPage) toggleThinking() tea.Cmd {
func (p *chatPage) openReasoningDialog() tea.Cmd {
return func() tea.Msg {
- cfg := config.Get()
- agentCfg := cfg.Agents["coder"]
- model := cfg.GetModelByType(agentCfg.Model)
- providerCfg := cfg.GetProviderForModel(agentCfg.Model)
+ agentCfg := p.cfg.Agents["coder"]
+ model := p.cfg.GetModelByType(agentCfg.Model)
+ providerCfg := p.cfg.GetProviderForModel(agentCfg.Model)
if providerCfg != nil && model != nil &&
providerCfg.Type == catwalk.TypeOpenAI && model.HasReasoningEffort {
// Return the OpenDialogMsg directly so it bubbles up to the main TUI
return dialogs.OpenDialogMsg{
- Model: reasoning.NewReasoningDialog(),
+ Model: reasoning.NewReasoningDialog(p.cfg),
}
}
return nil
@@ -576,7 +588,7 @@ func (p *chatPage) openReasoningDialog() tea.Cmd {
func (p *chatPage) handleReasoningEffortSelected(effort string) tea.Cmd {
return func() tea.Msg {
- cfg := config.Get()
+ cfg := p.cfg
agentCfg := cfg.Agents["coder"]
currentModel := cfg.Models[agentCfg.Model]
@@ -585,7 +597,7 @@ func (p *chatPage) handleReasoningEffortSelected(effort string) tea.Cmd {
cfg.Models[agentCfg.Model] = currentModel
// Update the agent with the new configuration
- if err := p.app.UpdateAgentModel(); err != nil {
+ if err := p.app.UpdateAgent(context.TODO()); err != nil {
return util.InfoMsg{
Type: util.InfoTypeError,
Msg: "Failed to update reasoning effort: " + err.Error(),
@@ -706,14 +718,13 @@ func (p *chatPage) changeFocus() {
func (p *chatPage) cancel() tea.Cmd {
if p.isCanceling {
p.isCanceling = false
- if p.app.CoderAgent != nil {
- p.app.CoderAgent.Cancel(p.session.ID)
- }
+ _ = p.app.ClearAgentSessionQueuedPrompts(context.TODO(), p.session.ID)
return nil
}
- if p.app.CoderAgent != nil && p.app.CoderAgent.QueuedPrompts(p.session.ID) > 0 {
- p.app.CoderAgent.ClearQueue(p.session.ID)
+ queued, _ := p.app.GetAgentSessionQueuedPrompts(context.TODO(), p.session.ID)
+ if queued > 0 {
+ _ = p.app.ClearAgentSessionQueuedPrompts(context.TODO(), p.session.ID)
return nil
}
p.isCanceling = true
@@ -739,18 +750,18 @@ func (p *chatPage) sendMessage(text string, attachments []message.Attachment) te
session := p.session
var cmds []tea.Cmd
if p.session.ID == "" {
- newSession, err := p.app.Sessions.Create(context.Background(), "New Session")
+ newSession, err := p.app.CreateSession(context.Background(), "New Session")
if err != nil {
return util.ReportError(err)
}
- session = newSession
+ session = *newSession
cmds = append(cmds, util.CmdHandler(chat.SessionSelectedMsg(session)))
}
- if p.app.CoderAgent == nil {
+ info, err := p.app.GetAgentInfo(context.TODO())
+ if err != nil || info.IsZero() {
return util.ReportError(fmt.Errorf("coder agent is not initialized"))
}
- _, err := p.app.CoderAgent.Run(context.Background(), session.ID, text, attachments...)
- if err != nil {
+ if err := p.app.SendMessage(context.Background(), session.ID, text, attachments...); err != nil {
return util.ReportError(err)
}
cmds = append(cmds, p.chat.GoToBottom())
@@ -762,7 +773,8 @@ func (p *chatPage) Bindings() []key.Binding {
p.keyMap.NewSession,
p.keyMap.AddAttachment,
}
- if p.app.CoderAgent != nil && p.app.CoderAgent.IsBusy() {
+ info, err := p.app.GetAgentInfo(context.TODO())
+ if err == nil && info.IsBusy {
cancelBinding := p.keyMap.Cancel
if p.isCanceling {
cancelBinding = key.NewBinding(
@@ -883,7 +895,8 @@ func (p *chatPage) Help() help.KeyMap {
}
return core.NewSimpleHelp(shortList, fullList)
}
- if p.app.CoderAgent != nil && p.app.CoderAgent.IsBusy() {
+ info, err := p.app.GetAgentInfo(context.TODO())
+ if err == nil && info.IsBusy {
cancelBinding := key.NewBinding(
key.WithKeys("esc"),
key.WithHelp("esc", "cancel"),
@@ -894,7 +907,8 @@ func (p *chatPage) Help() help.KeyMap {
key.WithHelp("esc", "press again to cancel"),
)
}
- if p.app.CoderAgent != nil && p.app.CoderAgent.QueuedPrompts(p.session.ID) > 0 {
+ queued, _ := p.app.GetAgentSessionQueuedPrompts(context.TODO(), p.session.ID)
+ if queued > 0 {
cancelBinding = key.NewBinding(
key.WithKeys("esc"),
key.WithHelp("esc", "clear queue"),
@@ -3,15 +3,18 @@ package tui
import (
"context"
"fmt"
+ "path/filepath"
"strings"
"time"
"github.com/charmbracelet/bubbles/v2/key"
tea "github.com/charmbracelet/bubbletea/v2"
- "github.com/charmbracelet/crush/internal/app"
+ "github.com/charmbracelet/crush/internal/client"
"github.com/charmbracelet/crush/internal/config"
"github.com/charmbracelet/crush/internal/llm/agent"
+ "github.com/charmbracelet/crush/internal/log"
"github.com/charmbracelet/crush/internal/permission"
+ "github.com/charmbracelet/crush/internal/proto"
"github.com/charmbracelet/crush/internal/pubsub"
cmpChat "github.com/charmbracelet/crush/internal/tui/components/chat"
"github.com/charmbracelet/crush/internal/tui/components/chat/splash"
@@ -64,7 +67,8 @@ type appModel struct {
status status.StatusCmp
showingFullHelp bool
- app *app.App
+ app *client.Client
+ cfg *config.Config
dialog dialogs.DialogCmp
completions completions.Completions
@@ -98,7 +102,7 @@ func (a appModel) Init() tea.Cmd {
func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmds []tea.Cmd
var cmd tea.Cmd
- a.isConfigured = config.HasInitialDataConfig()
+ a.isConfigured = config.HasInitialDataConfig(a.cfg)
switch msg := msg.(type) {
case tea.KeyboardEnhancementsMsg:
@@ -164,7 +168,7 @@ func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
// Commands
case commands.SwitchSessionsMsg:
return a, func() tea.Msg {
- allSessions, _ := a.app.Sessions.List(context.Background())
+ allSessions, _ := a.app.ListSessions(context.Background())
return dialogs.OpenDialogMsg{
Model: sessions.NewSessionDialogCmp(allSessions, a.selectedSessionID),
}
@@ -173,33 +177,43 @@ func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case commands.SwitchModelMsg:
return a, util.CmdHandler(
dialogs.OpenDialogMsg{
- Model: models.NewModelDialogCmp(),
+ Model: models.NewModelDialogCmp(a.cfg),
},
)
// Compact
case commands.CompactMsg:
return a, util.CmdHandler(dialogs.OpenDialogMsg{
- Model: compact.NewCompactDialogCmp(a.app.CoderAgent, msg.SessionID, true),
+ Model: compact.NewCompactDialogCmp(a.app, msg.SessionID, true),
})
case commands.QuitMsg:
return a, util.CmdHandler(dialogs.OpenDialogMsg{
Model: quit.NewQuitDialog(),
})
case commands.ToggleYoloModeMsg:
- a.app.Permissions.SetSkipRequests(!a.app.Permissions.SkipRequests())
+ skip, err := a.app.GetPermissionsSkipRequests(context.TODO())
+ if err != nil {
+ return a, util.ReportError(fmt.Errorf("failed to get permissions skip requests: %v", err))
+ }
+ if err := a.app.SetPermissionsSkipRequests(context.TODO(), !skip); err != nil {
+ return a, util.ReportError(fmt.Errorf("failed to toggle YOLO mode: %v", err))
+ }
case commands.ToggleHelpMsg:
a.status.ToggleFullHelp()
a.showingFullHelp = !a.showingFullHelp
return a, a.handleWindowResize(a.wWidth, a.wHeight)
// Model Switch
case models.ModelSelectedMsg:
- if a.app.CoderAgent.IsBusy() {
+ info, err := a.app.GetAgentInfo(context.TODO())
+ if err != nil {
+ return a, util.ReportError(fmt.Errorf("failed to check if agent is busy: %v", err))
+ }
+ if info.IsBusy {
return a, util.ReportWarn("Agent is busy, please wait...")
}
- config.Get().UpdatePreferredModel(msg.ModelType, msg.Model)
+ a.cfg.UpdatePreferredModel(msg.ModelType, msg.Model)
// Update the agent with the new model/provider configuration
- if err := a.app.UpdateAgentModel(); err != nil {
+ if err := a.app.UpdateAgent(context.TODO()); err != nil {
return a, util.ReportError(fmt.Errorf("model changed to %s but failed to update agent: %v", msg.Model.Model, err))
}
@@ -216,7 +230,7 @@ func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return a, util.CmdHandler(dialogs.CloseDialogMsg{})
}
return a, util.CmdHandler(dialogs.OpenDialogMsg{
- Model: filepicker.NewFilePickerCmp(a.app.Config().WorkingDir()),
+ Model: filepicker.NewFilePickerCmp(a.cfg.WorkingDir()),
})
// Permissions
case pubsub.Event[permission.PermissionNotification]:
@@ -235,23 +249,22 @@ func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case pubsub.Event[permission.PermissionRequest]:
return a, util.CmdHandler(dialogs.OpenDialogMsg{
Model: permissions.NewPermissionDialogCmp(msg.Payload, &permissions.Options{
- DiffMode: config.Get().Options.TUI.DiffMode,
+ DiffMode: a.cfg.Options.TUI.DiffMode,
}),
})
case permissions.PermissionResponseMsg:
- switch msg.Action {
- case permissions.PermissionAllow:
- a.app.Permissions.Grant(msg.Permission)
- case permissions.PermissionAllowForSession:
- a.app.Permissions.GrantPersistent(msg.Permission)
- case permissions.PermissionDeny:
- a.app.Permissions.Deny(msg.Permission)
+ if err := a.app.GrantPermission(context.TODO(), proto.PermissionGrant(msg)); err != nil {
+ return a, util.ReportError(fmt.Errorf("failed to grant permission: %v", err))
}
return a, nil
// Agent Events
case pubsub.Event[agent.AgentEvent]:
payload := msg.Payload
+ if payload.Type == agent.AgentEventTypeError {
+ return a, util.ReportError(fmt.Errorf("agent error: %v", payload.Error))
+ }
+
// Forward agent events to dialogs
if a.dialog.HasDialogs() && a.dialog.ActiveDialogID() == compact.CompactDialogID {
u, dialogCmd := a.dialog.Update(payload)
@@ -265,14 +278,18 @@ func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
// Handle auto-compact logic
if payload.Done && payload.Type == agent.AgentEventTypeResponse && a.selectedSessionID != "" {
// Get current session to check token usage
- session, err := a.app.Sessions.Get(context.Background(), a.selectedSessionID)
+ session, err := a.app.GetSession(context.Background(), a.selectedSessionID)
if err == nil {
- model := a.app.CoderAgent.Model()
+ info, err := a.app.GetAgentInfo(context.Background())
+ if err != nil {
+ return a, util.ReportError(fmt.Errorf("failed to check if agent is busy: %v", err))
+ }
+ model := info.Model
contextWindow := model.ContextWindow
tokens := session.CompletionTokens + session.PromptTokens
- if (tokens >= int64(float64(contextWindow)*0.95)) && !config.Get().Options.DisableAutoSummarize { // Show compact confirmation dialog
+ if (tokens >= int64(float64(contextWindow)*0.95)) && !a.cfg.Options.DisableAutoSummarize { // Show compact confirmation dialog
cmds = append(cmds, util.CmdHandler(dialogs.OpenDialogMsg{
- Model: compact.NewCompactDialogCmp(a.app.CoderAgent, a.selectedSessionID, false),
+ Model: compact.NewCompactDialogCmp(a.app, a.selectedSessionID, false),
}))
}
}
@@ -285,7 +302,7 @@ func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return a, nil
}
- a.isConfigured = config.HasInitialDataConfig()
+ a.isConfigured = config.HasInitialDataConfig(a.cfg)
updated, pageCmd := item.Update(msg)
if model, ok := updated.(util.Model); ok {
a.pages[a.currentPage] = model
@@ -452,7 +469,7 @@ func (a *appModel) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd {
return nil
}
return util.CmdHandler(dialogs.OpenDialogMsg{
- Model: commands.NewCommandDialog(a.selectedSessionID),
+ Model: commands.NewCommandDialog(a.cfg, a.selectedSessionID),
})
case key.Matches(msg, a.keyMap.Sessions):
// if the app is not configured show no sessions
@@ -472,7 +489,7 @@ func (a *appModel) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd {
}
cmds = append(cmds,
func() tea.Msg {
- allSessions, _ := a.app.Sessions.List(context.Background())
+ allSessions, _ := a.app.ListSessions(context.Background())
return dialogs.OpenDialogMsg{
Model: sessions.NewSessionDialogCmp(allSessions, a.selectedSessionID),
}
@@ -480,7 +497,8 @@ func (a *appModel) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd {
)
return tea.Sequence(cmds...)
case key.Matches(msg, a.keyMap.Suspend):
- if a.app.CoderAgent != nil && a.app.CoderAgent.IsBusy() {
+ info, err := a.app.GetAgentInfo(context.TODO())
+ if err != nil || info.IsBusy {
return util.ReportWarn("Agent is busy, please wait...")
}
return tea.Suspend
@@ -500,7 +518,11 @@ func (a *appModel) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd {
// moveToPage handles navigation between different pages in the application.
func (a *appModel) moveToPage(pageID page.PageID) tea.Cmd {
- if a.app.CoderAgent.IsBusy() {
+ info, err := a.app.GetAgentInfo(context.TODO())
+ if err != nil {
+ return util.ReportError(fmt.Errorf("failed to check if agent is busy: %v", err))
+ }
+ if info.IsBusy {
// TODO: maybe remove this : For now we don't move to any page if the agent is busy
return util.ReportWarn("Agent is busy, please wait...")
}
@@ -601,14 +623,25 @@ func (a *appModel) View() tea.View {
}
// New creates and initializes a new TUI application model.
-func New(app *app.App) tea.Model {
- chatPage := chat.New(app)
+func New(app *client.Client) (tea.Model, error) {
+ cfg, err := app.GetConfig(context.TODO())
+ if err != nil {
+ return nil, fmt.Errorf("failed to get config: %v", err)
+ }
+
+ // Setup logs
+ log.Setup(
+ filepath.Join(cfg.Options.DataDirectory, "logs", "tui.log"),
+ cfg.Options.Debug,
+ )
+
+ chatPage := chat.New(app, cfg)
keyMap := DefaultKeyMap()
keyMap.pageBindings = chatPage.Bindings()
-
model := &appModel{
currentPage: chat.ChatPageID,
app: app,
+ cfg: cfg,
status: status.NewStatusCmp(),
loadedPages: make(map[page.PageID]bool),
keyMap: keyMap,
@@ -621,5 +654,5 @@ func New(app *app.App) tea.Model {
completions: completions.New(),
}
- return model
+ return model, nil
}