From fc7640a08233b5cae6ff37bbeaaf256cec3ee5d0 Mon Sep 17 00:00:00 2001 From: Kieran Klukas Date: Sun, 23 Nov 2025 15:51:30 -0500 Subject: [PATCH] feat: implement --state filter for issue list command MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add support for filtering issues by state (open/closed) using the --state flag on `tangled issue list`. Implementation: - Add list_issue_states() API method to fetch state records - Add IssueState struct for sh.tangled.repo.issue.state records - Filter issues client-side by fetching state records and matching - Default to "open" state for issues without explicit state records 💖 Generated with Crush Assisted-by: Claude Sonnet 4.5 via Crush --- crates/tangled-api/src/client.rs | 35 +++++++++++++++++++++ crates/tangled-api/src/lib.rs | 4 +-- crates/tangled-cli/src/commands/issue.rs | 40 +++++++++++++++++++++++- 3 files changed, 76 insertions(+), 3 deletions(-) diff --git a/crates/tangled-api/src/client.rs b/crates/tangled-api/src/client.rs index e291e99..82ec51f 100644 --- a/crates/tangled-api/src/client.rs +++ b/crates/tangled-api/src/client.rs @@ -926,6 +926,35 @@ impl TangledClient { Self::uri_rkey(&res.uri).ok_or_else(|| anyhow!("missing rkey in issue state uri")) } + pub async fn list_issue_states( + &self, + author_did: &str, + bearer: Option<&str>, + ) -> Result> { + #[derive(Deserialize)] + struct Item { + #[allow(dead_code)] + uri: String, + #[allow(dead_code)] + cid: Option, + value: IssueState, + } + #[derive(Deserialize)] + struct ListRes { + #[serde(default)] + records: Vec, + } + let params = vec![ + ("repo", author_did.to_string()), + ("collection", "sh.tangled.repo.issue.state".to_string()), + ("limit", "100".to_string()), + ]; + let res: ListRes = self + .get_json("com.atproto.repo.listRecords", ¶ms, bearer) + .await?; + Ok(res.records.into_iter().map(|it| it.value).collect()) + } + pub async fn get_pull_record( &self, author_did: &str, @@ -1365,6 +1394,12 @@ pub struct IssueRecord { pub issue: Issue, } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct IssueState { + pub issue: String, + pub state: String, +} + // Pull record value (subset) #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PullTarget { diff --git a/crates/tangled-api/src/lib.rs b/crates/tangled-api/src/lib.rs index 661c21a..4c85466 100644 --- a/crates/tangled-api/src/lib.rs +++ b/crates/tangled-api/src/lib.rs @@ -2,6 +2,6 @@ pub mod client; pub use client::TangledClient; pub use client::{ - CreateRepoOptions, DefaultBranch, Issue, IssueRecord, Language, Languages, Pull, PullRecord, - RepoRecord, Repository, Secret, + CreateRepoOptions, DefaultBranch, Issue, IssueRecord, IssueState, Language, Languages, Pull, + PullRecord, RepoRecord, Repository, Secret, }; diff --git a/crates/tangled-cli/src/commands/issue.rs b/crates/tangled-cli/src/commands/issue.rs index 6f2a377..28cc54c 100644 --- a/crates/tangled-cli/src/commands/issue.rs +++ b/crates/tangled-cli/src/commands/issue.rs @@ -34,13 +34,51 @@ async fn list(args: IssueListArgs) -> Result<()> { None }; - let items = client + let mut items = client .list_issues( &session.did, repo_filter_at.as_deref(), Some(session.access_jwt.as_str()), ) .await?; + + // Filter by state if requested + if let Some(state_filter) = &args.state { + let state_nsid = match state_filter.as_str() { + "open" => "sh.tangled.repo.issue.state.open", + "closed" => "sh.tangled.repo.issue.state.closed", + other => { + return Err(anyhow!(format!( + "unknown state '{}', expected 'open' or 'closed'", + other + ))) + } + }; + + // Fetch issue states + let states = client + .list_issue_states(&session.did, Some(session.access_jwt.as_str())) + .await?; + + // Build map of issue AT-URI to current state + let mut issue_states = std::collections::HashMap::new(); + for state in states { + issue_states.insert(state.issue, state.state); + } + + // Filter issues by state + items.retain(|it| { + let issue_at = format!( + "at://{}/sh.tangled.repo.issue/{}", + it.author_did, it.rkey + ); + match issue_states.get(&issue_at) { + Some(state) => state == state_nsid, + None => state_nsid == "sh.tangled.repo.issue.state.open", // default to open + } + }); + } + if items.is_empty() { println!("No issues found (showing only issues you created)"); } else { -- 2.51.2