@@ -147,7 +147,9 @@ struct Cli {
147147
148148 /// Serve the editor to a browser over a local HTTP/WebSocket bridge.
149149 /// Optionally give a bind address (default 127.0.0.1:8137). Any FILES are
150- /// opened in the served editor.
150+ /// opened in the served editor. This also runs the session daemon, so
151+ /// `fresh -a` in the same directory attaches a terminal to the very same
152+ /// editor.
151153 #[ cfg( feature = "web" ) ]
152154 #[ arg( long, value_name = "ADDR" , num_args = 0 ..=1 , default_missing_value = "127.0.0.1:8137" ) ]
153155 web : Option < String > ,
@@ -2778,19 +2780,48 @@ fn kill_session_command(session: Option<&str>, args: &Args) -> AnyhowResult<()>
27782780}
27792781
27802782/// Run as a daemon server
2781- fn run_server_command ( args : & Args ) -> AnyhowResult < ( ) > {
2783+ /// Run the session daemon in this process.
2784+ ///
2785+ /// Two entry points land here, and they run the SAME daemon:
2786+ ///
2787+ /// - `fresh --server` — the detached process a `fresh -a` client spawns
2788+ /// when no daemon is live for the working directory. Chatty on stderr,
2789+ /// which the spawner has already redirected into the session log.
2790+ /// - `fresh --web [ADDR]` (`web_addr = Some`) — the same daemon in the
2791+ /// foreground, additionally serving the web UI. Same session, same
2792+ /// sockets: `fresh -a` in this working directory attaches a terminal to
2793+ /// the very editor the browser is looking at, and closing either one
2794+ /// leaves the session (and the other) running.
2795+ ///
2796+ /// `web_addr` also picks the console posture. The detached daemon logs at
2797+ /// `debug` into its log file; a foreground `--web` would flood the user's
2798+ /// terminal with it, so it logs at `warn` and skips the boot chatter.
2799+ /// `RUST_LOG` still overrides either.
2800+ fn run_server_command ( args : & Args , web_addr : Option < String > ) -> AnyhowResult < ( ) > {
27822801 use fresh:: server:: { EditorServer , EditorServerConfig } ;
27832802
2803+ let detached = web_addr. is_none ( ) ;
2804+ // Boot progress, useful in the daemon's log file but noise in a terminal.
2805+ macro_rules! boot {
2806+ ( $( $arg: tt) * ) => {
2807+ if detached {
2808+ eprintln!( $( $arg) * ) ;
2809+ }
2810+ } ;
2811+ }
2812+
27842813 // Initialize tracing to stderr (will go to log file when spawned detached)
27852814 use tracing_subscriber:: { fmt, EnvFilter } ;
2786- let filter = EnvFilter :: try_from_default_env ( ) . unwrap_or_else ( |_| EnvFilter :: new ( "debug" ) ) ;
2815+ let default_level = if detached { "debug" } else { "warn" } ;
2816+ let filter =
2817+ EnvFilter :: try_from_default_env ( ) . unwrap_or_else ( |_| EnvFilter :: new ( default_level) ) ;
27872818 fmt ( )
27882819 . with_env_filter ( filter)
27892820 . with_writer ( std:: io:: stderr)
27902821 . with_ansi ( false )
27912822 . init ( ) ;
27922823
2793- eprintln ! (
2824+ boot ! (
27942825 "[server] Starting server process for session {:?}" ,
27952826 args. session_name
27962827 ) ;
@@ -2828,7 +2859,7 @@ fn run_server_command(args: &Args) -> AnyhowResult<()> {
28282859 None => std:: env:: current_dir ( ) ?,
28292860 } ;
28302861 let config_dir = std:: env:: current_dir ( ) ?;
2831- eprintln ! ( "[server] Working directory: {:?}" , working_dir) ;
2862+ boot ! ( "[server] Working directory: {:?}" , working_dir) ;
28322863
28332864 let dir_context = fresh:: config_io:: DirectoryContext :: from_system ( ) ?;
28342865
@@ -2851,15 +2882,33 @@ fn run_server_command(args: &Args) -> AnyhowResult<()> {
28512882 ) ;
28522883
28532884 // Load editor config
2854- eprintln ! ( "[server] Loading editor config..." ) ;
2855- let editor_config = if let Some ( config_path) = & args. config {
2885+ boot ! ( "[server] Loading editor config..." ) ;
2886+ let mut editor_config = if let Some ( config_path) = & args. config {
28562887 config:: Config :: load_from_file ( config_path) ?
28572888 } else {
28582889 config:: Config :: load_with_layers ( & dir_context, & config_dir)
28592890 } ;
2860- eprintln ! ( "[server] Editor config loaded" ) ;
2891+ boot ! ( "[server] Editor config loaded" ) ;
2892+ // Cell-level (TUI) animations would stream to the browser as bursts of
2893+ // frame diffs, on top of the CSS-level motion the web frontend does for
2894+ // itself — the same reason the standalone bridge forces them off. One
2895+ // editor serves both transports here, so the whole session (attached
2896+ // terminals included) goes without them while `--web` is serving.
2897+ if web_addr. is_some ( ) {
2898+ editor_config. editor . animations = false ;
2899+ }
28612900 editor_config. apply_runtime_flags ( ) ;
28622901
2902+ // `--web` has no client to send an `OpenFiles` after the handshake, so the
2903+ // files from its own command line ride in on the config and are queued once
2904+ // the editor is up. The detached daemon leaves this empty — its `fresh -a`
2905+ // client sends the list itself, `--wait` and all.
2906+ let startup_files = if web_addr. is_some ( ) {
2907+ build_file_requests ( & args. files , & working_dir)
2908+ } else {
2909+ Vec :: new ( )
2910+ } ;
2911+
28632912 let session_keepalive: Option < Box < dyn std:: any:: Any + Send > > =
28642913 remote_session. map ( |rs| Box :: new ( rs) as Box < dyn std:: any:: Any + Send > ) ;
28652914 let startup_authority = if remote_info. is_some ( ) {
@@ -2880,31 +2929,77 @@ fn run_server_command(args: &Args) -> AnyhowResult<()> {
28802929 workspace_trust,
28812930 env_provider,
28822931 session_keepalive,
2932+ startup_files,
2933+ #[ cfg( feature = "web" ) ]
2934+ web_addr : web_addr. clone ( ) ,
28832935 } ;
28842936
2885- eprintln ! ( "[server] Creating EditorServer..." ) ;
2937+ boot ! ( "[server] Creating EditorServer..." ) ;
28862938 let mut server = match EditorServer :: new ( config) {
28872939 Ok ( s) => {
2888- eprintln ! ( "[server] EditorServer created successfully" ) ;
2940+ boot ! ( "[server] EditorServer created successfully" ) ;
28892941 s
28902942 }
28912943 Err ( e) => {
2892- eprintln ! ( "[server] EditorServer::new failed: {:?}" , e) ;
2944+ boot ! ( "[server] EditorServer::new failed: {:?}" , e) ;
28932945 return Err ( e. into ( ) ) ;
28942946 }
28952947 } ;
28962948
2897- eprintln ! ( "[server] Server ready at {:?}" , server. socket_paths( ) ) ;
2949+ boot ! ( "[server] Server ready at {:?}" , server. socket_paths( ) ) ;
28982950 tracing:: info!( "Editor server started at {:?}" , server. socket_paths( ) ) ;
2951+ // The web banner is the foreground path's only startup output: the URL to
2952+ // open, and the `fresh -a` invocation that attaches a terminal to the same
2953+ // session. Sessions are keyed by working directory unless named, which is
2954+ // exactly how `-a` resolves them.
2955+ if let Some ( addr) = & web_addr {
2956+ eprintln ! ( "fresh web bridge on http://{addr} (WS push on /ws)" ) ;
2957+ match & args. session_name {
2958+ Some ( name) => eprintln ! ( "attach a terminal to this session: fresh -a {name}" ) ,
2959+ None => eprintln ! (
2960+ "attach a terminal to this session: fresh -a (in {})" ,
2961+ working_dir. display( )
2962+ ) ,
2963+ }
2964+ }
28992965
29002966 // Run the server (blocking)
2901- eprintln ! ( "[server] Entering main loop..." ) ;
2967+ boot ! ( "[server] Entering main loop..." ) ;
29022968 server. run ( ) ?;
29032969
2904- eprintln ! ( "[server] Server shutting down" ) ;
2970+ boot ! ( "[server] Server shutting down" ) ;
29052971 Ok ( ( ) )
29062972}
29072973
2974+ /// `fresh --web [ADDR] [FILES…]` — run the session daemon in the foreground
2975+ /// with the web UI bridge hosted inside it.
2976+ ///
2977+ /// The daemon owns the editor; the browser and every `fresh -a` terminal are
2978+ /// transports onto that one editor, so a change made in the browser shows up in
2979+ /// an attached terminal (and the other way round), and closing either leaves the
2980+ /// session running. Because this binds the ordinary session sockets, a session
2981+ /// that is already live here would be shadowed rather than shared — refuse
2982+ /// instead, and say what to do about it.
2983+ #[ cfg( feature = "web" ) ]
2984+ fn run_web_command ( args : & Args , addr : & str ) -> AnyhowResult < ( ) > {
2985+ let socket_paths = resolve_session ( args. session_name . as_deref ( ) ) ?;
2986+ // A daemon that died without unlinking its sockets must not block the bind.
2987+ socket_paths. cleanup_if_stale ( ) ;
2988+ if socket_paths. is_server_alive ( ) {
2989+ // A user error, not a bug — printed clean and exited, the same way
2990+ // `main` special-cases SSH connection failures. Returning an
2991+ // `anyhow::Error` here would hand the user a backtrace instead
2992+ // (`real_main` turns backtraces on so genuine crashes are diagnosable).
2993+ eprintln ! (
2994+ "Error: a fresh session is already running here — `fresh -a` attaches a terminal to it."
2995+ ) ;
2996+ eprintln ! ( "To serve a separate session over the web, give it a name:" ) ;
2997+ eprintln ! ( " fresh --web {addr} --session-name NAME" ) ;
2998+ std:: process:: exit ( 1 ) ;
2999+ }
3000+ run_server_command ( args, Some ( addr. to_string ( ) ) )
3001+ }
3002+
29083003/// Resolve a session name to socket paths.
29093004///
29103005/// When `session_name` is `None`, uses the current working directory.
@@ -3874,7 +3969,7 @@ fn run_if_subcommand(
38743969 return Some ( kill_session_command ( session. as_deref ( ) , args) ) ;
38753970 }
38763971 if args. server {
3877- return Some ( run_server_command ( args) ) ;
3972+ return Some ( run_server_command ( args, None ) ) ;
38783973 }
38793974 if let Some ( ( session_name, files, wait) ) = & args. open_files_in_session {
38803975 return Some ( run_open_files_command (
@@ -3886,10 +3981,14 @@ fn run_if_subcommand(
38863981 if args. attach {
38873982 return Some ( run_attach_command ( args) ) ;
38883983 }
3984+ // `--web` runs the session daemon in the foreground with the web bridge
3985+ // hosted inside it, so the browser and any `fresh -a` terminal share one
3986+ // editor. It is NOT a separate editor process: the daemon binds the usual
3987+ // session sockets for this working directory (or `--session NAME`) first,
3988+ // so a session that is already live is joined rather than shadowed.
38893989 #[ cfg( feature = "web" ) ]
38903990 if let Some ( addr) = & args. web {
3891- let files: Vec < PathBuf > = args. files . iter ( ) . map ( PathBuf :: from) . collect ( ) ;
3892- return Some ( fresh:: webui:: run ( addr, & files) ) ;
3991+ return Some ( run_web_command ( args, addr) ) ;
38933992 }
38943993 #[ cfg( feature = "gui" ) ]
38953994 if !console_available || args. gui {
0 commit comments