use super::require_interactive;
use crate::cli::commands::{LinearCommentCmd, LinearCommentsCmd};
use crate::cli::ui::Loader;
use crate::commands::linear::{linear_graphql_errors, linear_graphql_request};
use crate::commands::text_util::truncate_chars;
use crate::commands::linear_cmd::issues::get_issue;
use crate::commands::terminal_table::{render_table, TableStyle};
use colored::Colorize;
use dialoguer::{theme::ColorfulTheme, Input};
use serde::Deserialize;
use serde_json::{json, Value as JsonValue};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LinearComment {
pub id: String,
pub body: String,
pub created_at: Option<String>,
pub user_name: Option<String>,
}
#[derive(Debug, Deserialize)]
struct CommentsPage {
nodes: Vec<CommentNode>,
}
#[derive(Debug, Deserialize)]
struct CommentNode {
id: String,
body: String,
#[serde(default, rename = "createdAt")]
created_at: Option<String>,
#[serde(default)]
user: Option<UserRef>,
}
#[derive(Debug, Deserialize)]
struct UserRef {
#[serde(default)]
name: Option<String>,
}
pub async fn list_comments(
api_key: &str,
issue_id_or_identifier: &str,
) -> Result<Vec<LinearComment>, String> {
let issue = get_issue(api_key, issue_id_or_identifier).await?;
let body = linear_graphql_request(
api_key,
&json!({
"query": r#"
query XbpLinearComments($id: String!) {
issue(id: $id) {
comments(first: 100, orderBy: createdAt) {
nodes {
id
body
createdAt
user { name }
}
}
}
}
"#,
"variables": { "id": issue.id }
}),
)
.await?;
linear_graphql_errors(&body)?;
let page = body
.get("data")
.and_then(|d| d.get("issue"))
.and_then(|i| i.get("comments"))
.cloned()
.ok_or_else(|| "Linear comments query returned no data.".to_string())?;
let page: CommentsPage = serde_json::from_value(page)
.map_err(|e| format!("Failed to decode Linear comments: {e}"))?;
Ok(page
.nodes
.into_iter()
.map(|n| LinearComment {
id: n.id,
body: n.body,
created_at: n.created_at,
user_name: n.user.and_then(|u| u.name),
})
.collect())
}
pub async fn create_comment(
api_key: &str,
issue_id_or_identifier: &str,
body_text: &str,
) -> Result<LinearComment, String> {
let issue = get_issue(api_key, issue_id_or_identifier).await?;
let body_text = body_text.trim();
if body_text.is_empty() {
return Err("Comment body cannot be empty.".into());
}
let body = linear_graphql_request(
api_key,
&json!({
"query": r#"
mutation XbpLinearCommentCreate($input: CommentCreateInput!) {
commentCreate(input: $input) {
success
comment {
id
body
createdAt
user { name }
}
}
}
"#,
"variables": {
"input": {
"issueId": issue.id,
"body": body_text
}
}
}),
)
.await?;
linear_graphql_errors(&body)?;
let payload = body
.get("data")
.and_then(|d| d.get("commentCreate"))
.cloned()
.ok_or_else(|| "Linear commentCreate returned no data.".to_string())?;
if payload.get("success").and_then(JsonValue::as_bool) == Some(false) {
return Err("Linear commentCreate reported success=false.".into());
}
let comment = payload
.get("comment")
.cloned()
.ok_or_else(|| "Linear commentCreate returned no comment.".to_string())?;
let node: CommentNode = serde_json::from_value(comment)
.map_err(|e| format!("Failed to decode created comment: {e}"))?;
Ok(LinearComment {
id: node.id,
body: node.body,
created_at: node.created_at,
user_name: node.user.and_then(|u| u.name),
})
}
pub async fn run_comments(api_key: &str, args: LinearCommentsCmd) -> Result<(), String> {
let loader = Loader::start("Fetching comments");
let comments = match list_comments(api_key, &args.id).await {
Ok(v) => {
loader.success();
v
}
Err(e) => {
loader.fail(&e);
return Err(e);
}
};
print_comments(&args.id, &comments);
Ok(())
}
pub async fn run_comment(api_key: &str, args: LinearCommentCmd) -> Result<(), String> {
let body_text = match args.body.as_deref() {
Some(b) if !b.trim().is_empty() => b.to_string(),
_ => {
require_interactive("Writing a Linear comment")?;
Input::<String>::with_theme(&ColorfulTheme::default())
.with_prompt(format!("Comment on {}", args.id))
.interact_text()
.map_err(|e| e.to_string())?
}
};
let loader = Loader::start("Posting comment");
let comment = match create_comment(api_key, &args.id, &body_text).await {
Ok(v) => {
loader.success();
v
}
Err(e) => {
loader.fail(&e);
return Err(e);
}
};
println!(
"{} comment on {} by {}",
"OK".bright_green().bold(),
args.id.bright_white().bold(),
comment.user_name.as_deref().unwrap_or("you").bright_cyan()
);
println!("{}", comment.body);
Ok(())
}
fn print_comments(issue_id: &str, comments: &[LinearComment]) {
if comments.is_empty() {
println!(
"{} {}",
issue_id.bright_white().bold(),
"— no comments".dimmed()
);
return;
}
println!(
"{} ({} comment(s))",
issue_id.bright_white().bold(),
comments.len()
);
let rows: Vec<Vec<String>> = comments
.iter()
.map(|c| {
vec![
c.user_name
.clone()
.unwrap_or_else(|| "-".into())
.bright_cyan()
.to_string(),
c.created_at
.clone()
.unwrap_or_else(|| "-".into())
.dimmed()
.to_string(),
truncate_chars(&c.body.replace('\n', " "), 80),
]
})
.collect();
print!(
"{}",
render_table(
&["Author", "When", "Body"],
&rows,
TableStyle::Pipe,
"",
)
);
}