diff --git a/internal/client/client.go b/internal/client/client.go index d4577eb836ae4991fb47a9ef37566c71c0d9ecd4..b7f37fd29c7b4f5955d70ce7b54cf8a90197c34f 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -23,7 +23,6 @@ const DummyHost = "api.crush.localhost" // Client represents an RPC client connected to a Crush server. type Client struct { h *http.Client - id string path string network string addr string @@ -62,16 +61,6 @@ func NewClient(path, network, address string) (*Client, error) { return c, nil } -// ID returns the client's instance unique identifier. -func (c *Client) ID() string { - return c.id -} - -// SetID sets the client's instance unique identifier. -func (c *Client) SetID(id string) { - c.id = id -} - // Path returns the client's instance filesystem path. func (c *Client) Path() string { return c.path diff --git a/internal/client/proto.go b/internal/client/proto.go index c048ecbfbf1e3575e1cdeb71dc952a7425d95582..7c9b77ef5dba6d5fa834d6f2bfce2431f88e74d7 100644 --- a/internal/client/proto.go +++ b/internal/client/proto.go @@ -22,9 +22,9 @@ import ( "github.com/charmbracelet/x/powernap/pkg/lsp/protocol" ) -func (c *Client) SubscribeEvents(ctx context.Context) (<-chan any, error) { +func (c *Client) SubscribeEvents(ctx context.Context, id string) (<-chan any, error) { events := make(chan any, 100) - rsp, err := c.get(ctx, fmt.Sprintf("/instances/%s/events", c.id), nil, http.Header{ + rsp, err := c.get(ctx, fmt.Sprintf("/instances/%s/events", id), nil, http.Header{ "Accept": []string{"text/event-stream"}, "Cache-Control": []string{"no-cache"}, "Connection": []string{"keep-alive"}, @@ -143,8 +143,8 @@ func sendEvent(ctx context.Context, evc chan any, ev any) { } } -func (c *Client) GetLSPDiagnostics(ctx context.Context, lsp string) (map[protocol.DocumentURI][]protocol.Diagnostic, error) { - rsp, err := c.get(ctx, fmt.Sprintf("/instances/%s/lsps/%s/diagnostics", c.id, lsp), nil, nil) +func (c *Client) GetLSPDiagnostics(ctx context.Context, id string, lsp string) (map[protocol.DocumentURI][]protocol.Diagnostic, error) { + rsp, err := c.get(ctx, fmt.Sprintf("/instances/%s/lsps/%s/diagnostics", id, lsp), nil, nil) if err != nil { return nil, fmt.Errorf("failed to get LSP diagnostics: %w", err) } @@ -159,8 +159,8 @@ func (c *Client) GetLSPDiagnostics(ctx context.Context, lsp string) (map[protoco return diagnostics, nil } -func (c *Client) GetLSPs(ctx context.Context) (map[string]app.LSPClientInfo, error) { - rsp, err := c.get(ctx, fmt.Sprintf("/instances/%s/lsps", c.id), nil, nil) +func (c *Client) GetLSPs(ctx context.Context, id string) (map[string]app.LSPClientInfo, error) { + rsp, err := c.get(ctx, fmt.Sprintf("/instances/%s/lsps", id), nil, nil) if err != nil { return nil, fmt.Errorf("failed to get LSPs: %w", err) } @@ -175,8 +175,8 @@ func (c *Client) GetLSPs(ctx context.Context) (map[string]app.LSPClientInfo, err return lsps, nil } -func (c *Client) GetAgentSessionQueuedPrompts(ctx context.Context, sessionID string) (int, error) { - rsp, err := c.get(ctx, fmt.Sprintf("/instances/%s/agent/sessions/%s/prompts/queued", c.id, sessionID), nil, nil) +func (c *Client) GetAgentSessionQueuedPrompts(ctx context.Context, id string, sessionID string) (int, error) { + rsp, err := c.get(ctx, fmt.Sprintf("/instances/%s/agent/sessions/%s/prompts/queued", id, sessionID), nil, nil) if err != nil { return 0, fmt.Errorf("failed to get session agent queued prompts: %w", err) } @@ -191,8 +191,8 @@ func (c *Client) GetAgentSessionQueuedPrompts(ctx context.Context, sessionID str return count, nil } -func (c *Client) ClearAgentSessionQueuedPrompts(ctx context.Context, sessionID string) error { - rsp, err := c.post(ctx, fmt.Sprintf("/instances/%s/agent/sessions/%s/prompts/clear", c.id, sessionID), nil, nil, nil) +func (c *Client) ClearAgentSessionQueuedPrompts(ctx context.Context, id string, sessionID string) error { + rsp, err := c.post(ctx, fmt.Sprintf("/instances/%s/agent/sessions/%s/prompts/clear", id, sessionID), nil, nil, nil) if err != nil { return fmt.Errorf("failed to clear session agent queued prompts: %w", err) } @@ -203,8 +203,8 @@ func (c *Client) ClearAgentSessionQueuedPrompts(ctx context.Context, sessionID s return nil } -func (c *Client) GetAgentInfo(ctx context.Context) (*proto.AgentInfo, error) { - rsp, err := c.get(ctx, fmt.Sprintf("/instances/%s/agent", c.id), nil, nil) +func (c *Client) GetAgentInfo(ctx context.Context, id string) (*proto.AgentInfo, error) { + rsp, err := c.get(ctx, fmt.Sprintf("/instances/%s/agent", id), nil, nil) if err != nil { return nil, fmt.Errorf("failed to get agent status: %w", err) } @@ -219,8 +219,8 @@ func (c *Client) GetAgentInfo(ctx context.Context) (*proto.AgentInfo, error) { return &info, nil } -func (c *Client) UpdateAgent(ctx context.Context) error { - rsp, err := c.post(ctx, fmt.Sprintf("/instances/%s/agent/update", c.id), nil, nil, nil) +func (c *Client) UpdateAgent(ctx context.Context, id string) error { + rsp, err := c.post(ctx, fmt.Sprintf("/instances/%s/agent/update", id), nil, nil, nil) if err != nil { return fmt.Errorf("failed to update agent: %w", err) } @@ -231,8 +231,8 @@ func (c *Client) UpdateAgent(ctx context.Context) error { return nil } -func (c *Client) SendMessage(ctx context.Context, sessionID, message string, attchments ...message.Attachment) error { - rsp, err := c.post(ctx, fmt.Sprintf("/instances/%s/agent", c.id), nil, jsonBody(proto.AgentMessage{ +func (c *Client) SendMessage(ctx context.Context, id string, sessionID, message string, attchments ...message.Attachment) error { + rsp, err := c.post(ctx, fmt.Sprintf("/instances/%s/agent", id), nil, jsonBody(proto.AgentMessage{ SessionID: sessionID, Prompt: message, Attachments: attchments, @@ -247,8 +247,8 @@ func (c *Client) SendMessage(ctx context.Context, sessionID, message string, att return nil } -func (c *Client) GetAgentSessionInfo(ctx context.Context, sessionID string) (*proto.AgentSession, error) { - rsp, err := c.get(ctx, fmt.Sprintf("/instances/%s/agent/sessions/%s", c.id, sessionID), nil, nil) +func (c *Client) GetAgentSessionInfo(ctx context.Context, id string, sessionID string) (*proto.AgentSession, error) { + rsp, err := c.get(ctx, fmt.Sprintf("/instances/%s/agent/sessions/%s", id, sessionID), nil, nil) if err != nil { return nil, fmt.Errorf("failed to get session agent info: %w", err) } @@ -263,8 +263,8 @@ func (c *Client) GetAgentSessionInfo(ctx context.Context, sessionID string) (*pr return &info, nil } -func (c *Client) AgentSummarizeSession(ctx context.Context, sessionID string) error { - rsp, err := c.post(ctx, fmt.Sprintf("/instances/%s/agent/sessions/%s/summarize", c.id, sessionID), nil, nil, nil) +func (c *Client) AgentSummarizeSession(ctx context.Context, id string, sessionID string) error { + rsp, err := c.post(ctx, fmt.Sprintf("/instances/%s/agent/sessions/%s/summarize", id, sessionID), nil, nil, nil) if err != nil { return fmt.Errorf("failed to summarize session: %w", err) } @@ -275,8 +275,8 @@ func (c *Client) AgentSummarizeSession(ctx context.Context, sessionID string) er return nil } -func (c *Client) ListMessages(ctx context.Context, sessionID string) ([]message.Message, error) { - rsp, err := c.get(ctx, fmt.Sprintf("/instances/%s/sessions/%s/messages", c.id, sessionID), nil, nil) +func (c *Client) ListMessages(ctx context.Context, id string, sessionID string) ([]message.Message, error) { + rsp, err := c.get(ctx, fmt.Sprintf("/instances/%s/sessions/%s/messages", id, sessionID), nil, nil) if err != nil { return nil, fmt.Errorf("failed to get messages: %w", err) } @@ -291,8 +291,8 @@ func (c *Client) ListMessages(ctx context.Context, sessionID string) ([]message. return messages, nil } -func (c *Client) GetSession(ctx context.Context, sessionID string) (*session.Session, error) { - rsp, err := c.get(ctx, fmt.Sprintf("/instances/%s/sessions/%s", c.id, sessionID), nil, nil) +func (c *Client) GetSession(ctx context.Context, id string, sessionID string) (*session.Session, error) { + rsp, err := c.get(ctx, fmt.Sprintf("/instances/%s/sessions/%s", id, sessionID), nil, nil) if err != nil { return nil, fmt.Errorf("failed to get session: %w", err) } @@ -307,8 +307,8 @@ func (c *Client) GetSession(ctx context.Context, sessionID string) (*session.Ses return &sess, nil } -func (c *Client) InitiateAgentProcessing(ctx context.Context) error { - rsp, err := c.post(ctx, fmt.Sprintf("/instances/%s/agent/init", c.id), nil, nil, nil) +func (c *Client) InitiateAgentProcessing(ctx context.Context, id string) error { + rsp, err := c.post(ctx, fmt.Sprintf("/instances/%s/agent/init", id), nil, nil, nil) if err != nil { return fmt.Errorf("failed to initiate session agent processing: %w", err) } @@ -319,8 +319,8 @@ func (c *Client) InitiateAgentProcessing(ctx context.Context) error { return nil } -func (c *Client) ListSessionHistoryFiles(ctx context.Context, sessionID string) ([]history.File, error) { - rsp, err := c.get(ctx, fmt.Sprintf("/instances/%s/sessions/%s/history", c.id, sessionID), nil, nil) +func (c *Client) ListSessionHistoryFiles(ctx context.Context, id string, sessionID string) ([]history.File, error) { + rsp, err := c.get(ctx, fmt.Sprintf("/instances/%s/sessions/%s/history", id, sessionID), nil, nil) if err != nil { return nil, fmt.Errorf("failed to get session history files: %w", err) } @@ -335,8 +335,8 @@ func (c *Client) ListSessionHistoryFiles(ctx context.Context, sessionID string) return files, nil } -func (c *Client) CreateSession(ctx context.Context, title string) (*session.Session, error) { - rsp, err := c.post(ctx, fmt.Sprintf("/instances/%s/sessions", c.id), nil, jsonBody(session.Session{Title: title}), http.Header{"Content-Type": []string{"application/json"}}) +func (c *Client) CreateSession(ctx context.Context, id string, title string) (*session.Session, error) { + rsp, err := c.post(ctx, fmt.Sprintf("/instances/%s/sessions", id), nil, jsonBody(session.Session{Title: title}), http.Header{"Content-Type": []string{"application/json"}}) if err != nil { return nil, fmt.Errorf("failed to create session: %w", err) } @@ -351,8 +351,8 @@ func (c *Client) CreateSession(ctx context.Context, title string) (*session.Sess return &sess, nil } -func (c *Client) ListSessions(ctx context.Context) ([]session.Session, error) { - rsp, err := c.get(ctx, fmt.Sprintf("/instances/%s/sessions", c.id), nil, nil) +func (c *Client) ListSessions(ctx context.Context, id string) ([]session.Session, error) { + rsp, err := c.get(ctx, fmt.Sprintf("/instances/%s/sessions", id), nil, nil) if err != nil { return nil, fmt.Errorf("failed to get sessions: %w", err) } @@ -367,8 +367,8 @@ func (c *Client) ListSessions(ctx context.Context) ([]session.Session, error) { return sessions, nil } -func (c *Client) GrantPermission(ctx context.Context, req proto.PermissionGrant) error { - rsp, err := c.post(ctx, fmt.Sprintf("/instances/%s/permissions/grant", c.id), nil, jsonBody(req), http.Header{"Content-Type": []string{"application/json"}}) +func (c *Client) GrantPermission(ctx context.Context, id string, req proto.PermissionGrant) error { + rsp, err := c.post(ctx, fmt.Sprintf("/instances/%s/permissions/grant", id), nil, jsonBody(req), http.Header{"Content-Type": []string{"application/json"}}) if err != nil { return fmt.Errorf("failed to grant permission: %w", err) } @@ -379,8 +379,8 @@ func (c *Client) GrantPermission(ctx context.Context, req proto.PermissionGrant) return nil } -func (c *Client) SetPermissionsSkipRequests(ctx context.Context, skip bool) error { - rsp, err := c.post(ctx, fmt.Sprintf("/instances/%s/permissions/skip", c.id), nil, jsonBody(proto.PermissionSkipRequest{Skip: skip}), http.Header{"Content-Type": []string{"application/json"}}) +func (c *Client) SetPermissionsSkipRequests(ctx context.Context, id string, skip bool) error { + rsp, err := c.post(ctx, fmt.Sprintf("/instances/%s/permissions/skip", id), nil, jsonBody(proto.PermissionSkipRequest{Skip: skip}), http.Header{"Content-Type": []string{"application/json"}}) if err != nil { return fmt.Errorf("failed to set permissions skip requests: %w", err) } @@ -391,8 +391,8 @@ func (c *Client) SetPermissionsSkipRequests(ctx context.Context, skip bool) erro return nil } -func (c *Client) GetPermissionsSkipRequests(ctx context.Context) (bool, error) { - rsp, err := c.get(ctx, fmt.Sprintf("/instances/%s/permissions/skip", c.id), nil, nil) +func (c *Client) GetPermissionsSkipRequests(ctx context.Context, id string) (bool, error) { + rsp, err := c.get(ctx, fmt.Sprintf("/instances/%s/permissions/skip", id), nil, nil) if err != nil { return false, fmt.Errorf("failed to get permissions skip requests: %w", err) } @@ -407,8 +407,8 @@ func (c *Client) GetPermissionsSkipRequests(ctx context.Context) (bool, error) { return skip.Skip, nil } -func (c *Client) GetConfig(ctx context.Context) (*config.Config, error) { - rsp, err := c.get(ctx, fmt.Sprintf("/instances/%s/config", c.id), nil, nil) +func (c *Client) GetConfig(ctx context.Context, id string) (*config.Config, error) { + rsp, err := c.get(ctx, fmt.Sprintf("/instances/%s/config", id), nil, nil) if err != nil { return nil, fmt.Errorf("failed to get config: %w", err) } diff --git a/internal/cmd/root.go b/internal/cmd/root.go index b1c31a116712bbe8afe8a660948fcacf5f1693ca..e3cae2f6e4c4b073806686eb94537793308fd3e7 100644 --- a/internal/cmd/root.go +++ b/internal/cmd/root.go @@ -108,7 +108,7 @@ crush -y // TODO: implement TCP support } - c, err := setupApp(cmd, hostURL) + c, ins, err := setupApp(cmd, hostURL) if err != nil { return err } @@ -128,12 +128,12 @@ crush -y return fmt.Errorf("failed to connect to crush server: %v", err) } - m, err := tui.New(c) + m, err := tui.New(c, ins) if err != nil { return fmt.Errorf("failed to create TUI model: %v", err) } - defer func() { c.DeleteInstance(cmd.Context(), c.ID()) }() + defer func() { c.DeleteInstance(cmd.Context(), ins.ID) }() event.AppInitialized() @@ -146,7 +146,7 @@ crush -y tea.WithFilter(tui.MouseEventFilter), // Filter mouse events based on focus state ) - evc, err := c.SubscribeEvents(cmd.Context()) + evc, err := c.SubscribeEvents(cmd.Context(), ins.ID) if err != nil { return fmt.Errorf("failed to subscribe to events: %v", err) } @@ -199,7 +199,7 @@ func streamEvents(ctx context.Context, evc <-chan any, p *tea.Program) { // setupApp handles the common setup logic for both interactive and non-interactive modes. // It returns the app instance, config, cleanup function, and any error. -func setupApp(cmd *cobra.Command, hostURL *url.URL) (*client.Client, error) { +func setupApp(cmd *cobra.Command, hostURL *url.URL) (*client.Client, *proto.Instance, error) { debug, _ := cmd.Flags().GetBool("debug") yolo, _ := cmd.Flags().GetBool("yolo") dataDir, _ := cmd.Flags().GetString("data-dir") @@ -207,12 +207,12 @@ func setupApp(cmd *cobra.Command, hostURL *url.URL) (*client.Client, error) { cwd, err := ResolveCwd(cmd) if err != nil { - return nil, err + return nil, nil, err } c, err := client.NewClient(cwd, hostURL.Scheme, hostURL.Host) if err != nil { - return nil, err + return nil, nil, err } ins, err := c.CreateInstance(ctx, proto.Instance{ @@ -222,21 +222,19 @@ func setupApp(cmd *cobra.Command, hostURL *url.URL) (*client.Client, error) { YOLO: yolo, }) if err != nil { - return nil, fmt.Errorf("failed to create or connect to instance: %v", err) + return nil, nil, fmt.Errorf("failed to create or connect to instance: %v", err) } - c.SetID(ins.ID) - cfg, err := c.GetGlobalConfig(cmd.Context()) if err != nil { - return nil, fmt.Errorf("failed to get global config: %v", err) + return nil, nil, fmt.Errorf("failed to get global config: %v", err) } if shouldEnableMetrics(cfg) { event.Init() } - return c, nil + return c, ins, nil } var safeNameRegexp = regexp.MustCompile(`[^a-zA-Z0-9._-]`) diff --git a/internal/cmd/run.go b/internal/cmd/run.go index ee19f920cfcc87525572caf1991446b1de16b5a2..fe3b5bdec9c872f2c8bbb451b45066bc0db122f6 100644 --- a/internal/cmd/run.go +++ b/internal/cmd/run.go @@ -31,13 +31,13 @@ crush run -q "Generate a README for this project" return fmt.Errorf("invalid host URL: %v", err) } - c, err := setupApp(cmd, hostURL) + c, ins, err := setupApp(cmd, hostURL) if err != nil { return err } - defer func() { c.DeleteInstance(cmd.Context(), c.ID()) }() + defer func() { c.DeleteInstance(cmd.Context(), ins.ID) }() - cfg, err := c.GetConfig(cmd.Context()) + cfg, err := c.GetConfig(cmd.Context(), ins.ID) if err != nil { return fmt.Errorf("failed to get config: %v", err) } diff --git a/internal/proto/proto.go b/internal/proto/proto.go index 1852cbaf0b71862a73f6e6d5fbf7d10a24a3a09c..37a4ac9aa78b88bfd6dc6b2f0d8911467cf21af2 100644 --- a/internal/proto/proto.go +++ b/internal/proto/proto.go @@ -4,17 +4,19 @@ import ( "time" "github.com/charmbracelet/catwalk/pkg/catwalk" + "github.com/charmbracelet/crush/internal/config" "github.com/charmbracelet/crush/internal/lsp" ) // Instance represents a running app.App instance with its associated resources // and state. type Instance struct { - ID string `json:"id"` - Path string `json:"path"` - YOLO bool `json:"yolo,omitempty"` - Debug bool `json:"debug,omitempty"` - DataDir string `json:"data_dir,omitempty"` + ID string `json:"id"` + Path string `json:"path"` + YOLO bool `json:"yolo,omitempty"` + Debug bool `json:"debug,omitempty"` + DataDir string `json:"data_dir,omitempty"` + Config *config.Config `json:"config,omitempty"` } // Error represents an error response. diff --git a/internal/server/proto.go b/internal/server/proto.go index d4da17bbdda243067cae571c74b825be289168e2..62486f73ff6a8d66ebbe12a1c0e768afb5296b68 100644 --- a/internal/server/proto.go +++ b/internal/server/proto.go @@ -67,12 +67,14 @@ func (c *controllerV1) handleGetConfig(w http.ResponseWriter, r *http.Request) { func (c *controllerV1) handleGetInstances(w http.ResponseWriter, r *http.Request) { instances := []proto.Instance{} for _, ins := range c.instances.Seq2() { + // TODO: implement pagination? instances = append(instances, proto.Instance{ - ID: ins.ID(), - Path: ins.Path(), + ID: ins.id, + Path: ins.path, YOLO: ins.cfg.Permissions != nil && ins.cfg.Permissions.SkipRequests, DataDir: ins.cfg.Options.DataDirectory, Debug: ins.cfg.Options.Debug, + Config: ins.cfg, }) } jsonEncode(w, instances) @@ -512,6 +514,25 @@ func (c *controllerV1) handleDeleteInstances(w http.ResponseWriter, r *http.Requ c.instances.Del(id) } +func (c *controllerV1) handleGetInstance(w http.ResponseWriter, r *http.Request) { + id := r.PathValue("id") + ins, ok := c.instances.Get(id) + if !ok { + c.logError(r, "instance not found", "id", id) + jsonError(w, http.StatusNotFound, "instance not found") + return + } + + jsonEncode(w, proto.Instance{ + ID: ins.id, + Path: ins.path, + YOLO: ins.cfg.Permissions != nil && ins.cfg.Permissions.SkipRequests, + DataDir: ins.cfg.Options.DataDirectory, + Debug: ins.cfg.Options.Debug, + Config: ins.cfg, + }) +} + func (c *controllerV1) handlePostInstances(w http.ResponseWriter, r *http.Request) { var args proto.Instance if err := json.NewDecoder(r.Body).Decode(&args); err != nil { @@ -575,6 +596,7 @@ func (c *controllerV1) handlePostInstances(w http.ResponseWriter, r *http.Reques DataDir: cfg.Options.DataDirectory, Debug: cfg.Options.Debug, YOLO: cfg.Permissions.SkipRequests, + Config: cfg, }) } diff --git a/internal/server/server.go b/internal/server/server.go index b12c1e797eea4f9100ec55702265c0215831be3d..afce3d66d5db6164b1f1440b04e1950c3d4edd5b 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -42,16 +42,6 @@ type Instance struct { path string } -// ID returns the unique identifier of the instance. -func (i *Instance) ID() string { - return i.id -} - -// Path returns the filesystem path associated with the instance. -func (i *Instance) Path() string { - return i.path -} - // ParseHostURL parses a host URL into a [url.URL]. func ParseHostURL(host string) (*url.URL, error) { proto, addr, ok := strings.Cut(host, "://") @@ -141,6 +131,7 @@ func NewServer(cfg *config.Config, network, address string) *Server { mux.HandleFunc("GET /v1/instances", c.handleGetInstances) mux.HandleFunc("POST /v1/instances", c.handlePostInstances) mux.HandleFunc("DELETE /v1/instances/{id}", c.handleDeleteInstances) + mux.HandleFunc("GET /v1/instances/{id}", c.handleGetInstance) mux.HandleFunc("GET /v1/instances/{id}/config", c.handleGetInstanceConfig) mux.HandleFunc("GET /v1/instances/{id}/events", c.handleGetInstanceEvents) mux.HandleFunc("GET /v1/instances/{id}/sessions", c.handleGetInstanceSessions) diff --git a/internal/tui/components/chat/chat.go b/internal/tui/components/chat/chat.go index 15086f866afe249451d6219e68241b89be4691b4..2fcce5300566bc2d2bfd50a5d9489ee3e4c1af55 100644 --- a/internal/tui/components/chat/chat.go +++ b/internal/tui/components/chat/chat.go @@ -9,10 +9,10 @@ import ( "github.com/charmbracelet/bubbles/v2/key" tea "github.com/charmbracelet/bubbletea/v2" "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" + "github.com/charmbracelet/crush/internal/proto" "github.com/charmbracelet/crush/internal/pubsub" "github.com/charmbracelet/crush/internal/session" "github.com/charmbracelet/crush/internal/tui/components/chat/messages" @@ -59,8 +59,8 @@ type MessageListCmp interface { // of chat messages with support for tool calls, real-time updates, and // session switching. type messageListCmp struct { - client *client.Client - cfg *config.Config + c *client.Client + ins *proto.Instance width, height int session session.Session listCmp list.List[list.Item] @@ -79,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 *client.Client, cfg *config.Config) MessageListCmp { +func New(app *client.Client, ins *proto.Instance) MessageListCmp { defaultListKeyMap := list.DefaultKeyMap() listCmp := list.New( []list.Item{}, @@ -90,8 +90,8 @@ func New(app *client.Client, cfg *config.Config) MessageListCmp { list.WithEnableMouse(), ) return &messageListCmp{ - client: app, - cfg: cfg, + c: app, + ins: ins, listCmp: listCmp, previousSelected: "", defaultListKeyMap: defaultListKeyMap, @@ -106,9 +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 - info, err := m.client.GetAgentInfo(context.TODO()) + info, err := m.c.GetAgentInfo(context.TODO(), m.ins.ID) if m.session.ID != "" && err == nil && !info.IsZero() { - queueSize, _ := m.client.GetAgentSessionQueuedPrompts(context.TODO(), m.session.ID) + queueSize, _ := m.c.GetAgentSessionQueuedPrompts(context.TODO(), m.ins.ID, m.session.ID) if queueSize != m.promptQueue { m.promptQueue = queueSize cmds = append(cmds, m.SetSize(m.width, m.height)) @@ -239,7 +239,7 @@ func (m *messageListCmp) View() string { m.listCmp.View(), ), } - info, err := m.client.GetAgentInfo(context.TODO()) + info, err := m.c.GetAgentInfo(context.TODO(), m.ins.ID) 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)) @@ -373,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(m.cfg, msg)) + return m.listCmp.AppendItem(messages.NewMessageCmp(m.ins.Config, msg)) } // handleToolMessage updates existing tool calls with their results. @@ -466,7 +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, + m.ins.Config, msg, time.Unix(m.lastUserMessageTime, 0), ), @@ -524,7 +524,7 @@ func (m *messageListCmp) handleNewAssistantMessage(msg message.Message) tea.Cmd if m.shouldShowAssistantMessage(msg) { cmd := m.listCmp.AppendItem( messages.NewMessageCmp( - m.cfg, + m.ins.Config, msg, ), ) @@ -547,7 +547,7 @@ func (m *messageListCmp) SetSession(session session.Session) tea.Cmd { } m.session = session - sessionMessages, err := m.client.ListMessages(context.Background(), session.ID) + sessionMessages, err := m.c.ListMessages(context.Background(), m.ins.ID, session.ID) if err != nil { return util.ReportError(err) } @@ -587,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(m.cfg, msg)) + uiMessages = append(uiMessages, messages.NewMessageCmp(m.ins.Config, 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(m.cfg, msg, time.Unix(m.lastUserMessageTime, 0))) + uiMessages = append(uiMessages, messages.NewAssistantSection(m.ins.Config, msg, time.Unix(m.lastUserMessageTime, 0))) } } } @@ -608,7 +608,7 @@ func (m *messageListCmp) convertAssistantMessage(msg message.Message, toolResult uiMessages = append( uiMessages, messages.NewMessageCmp( - m.cfg, msg, + m.ins.Config, msg, ), ) } @@ -619,7 +619,7 @@ func (m *messageListCmp) convertAssistantMessage(msg message.Message, toolResult 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.client.ListMessages(context.Background(), tc.ID) + nestedMessages, _ := m.c.ListMessages(context.Background(), m.ins.ID, tc.ID) nestedToolResultMap := m.buildToolResultMap(nestedMessages) nestedUIMessages := m.convertMessagesToUI(nestedMessages, nestedToolResultMap) nestedToolCalls := make([]messages.ToolCallCmp, 0, len(nestedUIMessages)) diff --git a/internal/tui/components/chat/editor/editor.go b/internal/tui/components/chat/editor/editor.go index df0e4027e60ec48e91c4c2f8d4c62e96e8d5e14b..d43d6e7babc160f368bffda4ddf328bf8e308089 100644 --- a/internal/tui/components/chat/editor/editor.go +++ b/internal/tui/components/chat/editor/editor.go @@ -19,6 +19,7 @@ import ( "github.com/charmbracelet/crush/internal/client" "github.com/charmbracelet/crush/internal/fsext" "github.com/charmbracelet/crush/internal/message" + "github.com/charmbracelet/crush/internal/proto" "github.com/charmbracelet/crush/internal/session" "github.com/charmbracelet/crush/internal/tui/components/chat" "github.com/charmbracelet/crush/internal/tui/components/completions" @@ -53,7 +54,8 @@ type editorCmp struct { width int height int x, y int - app *client.Client + c *client.Client + ins *proto.Instance session session.Session textarea *textarea.Model attachments []message.Attachment @@ -211,7 +213,7 @@ func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } case commands.OpenExternalEditorMsg: - info, err := m.app.GetAgentSessionInfo(context.TODO(), m.session.ID) + info, err := m.c.GetAgentSessionInfo(context.TODO(), m.ins.ID, m.session.ID) if err == nil && info.IsBusy { return m, util.ReportWarn("Agent is working, please wait...") } @@ -298,7 +300,7 @@ func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } } if key.Matches(msg, m.keyMap.OpenEditor) { - info, err := m.app.GetAgentSessionInfo(context.TODO(), m.session.ID) + info, err := m.c.GetAgentSessionInfo(context.TODO(), m.ins.ID, m.session.ID) if err == nil && info.IsBusy { return m, util.ReportWarn("Agent is working, please wait...") } @@ -367,7 +369,7 @@ func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } func (m *editorCmp) setEditorPrompt() { - skip, err := m.app.GetPermissionsSkipRequests(context.TODO()) + skip, err := m.c.GetPermissionsSkipRequests(context.TODO(), m.ins.ID) if err == nil && skip { m.textarea.SetPromptFunc(4, yoloPromptFunc) return @@ -418,13 +420,13 @@ func (m *editorCmp) randomizePlaceholders() { func (m *editorCmp) View() string { t := styles.CurrentTheme() // Update placeholder - info, err := m.app.GetAgentInfo(context.TODO()) + info, err := m.c.GetAgentInfo(context.TODO(), m.ins.ID) if err == nil && info.IsBusy { m.textarea.Placeholder = m.workingPlaceholder } else { m.textarea.Placeholder = m.readyPlaceholder } - skip, err := m.app.GetPermissionsSkipRequests(context.TODO()) + skip, err := m.c.GetPermissionsSkipRequests(context.TODO(), m.ins.ID) if err == nil && skip { m.textarea.Placeholder = "Yolo mode!" } @@ -568,7 +570,7 @@ func yoloPromptFunc(info textarea.PromptInfo) string { return fmt.Sprintf("%s ", t.YoloDotsBlurred) } -func New(app *client.Client) Editor { +func New(c *client.Client, ins *proto.Instance) Editor { t := styles.CurrentTheme() ta := textarea.New() ta.SetStyles(t.S().TextArea) @@ -578,7 +580,8 @@ func New(app *client.Client) Editor { ta.Focus() e := &editorCmp{ // TODO: remove the app instance from here - app: app, + c: c, + ins: ins, textarea: ta, keyMap: DefaultEditorKeyMap(), } diff --git a/internal/tui/components/chat/header/header.go b/internal/tui/components/chat/header/header.go index f92b1bfffcf3e8bda2b81e56a95f06c40cde7299..180b2c41af1cb013c0e43bc774341964676e504d 100644 --- a/internal/tui/components/chat/header/header.go +++ b/internal/tui/components/chat/header/header.go @@ -7,8 +7,8 @@ import ( tea "github.com/charmbracelet/bubbletea/v2" "github.com/charmbracelet/crush/internal/client" - "github.com/charmbracelet/crush/internal/config" "github.com/charmbracelet/crush/internal/fsext" + "github.com/charmbracelet/crush/internal/proto" "github.com/charmbracelet/crush/internal/pubsub" "github.com/charmbracelet/crush/internal/session" "github.com/charmbracelet/crush/internal/tui/styles" @@ -30,14 +30,14 @@ type header struct { width int session session.Session client *client.Client - cfg *config.Config + ins *proto.Instance detailsOpen bool } -func New(lspClients *client.Client, cfg *config.Config) Header { +func New(lspClients *client.Client, ins *proto.Instance) Header { return &header{ client: lspClients, - cfg: cfg, + ins: ins, width: 0, } } @@ -108,14 +108,14 @@ func (h *header) details(availWidth int) string { errorCount := 0 // TODO: Move this to update? - lsps, err := h.client.GetLSPs(context.TODO()) + lsps, err := h.client.GetLSPs(context.TODO(), h.ins.ID) if err != nil { return "" } for l := range lsps { // TODO: Same here, move to update? - diags, err := h.client.GetLSPDiagnostics(context.TODO(), l) + diags, err := h.client.GetLSPDiagnostics(context.TODO(), h.ins.ID, l) if err != nil { return "" } @@ -132,8 +132,8 @@ func (h *header) details(availWidth int) string { parts = append(parts, s.Error.Render(fmt.Sprintf("%s%d", styles.ErrorIcon, errorCount))) } - agentCfg := h.cfg.Agents["coder"] - model := h.cfg.GetModelByType(agentCfg.Model) + agentCfg := h.ins.Config.Agents["coder"] + model := h.ins.Config.GetModelByType(agentCfg.Model) if model == nil { return "No model" } @@ -154,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(h.cfg.WorkingDir()), dirTrimLimit) + cwd := fsext.DirTrim(fsext.PrettyPath(h.ins.Config.WorkingDir()), dirTrimLimit) cwd = ansi.Truncate(cwd, max(0, availWidth-lipgloss.Width(metadata)), "…") cwd = s.Muted.Render(cwd) diff --git a/internal/tui/components/chat/sidebar/sidebar.go b/internal/tui/components/chat/sidebar/sidebar.go index 65ca23107ca93f3755798966cd88c63a01cd386f..523bd2157cfc70341f2e3afb299896c266e56a86 100644 --- a/internal/tui/components/chat/sidebar/sidebar.go +++ b/internal/tui/components/chat/sidebar/sidebar.go @@ -15,6 +15,7 @@ import ( "github.com/charmbracelet/crush/internal/fsext" "github.com/charmbracelet/crush/internal/history" "github.com/charmbracelet/crush/internal/home" + "github.com/charmbracelet/crush/internal/proto" "github.com/charmbracelet/crush/internal/pubsub" "github.com/charmbracelet/crush/internal/session" "github.com/charmbracelet/crush/internal/tui/components/chat" @@ -69,16 +70,16 @@ type sidebarCmp struct { session session.Session logo string cwd string - client *client.Client + c *client.Client compactMode bool files *csync.Map[string, SessionFile] - cfg *config.Config + ins *proto.Instance } -func New(c *client.Client, cfg *config.Config, compact bool) Sidebar { +func New(c *client.Client, ins *proto.Instance, compact bool) Sidebar { return &sidebarCmp{ - client: c, - cfg: cfg, + c: c, + ins: ins, compactMode: compact, files: csync.NewMap[string, SessionFile](), } @@ -194,7 +195,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 := m.cfg.WorkingDir() + cwd := m.ins.Config.WorkingDir() path = strings.TrimPrefix(path, cwd) _, additions, deletions := diff.GenerateDiff(before, after, path) existing.Additions = additions @@ -221,7 +222,7 @@ func (m *sidebarCmp) handleFileHistoryEvent(event pubsub.Event[history.File]) te } func (m *sidebarCmp) loadSessionFiles() tea.Msg { - files, err := m.client.ListSessionHistoryFiles(context.Background(), m.session.ID) + files, err := m.c.ListSessionHistoryFiles(context.Background(), m.ins.ID, m.session.ID) if err != nil { return util.InfoMsg{ Type: util.InfoTypeError, @@ -247,7 +248,7 @@ func (m *sidebarCmp) loadSessionFiles() tea.Msg { sessionFiles := make([]SessionFile, 0, len(fileMap)) for path, fh := range fileMap { - cwd := m.cfg.WorkingDir() + cwd := m.ins.Config.WorkingDir() path = strings.TrimPrefix(path, cwd) before, _ := fsext.ToUnixLineEndings(fh.initialVersion.Content) after, _ := fsext.ToUnixLineEndings(fh.latestVersion.Content) @@ -267,7 +268,7 @@ func (m *sidebarCmp) loadSessionFiles() tea.Msg { func (m *sidebarCmp) SetSize(width, height int) tea.Cmd { m.logo = m.logoBlock() - m.cwd = cwd(m.cfg) + m.cwd = cwd(m.ins.Config) m.width = width m.height = height return nil @@ -406,7 +407,7 @@ func (m *sidebarCmp) filesBlockCompact(maxWidth int) string { maxItems = min(maxItems, availableHeight) } - return files.RenderFileBlock(m.cfg, fileSlice, files.RenderOptions{ + return files.RenderFileBlock(m.ins.Config, fileSlice, files.RenderOptions{ MaxWidth: maxWidth, MaxItems: maxItems, ShowSection: true, @@ -417,14 +418,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 := m.cfg.LSP.Sorted() + lspConfigs := m.ins.Config.LSP.Sorted() maxItems := min(5, len(lspConfigs)) availableHeight := m.height - 8 if availableHeight > 0 { maxItems = min(maxItems, availableHeight) } - return lspcomponent.RenderLSPBlock(m.client, m.cfg, lspcomponent.RenderOptions{ + return lspcomponent.RenderLSPBlock(m.c, m.ins, lspcomponent.RenderOptions{ MaxWidth: maxWidth, MaxItems: maxItems, ShowSection: true, @@ -435,13 +436,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(m.cfg.MCP.Sorted())) + maxItems := min(5, len(m.ins.Config.MCP.Sorted())) availableHeight := m.height - 8 if availableHeight > 0 { maxItems = min(maxItems, availableHeight) } - return mcp.RenderMCPBlock(m.cfg, mcp.RenderOptions{ + return mcp.RenderMCPBlock(m.ins.Config, mcp.RenderOptions{ MaxWidth: maxWidth, MaxItems: maxItems, ShowSection: true, @@ -469,7 +470,7 @@ func (m *sidebarCmp) filesBlock() string { maxFiles, _, _ := m.getDynamicLimits() maxFiles = min(len(fileSlice), maxFiles) - return files.RenderFileBlock(m.cfg, fileSlice, files.RenderOptions{ + return files.RenderFileBlock(m.ins.Config, fileSlice, files.RenderOptions{ MaxWidth: m.getMaxWidth(), MaxItems: maxFiles, ShowSection: true, @@ -480,10 +481,10 @@ func (m *sidebarCmp) filesBlock() string { func (m *sidebarCmp) lspBlock() string { // Limit the number of LSPs shown _, maxLSPs, _ := m.getDynamicLimits() - lspConfigs := m.cfg.LSP.Sorted() + lspConfigs := m.ins.Config.LSP.Sorted() maxLSPs = min(len(lspConfigs), maxLSPs) - return lspcomponent.RenderLSPBlock(m.client, m.cfg, lspcomponent.RenderOptions{ + return lspcomponent.RenderLSPBlock(m.c, m.ins, lspcomponent.RenderOptions{ MaxWidth: m.getMaxWidth(), MaxItems: maxLSPs, ShowSection: true, @@ -494,10 +495,10 @@ func (m *sidebarCmp) lspBlock() string { func (m *sidebarCmp) mcpBlock() string { // Limit the number of MCPs shown _, _, maxMCPs := m.getDynamicLimits() - mcps := m.cfg.MCP.Sorted() + mcps := m.ins.Config.MCP.Sorted() maxMCPs = min(len(mcps), maxMCPs) - return mcp.RenderMCPBlock(m.cfg, mcp.RenderOptions{ + return mcp.RenderMCPBlock(m.ins.Config, mcp.RenderOptions{ MaxWidth: m.getMaxWidth(), MaxItems: maxMCPs, ShowSection: true, @@ -544,15 +545,15 @@ func formatTokensAndCost(tokens, contextWindow int64, cost float64) string { } func (s *sidebarCmp) currentModelBlock() string { - agentCfg := s.cfg.Agents["coder"] + agentCfg := s.ins.Config.Agents["coder"] - selectedModel := s.cfg.Models[agentCfg.Model] + selectedModel := s.ins.Config.Models[agentCfg.Model] - model := s.cfg.GetModelByType(agentCfg.Model) + model := s.ins.Config.GetModelByType(agentCfg.Model) if model == nil { return "No model found" } - modelProvider := s.cfg.GetProviderForModel(agentCfg.Model) + modelProvider := s.ins.Config.GetProviderForModel(agentCfg.Model) t := styles.CurrentTheme() diff --git a/internal/tui/components/chat/splash/splash.go b/internal/tui/components/chat/splash/splash.go index 1c4f43d175c2aa278c4406fdc621f3aaca4502e2..88585a78bfcdefb435e3a4bf02b84e45b52c8352 100644 --- a/internal/tui/components/chat/splash/splash.go +++ b/internal/tui/components/chat/splash/splash.go @@ -13,6 +13,7 @@ import ( "github.com/charmbracelet/crush/internal/config" "github.com/charmbracelet/crush/internal/home" "github.com/charmbracelet/crush/internal/llm/prompt" + "github.com/charmbracelet/crush/internal/proto" "github.com/charmbracelet/crush/internal/tui/components/chat" "github.com/charmbracelet/crush/internal/tui/components/core" "github.com/charmbracelet/crush/internal/tui/components/core/layout" @@ -74,11 +75,11 @@ type splashCmp struct { isAPIKeyValid bool apiKeyValue string - client *client.Client - cfg *config.Config + c *client.Client + ins *proto.Instance } -func New(c *client.Client, cfg *config.Config) Splash { +func New(c *client.Client, ins *proto.Instance) Splash { keyMap := DefaultKeyMap() listKeyMap := list.DefaultKeyMap() listKeyMap.Down.SetEnabled(false) @@ -90,12 +91,12 @@ func New(c *client.Client, cfg *config.Config) Splash { listKeyMap.DownOneItem = keyMap.Next listKeyMap.UpOneItem = keyMap.Previous - modelList := models.NewModelListComponent(cfg, listKeyMap, "Find your fave", false) + modelList := models.NewModelListComponent(ins.Config, listKeyMap, "Find your fave", false) apiKeyInput := models.NewAPIKeyInput() return &splashCmp{ - client: c, - cfg: cfg, + c: c, + ins: ins, width: 0, height: 0, keyMap: keyMap, @@ -217,7 +218,7 @@ func (s *splashCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { }), func() tea.Msg { start := time.Now() - err := providerConfig.TestConnection(s.cfg.Resolver()) + err := providerConfig.TestConnection(s.ins.Config.Resolver()) // intentionally wait for at least 750ms to make sure the user sees the spinner elapsed := time.Since(start) if elapsed < 750*time.Millisecond { @@ -311,7 +312,7 @@ func (s *splashCmp) saveAPIKeyAndContinue(apiKey string) tea.Cmd { return nil } - err := s.cfg.SetProviderAPIKey(string(s.selectedModel.Provider.ID), apiKey) + err := s.ins.Config.SetProviderAPIKey(string(s.selectedModel.Provider.ID), apiKey) if err != nil { return util.ReportError(fmt.Errorf("failed to save API key: %w", err)) } @@ -329,7 +330,7 @@ func (s *splashCmp) saveAPIKeyAndContinue(apiKey string) tea.Cmd { func (s *splashCmp) initializeProject() tea.Cmd { s.needsProjectInit = false - if err := config.MarkProjectInitialized(s.cfg); err != nil { + if err := config.MarkProjectInitialized(s.ins.Config); err != nil { return util.ReportError(err) } var cmds []tea.Cmd @@ -347,7 +348,7 @@ func (s *splashCmp) initializeProject() tea.Cmd { } func (s *splashCmp) setPreferredModel(selectedItem models.ModelOption) tea.Cmd { - model := s.cfg.GetModel(string(selectedItem.Provider.ID), selectedItem.Model.ID) + model := s.ins.Config.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)) } @@ -359,7 +360,7 @@ func (s *splashCmp) setPreferredModel(selectedItem models.ModelOption) tea.Cmd { MaxTokens: model.DefaultMaxTokens, } - err := s.cfg.UpdatePreferredModel(config.SelectedModelTypeLarge, selectedModel) + err := s.ins.Config.UpdatePreferredModel(config.SelectedModelTypeLarge, selectedModel) if err != nil { return util.ReportError(err) } @@ -371,16 +372,16 @@ func (s *splashCmp) setPreferredModel(selectedItem models.ModelOption) tea.Cmd { } if knownProvider == nil { // for local provider we just use the same model - err = s.cfg.UpdatePreferredModel(config.SelectedModelTypeSmall, selectedModel) + err = s.ins.Config.UpdatePreferredModel(config.SelectedModelTypeSmall, selectedModel) if err != nil { return util.ReportError(err) } } else { smallModel := knownProvider.DefaultSmallModelID - model := s.cfg.GetModel(string(selectedItem.Provider.ID), smallModel) + model := s.ins.Config.GetModel(string(selectedItem.Provider.ID), smallModel) // should never happen if model == nil { - err = s.cfg.UpdatePreferredModel(config.SelectedModelTypeSmall, selectedModel) + err = s.ins.Config.UpdatePreferredModel(config.SelectedModelTypeSmall, selectedModel) if err != nil { return util.ReportError(err) } @@ -392,17 +393,17 @@ func (s *splashCmp) setPreferredModel(selectedItem models.ModelOption) tea.Cmd { ReasoningEffort: model.DefaultReasoningEffort, MaxTokens: model.DefaultMaxTokens, } - err = s.cfg.UpdatePreferredModel(config.SelectedModelTypeSmall, smallSelectedModel) + err = s.ins.Config.UpdatePreferredModel(config.SelectedModelTypeSmall, smallSelectedModel) if err != nil { return util.ReportError(err) } } - s.cfg.SetupAgents() + s.ins.Config.SetupAgents() return nil } func (s *splashCmp) getProvider(providerID catwalk.InferenceProvider) (*catwalk.Provider, error) { - providers, err := config.Providers(s.cfg) + providers, err := config.Providers(s.ins.Config) if err != nil { return nil, err } @@ -415,7 +416,7 @@ func (s *splashCmp) getProvider(providerID catwalk.InferenceProvider) (*catwalk. } func (s *splashCmp) isProviderConfigured(providerID string) bool { - if _, ok := s.cfg.Providers.Get(providerID); ok { + if _, ok := s.ins.Config.Providers.Get(providerID); ok { return true } return false @@ -652,11 +653,11 @@ func (s *splashCmp) cwdPart() string { } func (s *splashCmp) cwd() string { - return home.Short(s.cfg.WorkingDir()) + return home.Short(s.ins.Config.WorkingDir()) } -func LSPList(c *client.Client, cfg *config.Config, maxWidth int) []string { - return lspcomponent.RenderLSPList(c, cfg, lspcomponent.RenderOptions{ +func LSPList(c *client.Client, ins *proto.Instance, maxWidth int) []string { + return lspcomponent.RenderLSPList(c, ins, lspcomponent.RenderOptions{ MaxWidth: maxWidth, ShowSection: false, }) @@ -666,7 +667,7 @@ func (s *splashCmp) lspBlock() string { t := styles.CurrentTheme() maxWidth := s.getMaxInfoWidth() / 2 section := t.S().Subtle.Render("LSPs") - lspList := append([]string{section, ""}, LSPList(s.client, s.cfg, maxWidth-1)...) + lspList := append([]string{section, ""}, LSPList(s.c, s.ins, maxWidth-1)...) return t.S().Base.Width(maxWidth).PaddingRight(1).Render( lipgloss.JoinVertical( lipgloss.Left, @@ -686,7 +687,7 @@ func (s *splashCmp) mcpBlock() string { t := styles.CurrentTheme() maxWidth := s.getMaxInfoWidth() / 2 section := t.S().Subtle.Render("MCPs") - mcpList := append([]string{section, ""}, MCPList(s.cfg, maxWidth-1)...) + mcpList := append([]string{section, ""}, MCPList(s.ins.Config, maxWidth-1)...) return t.S().Base.Width(maxWidth).PaddingRight(1).Render( lipgloss.JoinVertical( lipgloss.Left, @@ -696,8 +697,8 @@ func (s *splashCmp) mcpBlock() string { } func (s *splashCmp) currentModelBlock() string { - agentCfg := s.cfg.Agents["coder"] - model := s.cfg.GetModelByType(agentCfg.Model) + agentCfg := s.ins.Config.Agents["coder"] + model := s.ins.Config.GetModelByType(agentCfg.Model) if model == nil { return "" } diff --git a/internal/tui/components/dialogs/commands/commands.go b/internal/tui/components/dialogs/commands/commands.go index 16470c3a0a8b09598d44efd9ae7824528ad4fc5c..fc5f7e50436a98ba8bebde2d6c5b176c83e86d51 100644 --- a/internal/tui/components/dialogs/commands/commands.go +++ b/internal/tui/components/dialogs/commands/commands.go @@ -354,7 +354,7 @@ func (c *commandDialogCmp) defaultCommands() []Command { if c.sessionID != "" { agentCfg := c.cfg.Agents["coder"] model := c.cfg.GetModelByType(agentCfg.Model) - if model.SupportsImages { + if model != nil && model.SupportsImages { commands = append(commands, Command{ ID: "file_picker", Title: "Open File Picker", diff --git a/internal/tui/components/dialogs/compact/compact.go b/internal/tui/components/dialogs/compact/compact.go index bd388055ce0df66073576149f55b41f869105081..e5ede4c91ce9479ab1069ebf04808d6e76a43bdb 100644 --- a/internal/tui/components/dialogs/compact/compact.go +++ b/internal/tui/components/dialogs/compact/compact.go @@ -9,6 +9,7 @@ import ( "github.com/charmbracelet/crush/internal/client" "github.com/charmbracelet/crush/internal/llm/agent" + "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" @@ -30,7 +31,8 @@ type compactDialogCmp struct { sessionID string state compactState progress string - client *client.Client + c *client.Client + ins *proto.Instance noAsk bool // If true, skip confirmation dialog } @@ -43,13 +45,14 @@ const ( ) // NewCompactDialogCmp creates a new session compact dialog -func NewCompactDialogCmp(c *client.Client, sessionID string, noAsk bool) CompactDialog { +func NewCompactDialogCmp(c *client.Client, ins *proto.Instance, sessionID string, noAsk bool) CompactDialog { return &compactDialogCmp{ sessionID: sessionID, keyMap: DefaultKeyMap(), state: stateConfirm, selected: 0, - client: c, + c: c, + ins: ins, noAsk: noAsk, } } @@ -134,7 +137,7 @@ func (c *compactDialogCmp) startCompaction() tea.Cmd { c.state = stateCompacting c.progress = "Starting summarization..." return func() tea.Msg { - err := c.client.AgentSummarizeSession(context.Background(), c.sessionID) + err := c.c.AgentSummarizeSession(context.Background(), c.ins.ID, c.sessionID) if err != nil { c.state = stateError c.progress = "Error: " + err.Error() diff --git a/internal/tui/components/lsp/lsp.go b/internal/tui/components/lsp/lsp.go index 2a0fc30a65ba2ce01d4dcd7db583f5b47d6009d5..b471f97a8fff28f2fbe653f378dfc69c85e30b40 100644 --- a/internal/tui/components/lsp/lsp.go +++ b/internal/tui/components/lsp/lsp.go @@ -7,8 +7,8 @@ import ( "strings" "github.com/charmbracelet/crush/internal/client" - "github.com/charmbracelet/crush/internal/config" "github.com/charmbracelet/crush/internal/lsp" + "github.com/charmbracelet/crush/internal/proto" "github.com/charmbracelet/crush/internal/tui/components/core" "github.com/charmbracelet/crush/internal/tui/styles" "github.com/charmbracelet/lipgloss/v2" @@ -24,7 +24,7 @@ type RenderOptions struct { } // RenderLSPList renders a list of LSP status items with the given options. -func RenderLSPList(c *client.Client, cfg *config.Config, opts RenderOptions) []string { +func RenderLSPList(c *client.Client, ins *proto.Instance, opts RenderOptions) []string { t := styles.CurrentTheme() lspList := []string{} @@ -37,14 +37,14 @@ func RenderLSPList(c *client.Client, cfg *config.Config, opts RenderOptions) []s lspList = append(lspList, section, "") } - lspConfigs := cfg.LSP.Sorted() + lspConfigs := ins.Config.LSP.Sorted() if len(lspConfigs) == 0 { lspList = append(lspList, t.S().Base.Foreground(t.Border).Render("None")) return lspList } // Get LSP states - lspStates, err := c.GetLSPs(context.TODO()) + lspStates, err := c.GetLSPs(context.TODO(), ins.ID) if err != nil { slog.Error("failed to get lsp clients") return nil @@ -98,7 +98,7 @@ func RenderLSPList(c *client.Client, cfg *config.Config, opts RenderOptions) []s protocol.SeverityInformation: 0, } if _, ok := lspStates[l.Name]; ok { - diags, err := c.GetLSPDiagnostics(context.TODO(), l.Name) + diags, err := c.GetLSPDiagnostics(context.TODO(), ins.ID, l.Name) if err != nil { slog.Error("couldn't get lsp diagnostics", "lsp", l.Name) return nil @@ -145,10 +145,10 @@ func RenderLSPList(c *client.Client, cfg *config.Config, opts RenderOptions) []s } // RenderLSPBlock renders a complete LSP block with optional truncation indicator. -func RenderLSPBlock(c *client.Client, cfg *config.Config, opts RenderOptions, showTruncationIndicator bool) string { +func RenderLSPBlock(c *client.Client, ins *proto.Instance, opts RenderOptions, showTruncationIndicator bool) string { t := styles.CurrentTheme() - lspList := RenderLSPList(c, cfg, opts) - cfg, err := c.GetConfig(context.TODO()) + lspList := RenderLSPList(c, ins, opts) + cfg, err := c.GetConfig(context.TODO(), ins.ID) if err != nil { slog.Error("failed to get config for lsp block rendering", "error", err) return "" diff --git a/internal/tui/page/chat/chat.go b/internal/tui/page/chat/chat.go index d188782f3abdd696e2948bec1a953dfbc524b71e..d7202d833624d11bc91c9a87c278e67e7abf27e2 100644 --- a/internal/tui/page/chat/chat.go +++ b/internal/tui/page/chat/chat.go @@ -15,6 +15,7 @@ import ( "github.com/charmbracelet/crush/internal/history" "github.com/charmbracelet/crush/internal/message" "github.com/charmbracelet/crush/internal/permission" + "github.com/charmbracelet/crush/internal/proto" "github.com/charmbracelet/crush/internal/pubsub" "github.com/charmbracelet/crush/internal/session" "github.com/charmbracelet/crush/internal/tui/components/anim" @@ -90,8 +91,8 @@ func cancelTimerCmd() tea.Cmd { type chatPage struct { width, height int detailsWidth, detailsHeight int - app *client.Client - cfg *config.Config + c *client.Client + ins *proto.Instance keyboardEnhancements tea.KeyboardEnhancementsMsg // Layout state @@ -118,33 +119,33 @@ type chatPage struct { isProjectInit bool } -func New(app *client.Client, cfg *config.Config) ChatPage { +func New(c *client.Client, ins *proto.Instance) ChatPage { return &chatPage{ - app: app, - cfg: cfg, + c: c, + ins: ins, keyMap: DefaultKeyMap(), - header: header.New(app, cfg), - sidebar: sidebar.New(app, cfg, false), - chat: chat.New(app, cfg), - editor: editor.New(app), - splash: splash.New(app, cfg), + header: header.New(c, ins), + sidebar: sidebar.New(c, ins, false), + chat: chat.New(c, ins), + editor: editor.New(c, ins), + splash: splash.New(c, ins), focusedPane: PanelTypeSplash, } } func (p *chatPage) Init() tea.Cmd { - compact := p.cfg.Options.TUI.CompactMode + compact := p.ins.Config.Options.TUI.CompactMode p.compact = compact p.forceCompact = compact p.sidebar.SetCompactMode(p.compact) // Set splash state based on config - if !config.HasInitialDataConfig(p.cfg) { + if !config.HasInitialDataConfig(p.ins.Config) { // First-time setup: show model selection p.splash.SetOnboarding(true) p.isOnboarding = true p.splashFullScreen = true - } else if b, _ := config.ProjectNeedsInitialization(p.cfg); b { + } else if b, _ := config.ProjectNeedsInitialization(p.ins.Config); b { // Project needs CRUSH.md initialization p.splash.SetProjectInit(true) p.isProjectInit = true @@ -332,7 +333,7 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return p, tea.Batch(cmds...) case commands.CommandRunCustomMsg: - info, err := p.app.GetAgentInfo(context.TODO()) + info, err := p.c.GetAgentInfo(context.TODO(), p.ins.ID) if err != nil { return p, util.ReportError(fmt.Errorf("failed to get agent info: %w", err)) } @@ -346,12 +347,12 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } case splash.OnboardingCompleteMsg: p.splashFullScreen = false - if b, _ := config.ProjectNeedsInitialization(p.cfg); b { + if b, _ := config.ProjectNeedsInitialization(p.ins.Config); b { p.splash.SetProjectInit(true) p.splashFullScreen = true return p, p.SetSize(p.width, p.height) } - err := p.app.InitiateAgentProcessing(context.TODO()) + err := p.c.InitiateAgentProcessing(context.TODO(), p.ins.ID) if err != nil { return p, util.ReportError(err) } @@ -360,7 +361,7 @@ 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: - info, err := p.app.GetAgentInfo(context.TODO()) + info, err := p.c.GetAgentInfo(context.TODO(), p.ins.ID) if err != nil { return p, util.ReportError(fmt.Errorf("failed to get agent info: %w", err)) } @@ -372,7 +373,7 @@ 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 - info, err := p.app.GetAgentInfo(context.TODO()) + info, err := p.c.GetAgentInfo(context.TODO(), p.ins.ID) if err != nil || info.IsZero() { return p, nil } @@ -381,8 +382,8 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } return p, p.newSession() case key.Matches(msg, p.keyMap.AddAttachment): - agentCfg := p.cfg.Agents["coder"] - model := p.cfg.GetModelByType(agentCfg.Model) + agentCfg := p.ins.Config.Agents["coder"] + model := p.ins.Config.GetModelByType(agentCfg.Model) if model.SupportsImages { return p, util.CmdHandler(commands.OpenFilePickerMsg{}) } else { @@ -397,7 +398,7 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) { p.changeFocus() return p, nil case key.Matches(msg, p.keyMap.Cancel): - info, err := p.app.GetAgentInfo(context.TODO()) + info, err := p.c.GetAgentInfo(context.TODO(), p.ins.ID) if err != nil { return p, util.ReportError(fmt.Errorf("failed to get agent info: %w", err)) } @@ -530,7 +531,7 @@ func (p *chatPage) View() string { func (p *chatPage) updateCompactConfig(compact bool) tea.Cmd { return func() tea.Msg { - err := p.cfg.SetCompactMode(compact) + err := p.ins.Config.SetCompactMode(compact) if err != nil { return util.InfoMsg{ Type: util.InfoTypeError, @@ -543,15 +544,15 @@ func (p *chatPage) updateCompactConfig(compact bool) tea.Cmd { func (p *chatPage) toggleThinking() tea.Cmd { return func() tea.Msg { - agentCfg := p.cfg.Agents["coder"] - currentModel := p.cfg.Models[agentCfg.Model] + agentCfg := p.ins.Config.Agents["coder"] + currentModel := p.ins.Config.Models[agentCfg.Model] // Toggle the thinking mode currentModel.Think = !currentModel.Think - p.cfg.Models[agentCfg.Model] = currentModel + p.ins.Config.Models[agentCfg.Model] = currentModel // Update the agent with the new configuration - if err := p.app.UpdateAgent(context.TODO()); err != nil { + if err := p.c.UpdateAgent(context.TODO(), p.ins.ID); err != nil { return util.InfoMsg{ Type: util.InfoTypeError, Msg: "Failed to update thinking mode: " + err.Error(), @@ -571,15 +572,15 @@ func (p *chatPage) toggleThinking() tea.Cmd { func (p *chatPage) openReasoningDialog() tea.Cmd { return func() tea.Msg { - agentCfg := p.cfg.Agents["coder"] - model := p.cfg.GetModelByType(agentCfg.Model) - providerCfg := p.cfg.GetProviderForModel(agentCfg.Model) + agentCfg := p.ins.Config.Agents["coder"] + model := p.ins.Config.GetModelByType(agentCfg.Model) + providerCfg := p.ins.Config.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(p.cfg), + Model: reasoning.NewReasoningDialog(p.ins.Config), } } return nil @@ -588,7 +589,7 @@ func (p *chatPage) openReasoningDialog() tea.Cmd { func (p *chatPage) handleReasoningEffortSelected(effort string) tea.Cmd { return func() tea.Msg { - cfg := p.cfg + cfg := p.ins.Config agentCfg := cfg.Agents["coder"] currentModel := cfg.Models[agentCfg.Model] @@ -597,7 +598,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.UpdateAgent(context.TODO()); err != nil { + if err := p.c.UpdateAgent(context.TODO(), p.ins.ID); err != nil { return util.InfoMsg{ Type: util.InfoTypeError, Msg: "Failed to update reasoning effort: " + err.Error(), @@ -718,13 +719,13 @@ func (p *chatPage) changeFocus() { func (p *chatPage) cancel() tea.Cmd { if p.isCanceling { p.isCanceling = false - _ = p.app.ClearAgentSessionQueuedPrompts(context.TODO(), p.session.ID) + _ = p.c.ClearAgentSessionQueuedPrompts(context.TODO(), p.ins.ID, p.session.ID) return nil } - queued, _ := p.app.GetAgentSessionQueuedPrompts(context.TODO(), p.session.ID) + queued, _ := p.c.GetAgentSessionQueuedPrompts(context.TODO(), p.ins.ID, p.session.ID) if queued > 0 { - _ = p.app.ClearAgentSessionQueuedPrompts(context.TODO(), p.session.ID) + _ = p.c.ClearAgentSessionQueuedPrompts(context.TODO(), p.ins.ID, p.session.ID) return nil } p.isCanceling = true @@ -750,18 +751,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.CreateSession(context.Background(), "New Session") + newSession, err := p.c.CreateSession(context.Background(), p.ins.ID, "New Session") if err != nil { return util.ReportError(err) } session = *newSession cmds = append(cmds, util.CmdHandler(chat.SessionSelectedMsg(session))) } - info, err := p.app.GetAgentInfo(context.TODO()) + info, err := p.c.GetAgentInfo(context.TODO(), p.ins.ID) if err != nil || info.IsZero() { return util.ReportError(fmt.Errorf("coder agent is not initialized")) } - if err := p.app.SendMessage(context.Background(), session.ID, text, attachments...); err != nil { + if err := p.c.SendMessage(context.Background(), p.ins.ID, session.ID, text, attachments...); err != nil { return util.ReportError(err) } cmds = append(cmds, p.chat.GoToBottom()) @@ -773,7 +774,7 @@ func (p *chatPage) Bindings() []key.Binding { p.keyMap.NewSession, p.keyMap.AddAttachment, } - info, err := p.app.GetAgentInfo(context.TODO()) + info, err := p.c.GetAgentInfo(context.TODO(), p.ins.ID) if err == nil && info.IsBusy { cancelBinding := p.keyMap.Cancel if p.isCanceling { @@ -895,7 +896,7 @@ func (p *chatPage) Help() help.KeyMap { } return core.NewSimpleHelp(shortList, fullList) } - info, err := p.app.GetAgentInfo(context.TODO()) + info, err := p.c.GetAgentInfo(context.TODO(), p.ins.ID) if err == nil && info.IsBusy { cancelBinding := key.NewBinding( key.WithKeys("esc", "alt+esc"), @@ -907,7 +908,7 @@ func (p *chatPage) Help() help.KeyMap { key.WithHelp("esc", "press again to cancel"), ) } - queued, _ := p.app.GetAgentSessionQueuedPrompts(context.TODO(), p.session.ID) + queued, _ := p.c.GetAgentSessionQueuedPrompts(context.TODO(), p.ins.ID, p.session.ID) if queued > 0 { cancelBinding = key.NewBinding( key.WithKeys("esc", "alt+esc"), diff --git a/internal/tui/tui.go b/internal/tui/tui.go index 3166ed7eedebe0476ed07fe3785f62214d682681..7bef2e6bc59f80fadba64db1a31c9f7a6455854f 100644 --- a/internal/tui/tui.go +++ b/internal/tui/tui.go @@ -68,8 +68,8 @@ type appModel struct { status status.StatusCmp showingFullHelp bool - app *client.Client - cfg *config.Config + c *client.Client + ins *proto.Instance dialog dialogs.DialogCmp completions completions.Completions @@ -103,7 +103,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.cfg) + a.isConfigured = config.HasInitialDataConfig(a.ins.Config) switch msg := msg.(type) { case tea.KeyboardEnhancementsMsg: @@ -169,7 +169,7 @@ func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // Commands case commands.SwitchSessionsMsg: return a, func() tea.Msg { - allSessions, _ := a.app.ListSessions(context.Background()) + allSessions, _ := a.c.ListSessions(context.Background(), a.ins.ID) return dialogs.OpenDialogMsg{ Model: sessions.NewSessionDialogCmp(allSessions, a.selectedSessionID), } @@ -178,24 +178,24 @@ func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case commands.SwitchModelMsg: return a, util.CmdHandler( dialogs.OpenDialogMsg{ - Model: models.NewModelDialogCmp(a.cfg), + Model: models.NewModelDialogCmp(a.ins.Config), }, ) // Compact case commands.CompactMsg: return a, util.CmdHandler(dialogs.OpenDialogMsg{ - Model: compact.NewCompactDialogCmp(a.app, msg.SessionID, true), + Model: compact.NewCompactDialogCmp(a.c, a.ins, msg.SessionID, true), }) case commands.QuitMsg: return a, util.CmdHandler(dialogs.OpenDialogMsg{ Model: quit.NewQuitDialog(), }) case commands.ToggleYoloModeMsg: - skip, err := a.app.GetPermissionsSkipRequests(context.TODO()) + skip, err := a.c.GetPermissionsSkipRequests(context.TODO(), a.ins.ID) 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 { + if err := a.c.SetPermissionsSkipRequests(context.TODO(), a.ins.ID, !skip); err != nil { return a, util.ReportError(fmt.Errorf("failed to toggle YOLO mode: %v", err)) } case commands.ToggleHelpMsg: @@ -204,17 +204,17 @@ func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return a, a.handleWindowResize(a.wWidth, a.wHeight) // Model Switch case models.ModelSelectedMsg: - info, err := a.app.GetAgentInfo(context.TODO()) + info, err := a.c.GetAgentInfo(context.TODO(), a.ins.ID) 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...") } - a.cfg.UpdatePreferredModel(msg.ModelType, msg.Model) + a.ins.Config.UpdatePreferredModel(msg.ModelType, msg.Model) // Update the agent with the new model/provider configuration - if err := a.app.UpdateAgent(context.TODO()); err != nil { + if err := a.c.UpdateAgent(context.TODO(), a.ins.ID); err != nil { return a, util.ReportError(fmt.Errorf("model changed to %s but failed to update agent: %v", msg.Model.Model, err)) } @@ -233,7 +233,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.cfg.WorkingDir()), + Model: filepicker.NewFilePickerCmp(a.ins.Config.WorkingDir()), }) // Permissions case pubsub.Event[permission.PermissionNotification]: @@ -252,11 +252,11 @@ 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: a.cfg.Options.TUI.DiffMode, + DiffMode: a.ins.Config.Options.TUI.DiffMode, }), }) case permissions.PermissionResponseMsg: - if err := a.app.GrantPermission(context.TODO(), proto.PermissionGrant(msg)); err != nil { + if err := a.c.GrantPermission(context.TODO(), a.ins.ID, proto.PermissionGrant(msg)); err != nil { return a, util.ReportError(fmt.Errorf("failed to grant permission: %v", err)) } return a, nil @@ -281,18 +281,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.GetSession(context.Background(), a.selectedSessionID) + session, err := a.c.GetSession(context.Background(), a.ins.ID, a.selectedSessionID) if err == nil { - info, err := a.app.GetAgentInfo(context.Background()) + info, err := a.c.GetAgentInfo(context.Background(), a.ins.ID) 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)) && !a.cfg.Options.DisableAutoSummarize { // Show compact confirmation dialog + if (tokens >= int64(float64(contextWindow)*0.95)) && !a.ins.Config.Options.DisableAutoSummarize { // Show compact confirmation dialog cmds = append(cmds, util.CmdHandler(dialogs.OpenDialogMsg{ - Model: compact.NewCompactDialogCmp(a.app, a.selectedSessionID, false), + Model: compact.NewCompactDialogCmp(a.c, a.ins, a.selectedSessionID, false), })) } } @@ -305,7 +305,7 @@ func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return a, nil } - a.isConfigured = config.HasInitialDataConfig(a.cfg) + a.isConfigured = config.HasInitialDataConfig(a.ins.Config) updated, pageCmd := item.Update(msg) if model, ok := updated.(util.Model); ok { a.pages[a.currentPage] = model @@ -472,7 +472,7 @@ func (a *appModel) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd { return nil } return util.CmdHandler(dialogs.OpenDialogMsg{ - Model: commands.NewCommandDialog(a.cfg, a.selectedSessionID), + Model: commands.NewCommandDialog(a.ins.Config, a.selectedSessionID), }) case key.Matches(msg, a.keyMap.Sessions): // if the app is not configured show no sessions @@ -492,7 +492,7 @@ func (a *appModel) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd { } cmds = append(cmds, func() tea.Msg { - allSessions, _ := a.app.ListSessions(context.Background()) + allSessions, _ := a.c.ListSessions(context.Background(), a.ins.ID) return dialogs.OpenDialogMsg{ Model: sessions.NewSessionDialogCmp(allSessions, a.selectedSessionID), } @@ -500,7 +500,7 @@ func (a *appModel) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd { ) return tea.Sequence(cmds...) case key.Matches(msg, a.keyMap.Suspend): - info, err := a.app.GetAgentInfo(context.TODO()) + info, err := a.c.GetAgentInfo(context.TODO(), a.ins.ID) if err != nil || info.IsBusy { return util.ReportWarn("Agent is busy, please wait...") } @@ -521,7 +521,7 @@ 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 { - info, err := a.app.GetAgentInfo(context.TODO()) + info, err := a.c.GetAgentInfo(context.TODO(), a.ins.ID) if err != nil { return util.ReportError(fmt.Errorf("failed to check if agent is busy: %v", err)) } @@ -626,25 +626,20 @@ func (a *appModel) View() tea.View { } // New creates and initializes a new TUI application model. -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) - } - +func New(c *client.Client, ins *proto.Instance) (tea.Model, error) { // Setup logs log.Setup( - filepath.Join(cfg.Options.DataDirectory, "logs", "tui.log"), - cfg.Options.Debug, + filepath.Join(ins.Config.Options.DataDirectory, "logs", "tui.log"), + ins.Config.Options.Debug, ) - chatPage := chat.New(app, cfg) + chatPage := chat.New(c, ins) keyMap := DefaultKeyMap() keyMap.pageBindings = chatPage.Bindings() model := &appModel{ + ins: ins, currentPage: chat.ChatPageID, - app: app, - cfg: cfg, + c: c, status: status.NewStatusCmp(), loadedPages: make(map[page.PageID]bool), keyMap: keyMap,