Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions cmux-tui/crates/acpmux/spec/acpmux-protocol.json
Original file line number Diff line number Diff line change
Expand Up @@ -678,6 +678,18 @@
"params": true,
"result": true
},
{
"kind": "request",
"name": "_acpmux/task_new",
"params": true,
"result": true
},
{
"kind": "request",
"name": "_acpmux/tasks",
"params": true,
"result": true
},
{
"kind": "request",
"name": "_acpmux/media_open",
Expand Down
26 changes: 5 additions & 21 deletions cmux-tui/crates/acpmux/src/cli/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
use clap::{Args, Parser, Subcommand};
use std::path::PathBuf;

pub use crate::cli::task::BriefsCmd;

#[derive(Parser)]
#[command(name = "acpmux", version = concat!(env!("CARGO_PKG_VERSION"), " (", env!("ACPMUX_BUILD"), ")"), about = "tmux for coding-agent harnesses: Claude Code, Codex, OpenCode, pi, Gemini", long_about = None, after_help = "Agents: `acpmux guide` (also --guide, --skill) prints the full guide for driving acpmux from a script or another agent.")]
pub struct Cli {
Expand All @@ -23,6 +25,9 @@ pub enum Command {
Dispatch(DispatchArgs),
#[command(subcommand)]
Briefs(BriefsCmd),
/// Give a session here a task, end one with its result in one call, list a chat's tasks.
#[command(subcommand)]
Task(crate::cli::task::TaskCmd),
/// Create a session and optionally send a first prompt. `run` is the
/// same with --quiet: it prints only the final reply. With --worktree,
/// --prompt-file, --report-to and --group it starts an agent on a task
Expand Down Expand Up @@ -511,27 +516,6 @@ pub struct DispatchArgs {
pub cwd: Option<PathBuf>,
}

#[derive(Subcommand)]
pub enum BriefsCmd {
#[command(alias = "list")]
Ls {
#[arg(long)]
status: Option<String>,
#[arg(long, default_value_t = 100)]
limit: usize,
},
Steer {
brief_id: String,
text: Vec<String>,
},
Report {
brief_id: String,
summary: Vec<String>,
#[arg(long, default_value = "reported")]
status: String,
},
}

#[derive(Subcommand)]
pub enum HarnessCmd {
/// Every harness with its kind, source file and problems.
Expand Down
7 changes: 7 additions & 0 deletions cmux-tui/crates/acpmux/src/cli/entry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,13 @@ async fn async_main(args: Vec<OsString>, invocation: Invocation) -> Result<()> {
})
.await
}
Some(Command::Task(cmd)) => {
let json_out = cli.json;
match crate::cli::task::run(cmd, json_out).await {
Ok(()) => Ok(()),
Err(e) => errors::exit_with(&e, json_out),
}
}
Some(Command::Forum(cmd)) => {
let json_out = cli.json;
match crate::cli::forum::run(cmd, json_out).await {
Expand Down
1 change: 1 addition & 0 deletions cmux-tui/crates/acpmux/src/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,5 +33,6 @@ pub mod session_folder;
pub mod session_limits;
pub mod shutdown;
pub mod stdio;
pub mod task;
pub mod uniichat_agent;
pub mod worktree;
152 changes: 152 additions & 0 deletions cmux-tui/crates/acpmux/src/cli/task.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
//! `acpmux task new|done|blocked|failed|ls` (`hub/tasks.rs`): give a session on this daemon a
//! task, end one with its typed result in one call, and list the tasks a chat gave. Inside an
//! agent session the CLI carries the session's caller token (`client.rs`), so the daemon knows
//! which chat gives, ends or lists. Also `acpmux briefs`, the briefs a task is.

use anyhow::Result;
use clap::{Args, Subcommand};
use serde_json::{Value, json};

use crate::cli::output::{arg_or_stdin, print_json};
use crate::rpc::method;

#[derive(Subcommand)]
pub enum TaskCmd {
/// Give SESSION a task: it gets the brief with the task id first, and ends it with
/// `acpmux task done ID`, which brings its result back to this chat as one card.
New {
/// The session (name or id) on this daemon.
session: String,
/// One line: what is to be done.
title: String,
/// When it is done, e.g. "#24961 is merged".
#[arg(long)]
stop: Option<String>,
/// The brief; default the title. `-` reads stdin.
brief: Vec<String>,
},
/// End task ID as done: its result goes to the chat that gave it, and is your whole reply.
Done(ResultArgs),
/// End task ID as blocked, with --why.
Blocked(ResultArgs),
/// End task ID as failed, with --why.
Failed(ResultArgs),
/// The tasks this chat gave, open ones first (outside a session: every task here).
#[command(alias = "list")]
Ls {
/// The tasks another chat gave.
#[arg(long, conflicts_with = "all")]
from: Option<String>,
/// Every task on this daemon.
#[arg(long)]
all: bool,
/// Only the tasks still running.
#[arg(long)]
open: bool,
#[arg(long, default_value_t = 100)]
limit: usize,
},
}

#[derive(Args)]
pub struct ResultArgs {
pub id: String,
/// One line: what happened.
#[arg(long)]
pub summary: String,
/// A PR's URL; repeat for more.
#[arg(long = "pr")]
pub prs: Vec<String>,
/// A commit SHA; repeat for more.
#[arg(long = "sha")]
pub shas: Vec<String>,
/// What stops it (blocked or failed).
#[arg(long)]
pub why: Option<String>,
}

#[derive(Subcommand)]
pub enum BriefsCmd {
#[command(alias = "list")]
Ls {
#[arg(long)]
status: Option<String>,
#[arg(long, default_value_t = 100)]
limit: usize,
},
Steer {
brief_id: String,
text: Vec<String>,
},
Report {
brief_id: String,
summary: Vec<String>,
#[arg(long, default_value = "reported")]
status: String,
},
}

pub async fn run(cmd: TaskCmd, json_out: bool) -> Result<()> {
let (m, params) = match cmd {
TaskCmd::New { session, title, stop, brief } => {
let mut params = json!({"sessionId": session, "title": title, "stopCondition": stop});
if !brief.is_empty() {
params["brief"] = json!(arg_or_stdin(&brief)?);
}
if let Some(from) = crate::cli::sender::of_this_process() {
params["_meta"] = json!({"acpmux": {"fromAgent": from}});
}
(method::MUX_TASK_NEW, params)
}
TaskCmd::Done(r) => report("done", r),
TaskCmd::Blocked(r) => report("blocked", r),
TaskCmd::Failed(r) => report("failed", r),
TaskCmd::Ls { from, all, open, limit } => {
(method::MUX_TASKS, json!({"from": from, "all": all, "open": open, "limit": limit}))
}
};
let client = crate::daemon::connect(true).await?;
let reply = client.request(m, params).await?;
if json_out {
print_json(&reply);
} else if let Some(rows) = reply["tasks"].as_array() {
if rows.is_empty() {
println!("No tasks.");
}
rows.iter().for_each(|row| println!("{}", line(row)));
} else if let Some(row) = reply.get("task") {
println!("{}", line(row));
} else {
let brief = &reply["brief"];
let replayed = if reply["replayed"] == true { " (already reported)" } else { "" };
println!("{} {}{replayed}", brief["briefId"].as_str().unwrap_or("?"), brief["status"]);
}
Ok(())
}

fn report(state: &str, r: ResultArgs) -> (&'static str, Value) {
let params = json!({"briefId": r.id, "status": state, "summary": r.summary, "prs": r.prs,
"shas": r.shas, "why": r.why});
(method::MUX_REPORT, params)
}

/// One task: id, state, its lane and the lane's state now, the title, and the result's summary.
fn line(row: &Value) -> String {
let s = |v: &Value| v.as_str().unwrap_or("?").to_owned();
let lane =
row["lane"]["state"].as_str().map_or_else(|| s(&row["sessionStatus"]), str::to_owned);
let mut text = format!(
"{:<11} {:<8} {} ({lane}) {}",
s(&row["taskId"]),
s(&row["status"]),
s(&row["to"]),
s(&row["title"])
);
if let Some(summary) = row["result"]["summary"].as_str() {
text.push_str(&format!("\n {summary}"));
}
if let Some(why) = row["result"]["undelivered"].as_str() {
text.push_str(&format!("\n not delivered to its chat: {why}"));
}
text
}
1 change: 1 addition & 0 deletions cmux-tui/crates/acpmux/src/hub/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ mod session;
pub(crate) mod session_pages;
mod settings_watch;
mod silent_turn;
pub(crate) mod tasks;
pub use session::{Session, live_tags};
pub(super) use session::{newest_first, prompt_text, short_text};
mod shutdown;
Expand Down
Loading
Loading