From 1b37d55bb6f00810032f9d0330750a89e5914ea0 Mon Sep 17 00:00:00 2001 From: Ayman Bagabas Date: Tue, 10 Mar 2026 22:04:32 +0300 Subject: [PATCH] feat: send server client version info --- internal/app/app.go | 9 ++++++++ internal/cmd/root.go | 47 ++++++++++++++++++++++++++++++++++------ internal/proto/proto.go | 1 + internal/server/proto.go | 23 ++++++++++++++++++++ 4 files changed, 73 insertions(+), 7 deletions(-) diff --git a/internal/app/app.go b/internal/app/app.go index a81cfeb9d88357ec80566513d00e7f3080f0ecd5..3c02a734c703079c364983ecf47dbe27564798bb 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -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 { diff --git a/internal/cmd/root.go b/internal/cmd/root.go index 11e2ccc377f6ecc5b8dfca39f1b84609528c92a0..bd71dba1f2a1e327978f41bf71588812f13bd34c 100644 --- a/internal/cmd/root.go +++ b/internal/cmd/root.go @@ -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 } diff --git a/internal/proto/proto.go b/internal/proto/proto.go index d7477b580a8a0027ac6af1874eec2a117f587901..25a0e409b308f1fd979f5f6bb7bd00619fad0b1e 100644 --- a/internal/proto/proto.go +++ b/internal/proto/proto.go @@ -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"` } diff --git a/internal/server/proto.go b/internal/server/proto.go index 588a485e362e07e67d4785c31101590c75320382..eca944db8e2473415594f2cb419afe82bb43296c 100644 --- a/internal/server/proto.go +++ b/internal/server/proto.go @@ -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,