@@ -148,6 +148,15 @@ func (app *App) Events() <-chan tea.Msg {
return app.events
}
+// SendEvent pushes a message into the application's events channel.
+// It is non-blocking; the message is dropped if the channel is full.
+func (app *App) SendEvent(msg tea.Msg) {
+ select {
+ case app.events <- msg:
+ default:
+ }
+}
+
// RunNonInteractive runs the application in non-interactive mode with the
// given prompt, printing to stdout.
func (app *App) RunNonInteractive(ctx context.Context, output io.Writer, prompt, largeModel, smallModel string, hideSpinner bool) error {
@@ -87,18 +87,51 @@ crush --yolo
crush --data-dir /path/to/custom/.crush
`,
RunE: func(cmd *cobra.Command, args []string) error {
- app, err := setupAppWithProgressBar(cmd)
+ hostURL, err := server.ParseHostURL(clientHost)
if err != nil {
+ return fmt.Errorf("invalid host URL: %v", err)
+ }
+
+ if err := ensureServer(cmd, hostURL); err != nil {
return err
}
- defer app.Shutdown()
+
+ appInstance, err := setupAppWithProgressBar(cmd)
+ if err != nil {
+ return err
+ }
+ defer appInstance.Shutdown()
+
+ // Register the workspace with the server so it tracks active
+ // clients and auto-shuts down when the last one exits.
+ cwd, _ := ResolveCwd(cmd)
+ dataDir, _ := cmd.Flags().GetString("data-dir")
+ debug, _ := cmd.Flags().GetBool("debug")
+ yolo, _ := cmd.Flags().GetBool("yolo")
+
+ c, err := client.NewClient(cwd, hostURL.Scheme, hostURL.Host)
+ if err != nil {
+ return fmt.Errorf("failed to create client: %v", err)
+ }
+
+ ws, err := c.CreateWorkspace(cmd.Context(), proto.Workspace{
+ Path: cwd,
+ DataDir: dataDir,
+ Debug: debug,
+ YOLO: yolo,
+ Version: version.Version,
+ })
+ if err != nil {
+ return fmt.Errorf("failed to register workspace: %v", err)
+ }
+ defer func() { _ = c.DeleteWorkspace(cmd.Context(), ws.ID) }()
event.AppInitialized()
// Set up the TUI.
var env uv.Environ = os.Environ()
- com := common.DefaultCommon(app)
+ com := common.DefaultCommon(appInstance)
model := ui.New(com)
program := tea.NewProgram(
@@ -107,7 +140,7 @@ crush --data-dir /path/to/custom/.crush
tea.WithContext(cmd.Context()),
tea.WithFilter(ui.MouseEventFilter), // Filter mouse events based on focus state
)
- go app.Subscribe(program)
+ go appInstance.Subscribe(program)
if _, err := program.Run(); err != nil {
event.Error(err)
@@ -283,7 +316,8 @@ func setupClientApp(cmd *cobra.Command, hostURL *url.URL) (*client.Client, *prot
}
// ensureServer auto-starts a detached server if the socket file does not
-// exist.
+// exist. When connecting to an existing server, it waits for the health
+// endpoint to respond.
func ensureServer(cmd *cobra.Command, hostURL *url.URL) error {
switch hostURL.Scheme {
case "unix", "npipe":
@@ -308,9 +342,8 @@ func ensureServer(cmd *cobra.Command, hostURL *url.URL) error {
if err != nil {
return fmt.Errorf("failed to initialize crush server: %v", err)
}
- default:
- // TCP: assume server is already running.
}
+
return nil
}
@@ -16,6 +16,7 @@ type Workspace struct {
YOLO bool `json:"yolo,omitempty"`
Debug bool `json:"debug,omitempty"`
DataDir string `json:"data_dir,omitempty"`
+ Version string `json:"version,omitempty"`
Config *config.Config `json:"config,omitempty"`
Env []string `json:"env,omitempty"`
}
@@ -16,6 +16,7 @@ import (
"github.com/charmbracelet/crush/internal/permission"
"github.com/charmbracelet/crush/internal/proto"
"github.com/charmbracelet/crush/internal/session"
+ "github.com/charmbracelet/crush/internal/ui/util"
"github.com/charmbracelet/crush/internal/version"
"github.com/google/uuid"
)
@@ -541,6 +542,16 @@ func (c *controllerV1) handleDeleteWorkspaces(w http.ResponseWriter, r *http.Req
ws.App.Shutdown()
}
c.workspaces.Del(id)
+
+ // When the last workspace is removed, shut down the server.
+ if c.workspaces.Len() == 0 {
+ slog.Info("Last workspace removed, shutting down server...")
+ go func() {
+ if err := c.Shutdown(context.Background()); err != nil {
+ slog.Error("Failed to shutdown server", "error", err)
+ }
+ }()
+ }
}
func (c *controllerV1) handleGetWorkspace(w http.ResponseWriter, r *http.Request) {
@@ -618,6 +629,18 @@ func (c *controllerV1) handlePostWorkspaces(w http.ResponseWriter, r *http.Reque
}
c.workspaces.Set(id, ws)
+
+ if args.Version != "" && args.Version != version.Version {
+ slog.Warn("Client/server version mismatch",
+ "client", args.Version,
+ "server", version.Version,
+ )
+ appWorkspace.SendEvent(util.NewWarnMsg(fmt.Sprintf(
+ "Server version %q differs from client version %q. Consider restarting the server.",
+ version.Version, args.Version,
+ )))
+ }
+
jsonEncode(w, proto.Workspace{
ID: id,
Path: args.Path,