#[allow(clippy::wildcard_imports)]
use super::*;
pub(super) fn view_apply(_config: &Config, store: &Store, args: &Value) -> ToolOutcome {
if let Err(value) = view_arguments(args, &["view"]) {
return Ok(value);
}
let reference = req_string(args, "view")?;
let mut ctx = match view_context(store) {
Ok(ctx) => ctx,
Err(value) => return Ok(value),
};
let board = ctx.config.default_config().clone();
let resolution = crate::workflow::active_resolution(&ctx.config).1;
let mut service = ViewService::new(&mut ctx.config, &ctx.projects, &ctx.path);
let detail = match service.show(&reference) {
Ok(detail) => detail,
Err(refusal) => return Ok(view_refusal_result(&refusal)),
};
if let Err(refusal) = service.mark_active(&reference, Some(ViewSurface::Mcp)) {
return Ok(view_refusal_result(&refusal));
}
let corpus = match Corpus::build(store) {
Ok(corpus) => corpus,
Err(error) => return Ok(error_result(error.to_string())),
};
let result = match service.apply(&reference, &board, &corpus, &resolution) {
Ok(result) => result,
Err(refusal) => return Ok(view_refusal_result(&refusal)),
};
let marker = service.active();
Ok(success_result(
view_apply_text(detail.view(), detail.plan(), &result),
json!({
"view_id": detail.view().id.as_str(),
"name": detail.view().name,
"scope": scope_value(&detail.view().scope),
"resolution": detail.view().scope.resolution(&ctx.projects).as_str(),
"plan": plan_value(detail.plan()),
"active": marker.map(marker_value),
"issues": result.issue_numbers(),
"count": result.issue_numbers().len(),
"board": board_summary_value(result.board()),
"diagnostics": result
.diagnostics()
.iter()
.map(diagnostic_value)
.collect::<Vec<_>>(),
}),
))
}
pub(super) fn view_create(_config: &Config, store: &Store, args: &Value) -> ToolOutcome {
if let Err(value) = view_arguments(
args,
&[
"name",
"scope",
"project",
"owner",
"shared_with",
"filter",
"sort",
"group",
"hide_columns",
"show_mismatch",
],
) {
return Ok(value);
}
let name = req_string(args, "name")?;
let scope_args = view_scope_args(args)?;
let plan_args = view_plan_args(args)?;
let mut ctx = match view_context(store) {
Ok(ctx) => ctx,
Err(value) => return Ok(value),
};
let mut service = ViewService::new(&mut ctx.config, &ctx.projects, &ctx.path);
let scope = match cmd_views::build_scope(&scope_args) {
Ok(scope) => scope,
Err(refusal) => return Ok(view_refusal_result(&refusal)),
};
let plan = match cmd_views::plan_from_args(store, &plan_args) {
Ok(plan) => plan,
Err(error) => return Ok(view_filter_rejection(&error)),
};
match service.create(ViewDefinition::new(name, scope, plan)) {
Ok(outcome) => {
let audit = outcome.audit().cloned();
let id = outcome.into_value();
Ok(success_result(
format!("Created {id}"),
view_accepted_value("accepted", "create", &id, &service, &ctx.projects, audit),
))
}
Err(error) => Ok(view_write_result(&error)),
}
}
pub(super) fn view_delete(_config: &Config, store: &Store, args: &Value) -> ToolOutcome {
if let Err(value) = view_arguments(args, &["view", "confirm"]) {
return Ok(value);
}
let reference = req_string(args, "view")?;
if args.get("confirm").and_then(Value::as_bool) != Some(true) {
return Ok(view_rejection(
DELETE_CONFIRMATION.to_string(),
&[ViewDiagnostic::new(
ViewDiagnosticKind::InvalidView,
DELETE_CONFIRMATION,
)
.with_field("confirm")
.with_remedy(
"pass `confirm: true` to acknowledge that only the saved definition is removed",
)],
));
}
let mut ctx = match view_context(store) {
Ok(ctx) => ctx,
Err(value) => return Ok(value),
};
let mut service = ViewService::new(&mut ctx.config, &ctx.projects, &ctx.path);
match service.delete(&reference, DeleteConfirmed::confirmed()) {
Ok(outcome) => {
let audit = outcome.audit().cloned();
let removed = outcome.into_value();
let view = removed.view();
let plan = ViewPlan::from_toml(&view.plan).ok();
let mut text = format!("Deleted {}", view.id);
if removed.marker_cleared() {
text.push_str("\nactive-marker: cleared");
}
Ok(success_result(
text,
json!({
"status": "accepted",
"operation": "delete",
"view": view_value(view, plan.as_ref(), &[], &ctx.projects),
"marker_cleared": removed.marker_cleared(),
"audit": audit.as_ref().map(audit_value),
}),
))
}
Err(error) => Ok(view_write_result(&error)),
}
}
pub(super) fn view_duplicate(_config: &Config, store: &Store, args: &Value) -> ToolOutcome {
if let Err(value) = view_arguments(args, &["view", "name"]) {
return Ok(value);
}
let reference = req_string(args, "view")?;
let name = opt_string(args, "name")?;
let mut ctx = match view_context(store) {
Ok(ctx) => ctx,
Err(value) => return Ok(value),
};
let mut service = ViewService::new(&mut ctx.config, &ctx.projects, &ctx.path);
match service.duplicate(&reference, name.as_deref()) {
Ok(outcome) => {
let audit = outcome.audit().cloned();
let id = outcome.into_value();
Ok(success_result(
format!("Duplicated {id}"),
view_accepted_value("accepted", "duplicate", &id, &service, &ctx.projects, audit),
))
}
Err(error) => Ok(view_write_result(&error)),
}
}
pub(super) fn view_list(_config: &Config, store: &Store, args: &Value) -> ToolOutcome {
if let Err(value) = view_arguments(args, &["scope"]) {
return Ok(value);
}
let scope = match opt_string(args, "scope")? {
Some(raw) => match cmd_views::parse_scope_kind(&raw) {
Ok(kind) => Some(kind),
Err(error) => return Ok(error_result(error.to_string())),
},
None => None,
};
let mut ctx = match view_context(store) {
Ok(ctx) => ctx,
Err(value) => return Ok(value),
};
let service = ViewService::new(&mut ctx.config, &ctx.projects, &ctx.path);
let mut rows = Vec::new();
let mut entries = Vec::new();
for view in service.list() {
if scope.is_some_and(|kind| view.scope.kind != kind) {
continue;
}
rows.push(format!(
"{}\t{}\t{}\t{}",
view.id,
view.name,
cmd_views::scope_label(&view.scope),
view.scope.resolution(&ctx.projects),
));
entries.push(match service.show(view.id.as_str()) {
Ok(detail) => view_value(
detail.view(),
Some(detail.plan()),
detail.diagnostics(),
&ctx.projects,
),
Err(refusal) => view_value(&view, None, refusal.diagnostics(), &ctx.projects),
});
}
let mut text = String::new();
for row in &rows {
text.push_str(row);
text.push('\n');
}
Ok(success_result(text, json!({ "views": entries })))
}
pub(super) fn view_share(_config: &Config, store: &Store, args: &Value) -> ToolOutcome {
if let Err(value) = view_arguments(args, &["view", "scope", "project", "owner", "shared_with"])
{
return Ok(value);
}
let reference = req_string(args, "view")?;
let scope_args = view_scope_args(args)?;
let mut ctx = match view_context(store) {
Ok(ctx) => ctx,
Err(value) => return Ok(value),
};
let mut service = ViewService::new(&mut ctx.config, &ctx.projects, &ctx.path);
let detail = match service.show(&reference) {
Ok(detail) => detail,
Err(refusal) => return Ok(view_refusal_result(&refusal)),
};
let name = detail.view().name.clone();
let plan = detail.plan().clone();
let scope = match cmd_views::build_scope(&scope_args) {
Ok(scope) => scope,
Err(refusal) => return Ok(view_refusal_result(&refusal)),
};
match service.update(&reference, ViewDefinition::new(name, scope, plan)) {
Ok(outcome) => {
let no_op = outcome.is_noop();
let audit = outcome.audit().cloned();
let id = outcome.into_value();
let (status, word) = if no_op {
("no-op", "Unchanged")
} else {
("accepted", "Shared")
};
Ok(success_result(
format!("{word} {id}"),
view_accepted_value(status, "share", &id, &service, &ctx.projects, audit),
))
}
Err(error) => Ok(view_write_result(&error)),
}
}
pub(super) fn view_show(_config: &Config, store: &Store, args: &Value) -> ToolOutcome {
if let Err(value) = view_arguments(args, &["view"]) {
return Ok(value);
}
let reference = req_string(args, "view")?;
let mut ctx = match view_context(store) {
Ok(ctx) => ctx,
Err(value) => return Ok(value),
};
let service = ViewService::new(&mut ctx.config, &ctx.projects, &ctx.path);
match service.show(&reference) {
Ok(detail) => Ok(success_result(
view_show_text(
detail.view(),
detail.plan(),
&ctx.projects,
detail.diagnostics(),
),
view_value(
detail.view(),
Some(detail.plan()),
detail.diagnostics(),
&ctx.projects,
),
)),
Err(refusal) => Ok(view_refusal_result(&refusal)),
}
}
pub(super) fn view_status(_config: &Config, store: &Store, args: &Value) -> ToolOutcome {
if let Err(value) = view_arguments(
args,
&[
"view",
"filter",
"sort",
"group",
"hide_columns",
"show_mismatch",
],
) {
return Ok(value);
}
let reference = opt_string(args, "view")?;
let live_args = view_plan_args(args)?;
let mut ctx = match view_context(store) {
Ok(ctx) => ctx,
Err(value) => return Ok(value),
};
let service = ViewService::new(&mut ctx.config, &ctx.projects, &ctx.path);
let live = match cmd_views::plan_from_args(store, &live_args) {
Ok(plan) => plan,
Err(error) => return Ok(view_filter_rejection(&error)),
};
let (divergence, view) = match &reference {
Some(reference) => match service.show(reference) {
Ok(detail) => {
let divergence = ViewDivergence::between(detail.plan(), &live);
(divergence, Some(detail))
}
Err(refusal) => return Ok(view_refusal_result(&refusal)),
},
None => (ViewDivergence::missing_marker(&live), None),
};
let mut text = String::new();
if let Some(detail) = &view {
text.push_str(&format!(
"id: {}\nname: {}\n",
detail.view().id,
detail.view().name
));
}
text.push_str(&format!("in-sync: {}\n", divergence.in_sync()));
for difference in divergence.differences() {
text.push_str(&format!(
"difference: {} saved={} live={}\n",
difference.field(),
difference.saved().unwrap_or("(unset)"),
difference.live().unwrap_or("(unset)"),
));
}
for diagnostic in divergence.diagnostics() {
text.push_str(&format!(
"diagnostic: {}\n",
cmd_views::render_diagnostic(diagnostic)
));
}
Ok(success_result(
text,
json!({
"in_sync": divergence.in_sync(),
"view_id": view.as_ref().map(|detail| detail.view().id.as_str()),
"saved_plan": divergence.saved_plan().map(plan_value),
"live_plan": plan_value(divergence.live_plan()),
"differences": divergence
.differences()
.iter()
.map(|difference| json!({
"field": difference.field(),
"saved": difference.saved(),
"live": difference.live(),
}))
.collect::<Vec<_>>(),
"diagnostics": divergence
.diagnostics()
.iter()
.map(diagnostic_value)
.collect::<Vec<_>>(),
}),
))
}
pub(super) fn view_update(_config: &Config, store: &Store, args: &Value) -> ToolOutcome {
if let Err(value) = view_arguments(
args,
&[
"view",
"name",
"filter",
"sort",
"group",
"hide_columns",
"show_mismatch",
],
) {
return Ok(value);
}
let reference = req_string(args, "view")?;
let name = opt_string(args, "name")?;
let plan_args = view_plan_args(args)?;
let mut ctx = match view_context(store) {
Ok(ctx) => ctx,
Err(value) => return Ok(value),
};
let mut service = ViewService::new(&mut ctx.config, &ctx.projects, &ctx.path);
let detail = match service.show(&reference) {
Ok(detail) => detail,
Err(refusal) => return Ok(view_refusal_result(&refusal)),
};
let new_name = name.unwrap_or_else(|| detail.view().name.clone());
let scope = detail.view().scope.clone();
let plan = if plan_args.is_any() {
match cmd_views::overlay_plan_args(store, detail.plan(), &plan_args) {
Ok(plan) => plan,
Err(error) => return Ok(view_filter_rejection(&error)),
}
} else {
detail.plan().clone()
};
match service.update(&reference, ViewDefinition::new(new_name, scope, plan)) {
Ok(outcome) => {
let no_op = outcome.is_noop();
let audit = outcome.audit().cloned();
let id = outcome.into_value();
let (status, word) = if no_op {
("no-op", "Unchanged")
} else {
("accepted", "Updated")
};
Ok(success_result(
format!("{word} {id}"),
view_accepted_value(status, "update", &id, &service, &ctx.projects, audit),
))
}
Err(error) => Ok(view_write_result(&error)),
}
}
pub(super) fn dispatch(
config: &Config,
store: &Store,
name: &str,
arguments: &Value,
) -> Option<ToolOutcome> {
match name {
"view_list" => Some(view_list(config, store, arguments)),
"view_show" => Some(view_show(config, store, arguments)),
"view_create" => Some(view_create(config, store, arguments)),
"view_update" => Some(view_update(config, store, arguments)),
"view_duplicate" => Some(view_duplicate(config, store, arguments)),
"view_delete" => Some(view_delete(config, store, arguments)),
"view_share" => Some(view_share(config, store, arguments)),
"view_apply" => Some(view_apply(config, store, arguments)),
"view_status" => Some(view_status(config, store, arguments)),
_ => None,
}
}
pub(super) fn view_accepted_value(
status: &str,
operation: &str,
id: &ViewId,
service: &ViewSvc<'_>,
projects: &BTreeSet<String>,
audit: Option<ViewAuditEvent>,
) -> Value {
let view = match service.show(id.as_str()) {
Ok(detail) => view_value(
detail.view(),
Some(detail.plan()),
detail.diagnostics(),
projects,
),
Err(refusal) => json!({
"id": id.as_str(),
"diagnostics": refusal.diagnostics().iter().map(diagnostic_value).collect::<Vec<_>>(),
}),
};
json!({
"status": status,
"operation": operation,
"view": view,
"audit": audit.as_ref().map(audit_value),
})
}
pub(super) fn view_refusal_result(refusal: &ViewRefusal) -> Value {
view_rejection(refusal.render(), refusal.diagnostics())
}
pub(super) fn view_rejection(text: String, diagnostics: &[ViewDiagnostic]) -> Value {
json!({
"content": [{ "type": "text", "text": text }],
"structuredContent": {
"status": "rejected",
"diagnostics": diagnostics.iter().map(diagnostic_value).collect::<Vec<_>>(),
},
"isError": true,
})
}
pub(super) fn view_write_result(error: &ViewWriteError) -> Value {
match error {
ViewWriteError::Refused(refusal) => view_refusal_result(refusal),
ViewWriteError::Failed(failure) => json!({
"content": [{ "type": "text", "text": failure.to_string() }],
"structuredContent": { "status": "failed", "diagnostics": [] },
"isError": true,
}),
}
}
pub(super) fn view_filter_rejection(error: &crate::error::TixError) -> Value {
let diagnostic = ViewDiagnostic::new(ViewDiagnosticKind::InvalidFilter, error.to_string())
.with_field("plan.filter");
view_rejection(error.to_string(), &[diagnostic])
}
pub(super) fn diagnostic_value(diagnostic: &ViewDiagnostic) -> Value {
json!({
"kind": diagnostic.kind().as_str(),
"field": diagnostic.field(),
"message": diagnostic.message(),
"remedy": diagnostic.remedy(),
"view_id": diagnostic.view_id().map(ViewId::as_str),
})
}
pub(super) fn scope_value(scope: &ViewScope) -> Value {
json!({
"kind": scope.kind.as_str(),
"project_id": scope.project_id,
"owner": scope.owner,
"shared_with": scope.shared_with,
"extensions": json_table(&scope.extensions),
})
}
pub(super) fn plan_value(plan: &ViewPlan) -> Value {
json_table(&plan.to_toml())
}
pub(super) fn json_table(table: &toml::Table) -> Value {
serde_json::to_value(table).unwrap_or(Value::Null)
}
pub(super) fn audit_value(event: &ViewAuditEvent) -> Value {
json!({
"mutation": event.kind().as_str(),
"view_id": event.view_id().as_str(),
"view_name": event.view_name(),
})
}
pub(super) fn marker_value(marker: &ActiveViewMarker) -> Value {
json!({
"view_id": marker.view_id().as_str(),
"source": marker.source().map(ViewSurface::as_str),
"applied_at": marker.applied_at(),
"applied_plan": plan_value(marker.applied_plan()),
})
}
pub(super) fn board_summary_value(board: &crate::ui::board::BoardState) -> Value {
json!({
"columns": board.columns.iter().map(board_column_value).collect::<Vec<_>>(),
"hidden": board.hidden.iter().map(board_column_value).collect::<Vec<_>>(),
"mismatch": board.mismatch.cards.len(),
})
}
pub(super) fn board_column_value(column: &crate::ui::board::BoardColumn) -> Value {
json!({ "name": column.name, "count": column.cards.len() })
}
pub(super) fn view_show_text(
view: &SavedView,
plan: &ViewPlan,
projects: &BTreeSet<String>,
diagnostics: &[ViewDiagnostic],
) -> String {
let mut out = format!(
"id: {}\nname: {}\nscope: {}\nresolution: {}\n",
view.id,
view.name,
cmd_views::scope_label(&view.scope),
view.scope.resolution(projects),
);
for line in cmd_views::plan_lines(plan) {
out.push_str(&line);
out.push('\n');
}
for diagnostic in diagnostics {
out.push_str(&format!(
"diagnostic: {}\n",
cmd_views::render_diagnostic(diagnostic)
));
}
out
}
pub(super) fn view_apply_text(
view: &SavedView,
plan: &ViewPlan,
result: &ViewApplyResult,
) -> String {
let mut out = format!(
"id: {}\nname: {}\nscope: {}\n",
view.id,
view.name,
cmd_views::scope_label(&view.scope),
);
for line in cmd_views::plan_lines(plan) {
out.push_str(&line);
out.push('\n');
}
out.push_str(&format!("active: {}\n", view.id));
out.push_str(&format!(
"issues: {}\n",
cmd_views::join_numbers(result.issue_numbers())
));
out.push_str(&format!("count: {}\n", result.issue_numbers().len()));
for diagnostic in result.diagnostics() {
out.push_str(&format!(
"diagnostic: {}\n",
cmd_views::render_diagnostic(diagnostic)
));
}
out
}
pub(super) struct ViewContext {
config: Config,
path: std::path::PathBuf,
projects: BTreeSet<String>,
}
pub(super) fn view_context(store: &Store) -> std::result::Result<ViewContext, Value> {
let path = cmd_views::config_path(store);
let root = path.parent().unwrap_or_else(|| Path::new("."));
match Config::load(root) {
Ok(config) => Ok(ViewContext {
config,
path,
projects: cmd_views::project_directory(),
}),
Err(error) => Err(error_result(error.to_string())),
}
}
pub(super) fn view_arguments(args: &Value, allowed: &[&str]) -> std::result::Result<(), Value> {
let Some(object) = args.as_object() else {
return Ok(());
};
let unknown: Vec<&str> = object
.keys()
.map(String::as_str)
.filter(|key| !allowed.contains(key))
.collect();
if unknown.is_empty() {
return Ok(());
}
let diagnostics: Vec<ViewDiagnostic> = unknown
.iter()
.map(|key| {
ViewDiagnostic::new(
ViewDiagnosticKind::InvalidView,
format!("`{key}` is not a field of this tool"),
)
.with_field(*key)
.with_remedy(
"remove it: a saved view is repository data, and host identity, permissions, \
and executable query payloads are not saved-view fields",
)
})
.collect();
let rendered = ViewRefusal::from_diagnostics(diagnostics.clone()).render();
Err(view_rejection(rendered, &diagnostics))
}
pub(super) fn view_scope_args(args: &Value) -> std::result::Result<crate::cli::ViewScopeArgs, ()> {
Ok(crate::cli::ViewScopeArgs {
scope: opt_string(args, "scope")?,
project: opt_string(args, "project")?,
owner: opt_string(args, "owner")?,
shared_with: opt_string(args, "shared_with")?,
})
}
pub(super) fn view_plan_args(args: &Value) -> std::result::Result<crate::cli::ViewPlanArgs, ()> {
Ok(crate::cli::ViewPlanArgs {
filter: opt_string(args, "filter")?,
sort: opt_string(args, "sort")?,
group: opt_string(args, "group")?,
hide_columns: opt_string(args, "hide_columns")?,
show_mismatch: opt_bool(args, "show_mismatch")?,
})
}
pub(super) fn view_value(
view: &SavedView,
plan: Option<&ViewPlan>,
diagnostics: &[ViewDiagnostic],
projects: &BTreeSet<String>,
) -> Value {
json!({
"id": view.id.as_str(),
"name": view.name,
"scope": scope_value(&view.scope),
"scope_label": cmd_views::scope_label(&view.scope),
"resolution": view.scope.resolution(projects).as_str(),
"plan": plan.map(plan_value),
"extensions": json_table(&view.extensions),
"diagnostics": diagnostics.iter().map(diagnostic_value).collect::<Vec<_>>(),
})
}