Repository: Nukesor/pueue (language: rust)

Issue:
feature suggestion: --folow argument for `pueue add`
### A detailed description of the feature you would like to see added.

I would like to have a simple `--follow` flag to `add`. Running `pueue add --folow <CMD>` should act just like:
```bash
pueue add <CMD>
pueue follow <TASK_ID_FROM_ADD>
```
Basically, it adds the task and follows its output. Just like the separate commands, Ctrl+C during a `pueue add --folow` should stop following but continue running the task.

I am not quite sure what the behaviour should be if the task cannot be immediately started. Running separate add/follow will just block while waiting for the command to start. Maybe it should print a warning then continue waiting.

### Explain your usecase of the requested feature

This will help get instant feedback when there is an initialisation problem for long-running tasks. A lot of the time, I run `pueue add` but make simple mistakes in the command (e.g. typo, wrong working directory). The mistakes cause the task to fail immediately but this is not immediately visible.

At the moment, I have to remember to `pueue status` after every `add` to make sure that the command is running properly. It's easy to forget this step, go away, and come back to an error instead of the the results I waited for.

### Alternatives

One could make an alias, however this makes it harder to add more options to `add`. I guess this bash would work:
```bash
pueue-add-follow() {
  task=$(pueue add --print-task-id "$@")
  sleep 1 # XXX: race condition! hopefully, if it is not queued, it will start after one second.
  pueue --color=always status | grep '^\s*'"$task"' '  # XXX: hack: status query does not support filtering by ID
  pueue follow $task
}
```
but it's a little janky.
The comments show how difficult it is to mimic the behaviour, especially if we want user-friendly information about whether the new task is running or in a queue.

Myself, I just try to remember `pueue status` after every `pueue add`.

### Additional context

Thank you for the well-made project! :)


Discussion / hints:
Hey @katrinafyi :)

Thanks for the kind words and your detailed feature request.

I've thought about your request for a bit and I feel like your proposed alternative bash function should actually be the way to go.

### My reasoning

The normal usecase of how pueue is usually used (and what it's designed for) is to queue multiple long running commands that run sequentially to at least some degree. Having a `--follow` flag on `add` if the command might only be started hours in the future will be very confusing for some people (At least I find it confusing :D).

I see how that makes total sense if one has mostly short-lived commands or if the parallelization of a group is set to a very high number. However, that's not what I consider the 95% usecase.

### Regarding the script:

The race condition issue in your script is fixed in v4.0.0, for which there's already a [release candidate](https://github.com/Nukesor/pueue/releases/tag/v4.0.0-rc.1)

To wait for the task to enter the running state, the `pueue wait` subcommand can be used.
The script would then look something like this:

```sh
pueue-add-follow() {
  task_id=$(pueue add --print-task-id "$@")
  pueue wait --status running $task_id
  pueue follow $task_id
}
```

It also prints out state transitions with time stamps (even though the original state isn't printed. But I would be up for a feature request that prints the original states of all tasks that're waited for)
Would that be a more reasonable solution for you?
I guess I am the 5%, I use pueue as a more powerful `&` (indeed, with unbounded parallelism :)). It is pretty nice for this use case, as commands are detached from the parent shell and can accessed across shells.

I would still like to see this feature, as I feel like it's a small change that would patch up a big weakness in this use case. 

That said, I see how it could have confusing interactions with queued tasks. Maybe --follow could be made to imply --immediate? When looking through help for this option, I actually assumed --immediate would act like my --follow, but it does not :)

