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
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion crates/orchestrator-cli/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "orchestrator-cli"
version = "0.6.21"
version = "0.6.22"
edition = "2021"
license = "Elastic-2.0"
default-run = "animus"
Expand Down
6 changes: 6 additions & 0 deletions crates/orchestrator-cli/src/cli_types/subject_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,12 @@ pub(crate) struct SubjectListArgs {
/// Opaque cursor from a prior page's `next_cursor`, to fetch the next page.
#[arg(long, value_name = "CURSOR")]
pub cursor: Option<String>,

/// Case-insensitive substring filter on the subject TITLE. Looks a subject
/// up by name without paging the whole set: the full set is fetched and
/// filtered by title, then `--limit` is applied to the matches.
#[arg(long, value_name = "TEXT")]
pub query: Option<String>,
}

#[derive(Debug, Args)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ pub(super) fn build_subject_list_args(input: &SubjectListInput) -> Vec<String> {
args.push(limit.to_string());
}
push_opt(&mut args, "--cursor", input.cursor.clone());
push_opt(&mut args, "--query", input.query.clone());
args
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ pub(super) struct SubjectListInput {
/// Opaque cursor from a prior result's `next_cursor`, to fetch the next page.
#[serde(default)]
pub(super) cursor: Option<String>,
/// Case-insensitive substring filter on the subject title — look a subject up
/// by name (e.g. a repo's owner/repo) without paging. The full set is fetched
/// and filtered, so the match is found even on a large board.
#[serde(default)]
pub(super) query: Option<String>,
#[serde(default)]
pub(super) project_root: Option<String>,
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1581,6 +1581,7 @@ fn mcp_subject_list_routes_via_control() {
status: Some("ready".to_string()),
limit: Some(25),
cursor: Some("page2".to_string()),
query: Some("flaky".to_string()),
project_root: None,
};
let args = build_subject_list_args(&input);
Expand All @@ -1597,6 +1598,8 @@ fn mcp_subject_list_routes_via_control() {
"25".to_string(),
"--cursor".to_string(),
"page2".to_string(),
"--query".to_string(),
"flaky".to_string(),
]
);
}
Expand Down
56 changes: 54 additions & 2 deletions crates/orchestrator-cli/src/services/operations/ops_subject.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ const DEFAULT_SUBJECT_LIST_LIMIT: u32 = 50;

async fn handle_subject_list(args: SubjectListArgs, project_root: &str, json: bool) -> Result<()> {
let kind = resolve_kind(args.kind.as_deref(), project_root, json)?;
let query = args.query.as_deref().map(str::trim).filter(|s| !s.is_empty()).map(str::to_string);
let mut filter = serde_json::Map::new();
filter.insert("kind".to_string(), json!([kind]));
if let Some(status) = args.status.as_deref() {
Expand All @@ -64,16 +65,67 @@ async fn handle_subject_list(args: SubjectListArgs, project_root: &str, json: bo
// that honor `limit` also return `total` + `next_cursor` for paging; backends
// that ignore it simply return everything (no behavior change for them).
let limit = args.limit.unwrap_or(DEFAULT_SUBJECT_LIST_LIMIT);
if limit > 0 {
filter.insert("limit".to_string(), json!(limit));
// A --query lookup must see the WHOLE set (the backend has no title filter,
// and the daemon drops unknown filter fields), so fetch unbounded and apply
// both the title filter and the page limit client-side below.
let fetch_limit = if query.is_some() { 0 } else { limit };
if fetch_limit > 0 {
filter.insert("limit".to_string(), json!(fetch_limit));
}
if let Some(cursor) = args.cursor.as_deref() {
filter.insert("cursor".to_string(), json!(cursor));
}
let params = Some(Value::Object(filter));
if let Some(q) = query {
return dispatch_list_filtered(&kind, params, &q, limit, project_root, json).await;
}
dispatch(&kind, "list", params, project_root, json).await
}

/// `subject list --query`: fetch the full set, filter by a case-insensitive
/// substring of the subject TITLE, then apply the page `limit` to the matches.
/// Lets agents/UI resolve a subject by name without paging — and without the
/// agent-facing MCP result truncating a huge unfiltered list. Mirrors
/// `dispatch`'s output (json envelope vs human table); the result is the
/// filtered subset with an exact `total` and a null `next_cursor`.
async fn dispatch_list_filtered(
kind: &str,
params: Option<Value>,
query: &str,
limit: u32,
project_root: &str,
json: bool,
) -> Result<()> {
let resolution = resolve_subject_dispatch(Path::new(project_root)).await?;
let method = format!("{kind}/list");
let raw = route_or_not_found(&resolution.selected, &method, params).await?;
let needle = query.to_lowercase();
let mut matches: Vec<Value> = extract_subjects(&raw)
.into_iter()
.filter(|s| s.get("title").and_then(Value::as_str).map(|t| t.to_lowercase().contains(&needle)).unwrap_or(false))
.cloned()
.collect();
let total = matches.len() as u64;
if limit > 0 {
matches.truncate(limit as usize);
}
let result = json!({ "subjects": matches, "next_cursor": Value::Null, "total": total });
if json {
return print_value(
SubjectCallResponse {
kind: kind.to_string(),
verb: "list",
method,
plugin_count: resolution.selected.plugin_count(),
result,
},
true,
);
}
render_subject_human("list", kind, &result);
Ok(())
}

async fn handle_subject_get(args: SubjectGetArgs, project_root: &str, json: bool) -> Result<()> {
let kind = resolve_kind(args.kind.as_deref(), project_root, json)?;
if args.id.trim().is_empty() {
Expand Down
Loading