But it is up to you, using the function alias will not be too bad.
Hmm. Let me think about the `--follow` as only applicable in conjunction with `--immediate` for a bit. Sounds reasonable :)

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1809f506d..ee796eaa5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -91,6 +91,7 @@ Upon updating Pueue and restarting the daemon, the previous state will be wiped,
 - Added Windows service on Windows to allow a true daemon experience. [#344](https://github.com/Nukesor/pueue/issues/344) [#567](https://github.com/Nukesor/pueue/pull/567)
 - Add `queued_count` and `stashed_count` to callback template variables. This allows users to fire callbacks when whole groups are finished. [#578](https://github.com/Nukesor/pueue/issues/578)
 - Add new subcommand to set or unset environment variables for tasks. [#503](https://github.com/Nukesor/pueue/issues/503)
+- Add `add --follow` flag that may be called in combination with `--immediate` [#592](https://github.com/Nukesor/pueue/issues/592)
 
 ### Fixed
 
diff --git a/Justfile b/Justfile
index ba9ac5b85..384dbeb39 100644
--- a/Justfile
+++ b/Justfile
@@ -15,7 +15,7 @@ lint:
     just ensure-command cargo-nextest
     cargo fmt --all -- --check
     taplo format --check
-    cargo clippy --tests --workspace -- -D warnings
+    cargo clippy --tests --workspace --all -- -D warnings
 
 format:
     just ensure-command taplo
diff --git a/pueue/src/client/cli.rs b/pueue/src/client/cli.rs
index e443bf480..4d4f6c8b8 100644
--- a/pueue/src/client/cli.rs
+++ b/pueue/src/client/cli.rs
@@ -40,6 +40,15 @@ pub enum SubCommand {
         #[arg(name = "immediate", short, long, conflicts_with = "stashed")]
         start_immediately: bool,
 
+        /// Immediately follow a task, if it's started with --immediate.
+        #[arg(
+            name = "follow",
+            long,
+            requires = "immediate",
+            conflicts_with = "print_task_id"
+        )]
+        follow: bool,
+
         /// Create the task in Stashed state.
         ///
         /// Useful to avoid immediate execution if the queue is empty.
diff --git a/pueue/src/client/client.rs b/pueue/src/client/client.rs
index bef14f048..c9f6f058a 100644
--- a/pueue/src/client/client.rs
+++ b/pueue/src/client/client.rs
@@ -7,6 +7,7 @@ use clap::crate_version;
 use crossterm::tty::IsTty;
 use log::{error, warn};
 
+use pueue_lib::format::format_datetime;
 use pueue_lib::network::message::*;
 use pueue_lib::network::protocol::*;
 use pueue_lib::network::secret::read_shared_secret;
@@ -48,15 +49,15 @@ pub fn group_or_default(group: &Option<String>) -> String {
 /// If no parameters are given, it returns to the default group.
 pub fn selection_from_params(
     all: bool,
-    group: &Option<String>,
-    task_ids: &[usize],
+    group: Option<String>,
+    task_ids: Vec<usize>,
 ) -> TaskSelection {
     if all {
         TaskSelection::All
     } else if let Some(group) = group {
-        TaskSelection::Group(group.clone())
+        TaskSelection::Group(group)
     } else if !task_ids.is_empty() {
-        TaskSelection::TaskIds(task_ids.to_owned())
+        TaskSelection::TaskIds(task_ids)
     } else {
         TaskSelection::Group(PUEUE_DEFAULT_GROUP.into())
     }
@@ -150,12 +151,12 @@ impl Client {
     /// The command handling is split into "simple" and "complex" commands.
     pub async fn start(&mut self) -> Result<()> {
         // Return early, if the command has already been handled.
-        if self.handle_complex_command().await? {
+        if self.handle_complex_command(self.subcommand.clone()).await? {
             return Ok(());
         }
 
         // The handling of "generic" commands is encapsulated in this function.
-        self.handle_simple_command().await?;
+        self.handle_simple_command(self.subcommand.clone()).await?;
 
         Ok(())
     }
@@ -172,9 +173,120 @@ impl Client {
     /// This indicates that the client can now shut down.
     /// If `Ok(false)` is returned, the client will continue and handle the Subcommand in the
     /// [Client::handle_simple_command] function.
-    async fn handle_complex_command(&mut self) -> Result<bool> {
+    async fn handle_complex_command(&mut self, subcommand: SubCommand) -> Result<bool> {
         // This match handles all "complex" commands.
-        match &self.subcommand {
+        match subcommand {
+            SubCommand::Add {
+                mut command,
+                working_directory,
+                escape,
+                start_immediately,
+                stashed,
+                group,
+                delay_until,
+                dependencies,
+                priority,
+                label,
+                print_task_id,
+                follow,
+            } => {
+                // Either take the user-specified path or default to the current working directory.
+                let path = working_directory
+                    .as_ref()
+                    .map(|path| Ok(path.clone()))
+                    .unwrap_or_else(current_dir)?;
+
+                // The user can request to escape any special shell characters in all parameter strings before
+                // we concatenated them to a single string.
+                if escape {
+                    command = command
+                        .iter()
+                        .map(|parameter| shell_escape::escape(Cow::from(parameter)).into_owned())
+                        .collect();
+                }
+
+                // Add the message to the daemon.
+                let message = Message::Add(AddMessage {
+                    command: command.join(" "),
+                    path,
+                    // Catch the current environment for later injection into the task's process.
+                    envs: HashMap::from_iter(vars()),
+                    start_immediately,
+                    stashed,
+                    group: group_or_default(&group),
+                    enqueue_at: delay_until,
+                    dependencies,
+                    priority,
+                    label,
+                });
+                send_message(message, &mut self.stream).await?;
+
+                // Get the response from the daemon.
+                let response = receive_message(&mut self.stream).await?;
+
+                // Make sure the task has been added, otherwise handle the response and return.
+                let Message::AddedTask(AddedTaskMessage {
+                    task_id,
+                    enqueue_at,
+                    group_is_paused,
+                }) = response
+                else {
+                    self.handle_response(response)?;
+                    return Ok(true);
+                };
+
+                let mut output = if print_task_id {
+                    // Only print the task id if that was requested.
+                    format!("{task_id}")
+                } else if let Some(enqueue_at) = enqueue_at {
+                    let enqueue_at = format_datetime(&self.settings, &enqueue_at);
+                    format!("New task added (id {task_id}). It will be enqueued at {enqueue_at}")
+                } else {
+                    format!("New task added (id {task_id}).")
+                };
+
+                // Also notify the user if the task's group is paused
+                if !print_task_id && group_is_paused && !follow {
+                    output.push_str("\nThe group of this task is currently paused!");
+                }
+
+                // If we were to follow the task immediately, print the task info to `stderr` so
+                // that the actual log output may be piped into another command.
+                if follow {
+                    eprintln!("{output}");
+                } else {
+                    println!("{output}");
+                }
+
+                if follow {
+                    // If we're supposed to read the log files from the local system, we don't have to
+                    // do any communication with the daemon.
+                    // Thereby we handle this in a separate function.
+                    if self.settings.client.read_local_logs {
+                        local_follow(
+                            &mut self.stream,
+                            &self.settings.shared.pueue_directory(),
+                            Some(task_id),
+                            None,
+                        )
+                        .await?;
+                        return Ok(true);
+                    } else {
+                        // In case we need to follow the daemon, go ahead and overwrite
+                        // `self.subcommand` with a `Follow` subcommand.
+                        // Afterwards, return an `Ok(false)`, which signals the client that it
+                        // should continue processing `self.subcommand`.
+                        // This is pretty hacky. TODO: Refactor the whole client at some point.
+                        self.subcommand = SubCommand::Follow {
+                            task_id: Some(task_id),
+                            lines: None,
+                        };
+                        return Ok(false);
+                    }
+                }
+
+                Ok(true)
+            }
             SubCommand::Reset { force, groups } => {
                 // Get the current state and check if there're any running tasks.
                 // If there are, ask the user if they really want to reset the state.
@@ -184,7 +296,7 @@ impl Client {
                 let groups: Vec<String> = if groups.is_empty() {
                     state.groups.keys().cloned().collect()
                 } else {
-                    groups.clone()
+                    groups
                 };
 
                 // Check if there're any running tasks for that group
@@ -217,8 +329,8 @@ impl Client {
                 quiet,
                 status,
             } => {
-                let selection = selection_from_params(*all, group, task_ids);
-                wait(&mut self.stream, &self.style, selection, *quiet, status).await?;
+                let selection = selection_from_params(all, group, task_ids);
+                wait(&mut self.stream, &self.style, selection, quiet, status).await?;
                 Ok(true)
             }
             SubCommand::Restart {
@@ -232,18 +344,17 @@ impl Client {
                 edit,
             } => {
                 // `not_in_place` superseeds both other configs
-                let in_place =
-                    (self.settings.client.restart_in_place || *in_place) && !*not_in_place;
+                let in_place = (self.settings.client.restart_in_place || in_place) && !not_in_place;
                 restart(
                     &mut self.stream,
                     &self.settings,
-                    task_ids.clone(),
-                    *all_failed,
-                    failed_in_group.clone(),
-                    *start_immediately,
-                    *stashed,
+                    task_ids,
+                    all_failed,
+                    failed_in_group,
+                    start_immediately,
+                    stashed,
                     in_place,
-                    *edit,
+                    edit,
                 )
                 .await?;
                 Ok(true)
@@ -257,7 +368,7 @@ impl Client {
                         &mut self.stream,
                         &self.settings.shared.pueue_directory(),
                         task_id,
-                        *lines,
+                        lines,
                     )
                     .await?;
                     return Ok(true);
@@ -285,10 +396,10 @@ impl Client {
     ///
     /// The only exception is streaming of log output.
     /// In that case, we send one request and contine receiving until the stream shuts down.
-    async fn handle_simple_command(&mut self) -> Result<()> {
+    async fn handle_simple_command(&mut self, subcommand: SubCommand) -> Result<()> {
         // Create the message that should be sent to the daemon
         // depending on the given commandline options.
-        let message = self.get_message_from_opt()?;
+        let message = self.get_message_from_cmd(subcommand)?;
 
         // Create the message payload and send it to the daemon.
         send_message(message, &mut self.stream).await?;
@@ -384,58 +495,13 @@ impl Client {
     ///
     /// This function is pretty large, but it consists mostly of simple conversions
     /// of [SubCommand] variant to a [Message] variant.
-    fn get_message_from_opt(&self) -> Result<Message> {
-        Ok(match self.subcommand.clone() {
-            SubCommand::Add {
-                command,
-                working_directory,
-                escape,
-                start_immediately,
-                stashed,
-                group,
-                delay_until,
-                dependencies,
-                priority,
-                label,
-                print_task_id,
-            } => {
-                // Either take the user-specified path or default to the current working directory.
-                let path = working_directory
-                    .as_ref()
-                    .map(|path| Ok(path.clone()))
-                    .unwrap_or_else(current_dir)?;
-
-                let mut command = command.clone();
-                // The user can request to escape any special shell characters in all parameter strings before
-                // we concatenated them to a single string.
-                if escape {
-                    command = command
-                        .iter()
-                        .map(|parameter| shell_escape::escape(Cow::from(parameter)).into_owned())
-                        .collect();
-                }
-
-                AddMessage {
-                    command: command.join(" "),
-                    path,
-                    // Catch the current environment for later injection into the task's process.
-                    envs: HashMap::from_iter(vars()),
-                    start_immediately,
-                    stashed,
-                    group: group_or_default(&group),
-                    enqueue_at: delay_until,
-                    dependencies,
-                    priority,
-                    label,
-                    print_task_id,
-                }
-                .into()
-            }
+    fn get_message_from_cmd(&self, subcommand: SubCommand) -> Result<Message> {
+        Ok(match subcommand {
             SubCommand::Remove { task_ids } => {
                 if self.settings.client.show_confirmation_questions {
                     self.handle_user_confirmation("remove", &task_ids)?;
                 }
-                Message::Remove(task_ids.clone())
+                Message::Remove(task_ids)
             }
             SubCommand::Stash {
                 task_ids,
@@ -443,7 +509,7 @@ impl Client {
                 all,
                 delay_until,
             } => {
-                let selection = selection_from_params(all, &group, &task_ids);
+                let selection = selection_from_params(all, group, task_ids);
                 StashMessage {
                     tasks: selection,
                     enqueue_at: delay_until,
@@ -464,7 +530,7 @@ impl Client {
                 all,
                 delay_until,
             } => {
-                let selection = selection_from_params(all, &group, &task_ids);
+                let selection = selection_from_params(all, group, task_ids);
                 EnqueueMessage {
                     tasks: selection,
                     enqueue_at: delay_until,
@@ -477,7 +543,7 @@ impl Client {
                 all,
                 ..
             } => StartMessage {
-                tasks: selection_from_params(all, &group, &task_ids),
+                tasks: selection_from_params(all, group, task_ids),
             }
             .into(),
             SubCommand::Pause {
@@ -487,7 +553,7 @@ impl Client {
                 all,
                 ..
             } => PauseMessage {
-                tasks: selection_from_params(all, &group, &task_ids),
+                tasks: selection_from_params(all, group, task_ids),
                 wait,
             }
             .into(),
@@ -502,16 +568,12 @@ impl Client {
                     self.handle_user_confirmation("kill", &task_ids)?;
                 }
                 KillMessage {
-                    tasks: selection_from_params(all, &group, &task_ids),
+                    tasks: selection_from_params(all, group, task_ids),
                     signal,
                 }
                 .into()
             }
-            SubCommand::Send { task_id, input } => SendMessage {
-                task_id,
-                input: input.clone(),
-            }
-            .into(),
+            SubCommand::Send { task_id, input } => SendMessage { task_id, input }.into(),
             SubCommand::Env { cmd } => Message::from(match cmd {
                 EnvCommand::Set {
                     task_id,
@@ -544,7 +606,7 @@ impl Client {
                 ..
             } => {
                 let lines = determine_log_line_amount(full, &lines);
-                let selection = selection_from_params(all, &group, &task_ids);
+                let selection = selection_from_params(all, group, task_ids);
 
                 let message = LogRequestMessage {
                     tasks: selection,
@@ -590,6 +652,7 @@ impl Client {
                 }
                 None => GroupMessage::List.into(),
             },
+            SubCommand::Add { .. } => bail!("Add has to be handled earlier"),
             SubCommand::FormatStatus { .. } => bail!("FormatStatus has to be handled earlier"),
             SubCommand::Completions { .. } => bail!("Completions have to be handled earlier"),
             SubCommand::Restart { .. } => bail!("Restarts have to be handled earlier"),
diff --git a/pueue/src/client/commands/edit.rs b/pueue/src/client/commands/edit.rs
index e6214ceda..7bff9e936 100644
--- a/pueue/src/client/commands/edit.rs
+++ b/pueue/src/client/commands/edit.rs
@@ -23,10 +23,10 @@ use pueue_lib::process_helper::compile_shell_command;
 pub async fn edit(
     stream: &mut GenericStream,
     settings: &Settings,
-    task_ids: &[usize],
+    task_ids: Vec<usize>,
 ) -> Result<Message> {
     // Request the data to edit from the server and issue a task-lock while doing so.
-    let init_message = Message::EditRequest(task_ids.to_vec());
+    let init_message = Message::EditRequest(task_ids);
     send_message(init_message, stream).await?;
 
     let init_response = receive_message(stream).await?;
diff --git a/pueue/src/client/commands/local_follow.rs b/pueue/src/client/commands/local_follow.rs
index 609171d55..cd9c547c3 100644
--- a/pueue/src/client/commands/local_follow.rs
+++ b/pueue/src/client/commands/local_follow.rs
@@ -17,11 +17,11 @@ use crate::client::display::follow_local_task_logs;
 pub async fn local_follow(
     stream: &mut GenericStream,
     pueue_directory: &Path,
-    task_id: &Option<usize>,
+    task_id: Option<usize>,
     lines: Option<usize>,
 ) -> Result<()> {
     let task_id = match task_id {
-        Some(task_id) => *task_id,
+        Some(task_id) => task_id,
         None => {
             // The user didn't provide a task id.
             // Check whether we can find a single running task to follow.
diff --git a/pueue/src/client/commands/restart.rs b/pueue/src/client/commands/restart.rs
index c9083fc18..6ff9ca645 100644
--- a/pueue/src/client/commands/restart.rs
+++ b/pueue/src/client/commands/restart.rs
@@ -143,7 +143,6 @@ pub async fn restart(
             dependencies: Vec::new(),
             priority: Some(task.priority),
             label: task.label,
-            print_task_id: false,
         };
 
         // Send the cloned task to the daemon and abort on any failure messages.
diff --git a/pueue/src/client/commands/wait.rs b/pueue/src/client/commands/wait.rs
index 5be63f575..b3818c1cf 100644
--- a/pueue/src/client/commands/wait.rs
+++ b/pueue/src/client/commands/wait.rs
@@ -42,7 +42,7 @@ pub async fn wait(
     style: &OutputStyle,
     selection: TaskSelection,
     quiet: bool,
-    target_status: &Option<WaitTargetStatus>,
+    target_status: Option<WaitTargetStatus>,
 ) -> Result<()> {
     let mut first_run = true;
     // Create a list of tracked tasks.
diff --git a/pueue/src/client/display/log/mod.rs b/pueue/src/client/display/log/mod.rs
index 9b8accb76..b3203c97d 100644
--- a/pueue/src/client/display/log/mod.rs
+++ b/pueue/src/client/display/log/mod.rs
@@ -70,7 +70,7 @@ pub fn print_logs(
         return;
     }
 
-    let selection = selection_from_params(*all, group, task_ids);
+    let selection = selection_from_params(*all, group.clone(), task_ids.clone());
     if task_logs.is_empty() {
         match selection {
             TaskSelection::TaskIds(_) => {
diff --git a/pueue/src/daemon/network/message_handler/add.rs b/pueue/src/daemon/network/message_handler/add.rs
index f7377cc50..ceb266cd3 100644
--- a/pueue/src/daemon/network/message_handler/add.rs
+++ b/pueue/src/daemon/network/message_handler/add.rs
@@ -79,20 +79,10 @@ pub fn add_task(settings: &Settings, state: &SharedState, message: AddMessage) -
         process_handler::start::start(settings, &mut state, TaskSelection::TaskIds(vec![task_id]));
     }
 
-    // Create the customized response for the client.
-    let mut response = if message.print_task_id {
-        task_id.to_string()
-    } else if let Some(enqueue_at) = message.enqueue_at {
-        let enqueue_at = format_datetime(settings, &enqueue_at);
-        format!("New task added (id {task_id}). It will be enqueued at {enqueue_at}")
-    } else {
-        format!("New task added (id {task_id}).")
-    };
-
-    // Notify the user if the task's group is paused
-    if !message.print_task_id && group_is_paused {
-        response.push_str("\nThe group of this task is currently paused!")
+    AddedTaskMessage {
+        task_id,
+        enqueue_at: message.enqueue_at,
+        group_is_paused,
     }
-
-    create_success_message(response)
+    .into()
 }
diff --git a/pueue/src/daemon/network/message_handler/enqueue.rs b/pueue/src/daemon/network/message_handler/enqueue.rs
index 03e77f7bd..2b3efa7b0 100644
--- a/pueue/src/daemon/network/message_handler/enqueue.rs
+++ b/pueue/src/daemon/network/message_handler/enqueue.rs
@@ -1,5 +1,6 @@
 use chrono::Local;
 use pueue_lib::{
+    format::format_datetime,
     network::message::*,
     settings::Settings,
     state::SharedState,
@@ -9,8 +10,6 @@ use pueue_lib::{
 
 use crate::daemon::network::response_helper::*;
 
-use super::format_datetime;
-
 /// Invoked when calling `pueue enqueue`.
 /// Enqueue specific stashed tasks.
 pub fn enqueue(settings: &Settings, state: &SharedState, message: EnqueueMessage) -> Message {
diff --git a/pueue/src/daemon/network/message_handler/mod.rs b/pueue/src/daemon/network/message_handler/mod.rs
index 0bf39eee3..50f3e52ca 100644
--- a/pueue/src/daemon/network/message_handler/mod.rs
+++ b/pueue/src/daemon/network/message_handler/mod.rs
@@ -1,6 +1,5 @@
 use std::fmt::Display;
 
-use chrono::{DateTime, Local};
 use pueue_lib::failure_msg;
 use pueue_lib::network::message::*;
 use pueue_lib::settings::Settings;
@@ -61,16 +60,6 @@ fn get_status(state: &SharedState) -> Message {
     Message::StatusResponse(Box::new(state))
 }
 
-// If the enqueue at time is today, only show the time. Otherwise, include the date.
-fn format_datetime(settings: &Settings, enqueue_at: &DateTime<Local>) -> String {
-    let format_string = if enqueue_at.date_naive() == Local::now().date_naive() {
-        &settings.client.status_time_format
-    } else {
-        &settings.client.status_datetime_format
-    };
-    enqueue_at.format(format_string).to_string()
-}
-
 fn ok_or_failure_message<T, E: Display>(result: Result<T, E>) -> Result<T, Message> {
     match result {
         Ok(inner) => Ok(inner),
diff --git a/pueue/src/daemon/network/message_handler/stash.rs b/pueue/src/daemon/network/message_handler/stash.rs
index 461e3d04e..d63508cf5 100644
--- a/pueue/src/daemon/network/message_handler/stash.rs
+++ b/pueue/src/daemon/network/message_handler/stash.rs
@@ -1,11 +1,10 @@
 use pueue_lib::{
-    network::message::*, settings::Settings, state::SharedState, success_msg, task::TaskStatus,
+    format::format_datetime, network::message::*, settings::Settings, state::SharedState,
+    success_msg, task::TaskStatus,
 };
 
 use crate::daemon::network::response_helper::*;
 
-use super::format_datetime;
-
 /// Invoked when calling `pueue stash`.
 /// Stash specific queued tasks.
 /// They won't be executed until they're enqueued or explicitly started.
diff --git a/pueue_lib/src/format.rs b/pueue_lib/src/format.rs
new file mode 100644
index 000000000..f7a44f6b9
--- /dev/null
+++ b/pueue_lib/src/format.rs
@@ -0,0 +1,13 @@
+use chrono::{DateTime, Local};
+
+use crate::settings::Settings;
+
+// If the enqueue at time is today, only show the time. Otherwise, include the date.
+pub fn format_datetime(settings: &Settings, enqueue_at: &DateTime<Local>) -> String {
+    let format_string = if enqueue_at.date_naive() == Local::now().date_naive() {
+        &settings.client.status_time_format
+    } else {
+        &settings.client.status_datetime_format
+    };
+    enqueue_at.format(format_string).to_string()
+}
diff --git a/pueue_lib/src/lib.rs b/pueue_lib/src/lib.rs
index d5f5403da..5c502bb58 100644
--- a/pueue_lib/src/lib.rs
+++ b/pueue_lib/src/lib.rs
@@ -8,6 +8,8 @@ pub mod aliasing;
 pub mod children;
 /// Pueue lib's own Error implementation.
 pub mod error;
+/// Formatting methods for several data types.
+pub mod format;
 /// Helper classes to read and write log files of Pueue's tasks.
 pub mod log;
 pub mod network;
diff --git a/pueue_lib/src/network/message.rs b/pueue_lib/src/network/message.rs
index 2df6d9d5b..149eaf862 100644
--- a/pueue_lib/src/network/message.rs
+++ b/pueue_lib/src/network/message.rs
@@ -42,6 +42,7 @@ macro_rules! failure_msg {
 #[derive(PartialEq, Eq, Clone, Debug, Deserialize, Serialize)]
 pub enum Message {
     Add(AddMessage),
+    AddedTask(AddedTaskMessage),
     Remove(Vec<usize>),
     Switch(SwitchMessage),
     Stash(StashMessage),
@@ -94,6 +95,12 @@ pub enum Message {
     Parallel(ParallelMessage),
 }
 
+impl Message {
+    pub fn response_success(&self) -> bool {
+        matches!(&self, Message::AddedTask(_) | Message::Success(_))
+    }
+}
+
 /// This enum is used to express a selection of tasks.
 /// As commands can be executed on various sets of tasks, we need some kind of datastructure to
 /// explicitly and unambiguously specify the selection.
@@ -116,7 +123,6 @@ pub struct AddMessage {
     pub dependencies: Vec<usize>,
     pub priority: Option<i32>,
     pub label: Option<String>,
-    pub print_task_id: bool,
 }
 
 /// We use a custom `Debug` implementation for [AddMessage], as the `envs` field just has
@@ -136,13 +142,20 @@ impl std::fmt::Debug for AddMessage {
             .field("enqueue_at", &self.enqueue_at)
             .field("dependencies", &self.dependencies)
             .field("label", &self.label)
-            .field("print_task_id", &self.print_task_id)
             .finish()
     }
 }
 
 impl_into_message!(AddMessage, Message::Add);
 
+#[derive(PartialEq, Eq, Clone, Debug, Default, Deserialize, Serialize)]
+pub struct AddedTaskMessage {
+    pub task_id: usize,
+    pub enqueue_at: Option<DateTime<Local>>,
+    pub group_is_paused: bool,
+}
+impl_into_message!(AddedTaskMessage, Message::AddedTask);
+
 #[derive(PartialEq, Eq, Clone, Debug, Deserialize, Serialize)]
 pub struct SwitchMessage {
     pub task_id_1: usize,


Test patch:
diff --git a/pueue/tests/client/integration/follow.rs b/pueue/tests/client/integration/follow.rs
index 96481c5eb..d6a518df7 100644
--- a/pueue/tests/client/integration/follow.rs
+++ b/pueue/tests/client/integration/follow.rs
@@ -40,6 +40,26 @@ async fn default(#[case] read_local_logs: bool) -> Result<()> {
     Ok(())
 }
 
+/// Test that a task is immediately followed when added with `--immediate --follow`.
+#[rstest]
+#[case(true)]
+#[case(false)]
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn follow_on_immediate_add(#[case] read_local_logs: bool) -> Result<()> {
+    let mut daemon = daemon().await?;
+    set_read_local_logs(&mut daemon, read_local_logs)?;
+    let shared = &daemon.settings.shared;
+
+    let output = run_client_command(
+        shared,
+        &["add", "--immediate", "--follow", "sleep 1 && echo test"],
+    )?;
+
+    assert_snapshot_matches_stdout("follow__default", output.stdout)?;
+
+    Ok(())
+}
+
 /// Test that the remote `follow` command works, if one specifies to only show the last few lines
 /// of recent output.
 #[rstest]
diff --git a/pueue/tests/helper/asserts.rs b/pueue/tests/helper/asserts.rs
index 1aa578e1e..03315a59e 100644
--- a/pueue/tests/helper/asserts.rs
+++ b/pueue/tests/helper/asserts.rs
@@ -10,10 +10,9 @@ use super::{get_state, send_message};
 
 /// Assert that a message is a successful message.
 pub fn assert_success(message: Message) {
-    assert_matches!(
-        message,
-        Message::Success(_),
-        "Expected to get SuccessMessage, got {message:?}",
+    assert!(
+        message.response_success(),
+        "Expected to get successful message, got {message:?}",
     );
 }
 
diff --git a/pueue/tests/helper/task.rs b/pueue/tests/helper/task.rs
index 8b76c1ecf..9055f59e4 100644
--- a/pueue/tests/helper/task.rs
+++ b/pueue/tests/helper/task.rs
@@ -24,7 +24,6 @@ pub fn create_add_message<C: ToString>(shared: &Shared, command: C) -> AddMessag
         dependencies: Vec::new(),
         priority: None,
         label: None,
-        print_task_id: false,
     }
 }