Rust CLI for tangled
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: implement --state filter for issue list command

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 <crush@charm.land>

+76 -3
+35
crates/tangled-api/src/client.rs
··· 926 926 Self::uri_rkey(&res.uri).ok_or_else(|| anyhow!("missing rkey in issue state uri")) 927 927 } 928 928 929 + pub async fn list_issue_states( 930 + &self, 931 + author_did: &str, 932 + bearer: Option<&str>, 933 + ) -> Result<Vec<IssueState>> { 934 + #[derive(Deserialize)] 935 + struct Item { 936 + #[allow(dead_code)] 937 + uri: String, 938 + #[allow(dead_code)] 939 + cid: Option<String>, 940 + value: IssueState, 941 + } 942 + #[derive(Deserialize)] 943 + struct ListRes { 944 + #[serde(default)] 945 + records: Vec<Item>, 946 + } 947 + let params = vec![ 948 + ("repo", author_did.to_string()), 949 + ("collection", "sh.tangled.repo.issue.state".to_string()), 950 + ("limit", "100".to_string()), 951 + ]; 952 + let res: ListRes = self 953 + .get_json("com.atproto.repo.listRecords", &params, bearer) 954 + .await?; 955 + Ok(res.records.into_iter().map(|it| it.value).collect()) 956 + } 957 + 929 958 pub async fn get_pull_record( 930 959 &self, 931 960 author_did: &str, ··· 1363 1392 pub author_did: String, 1364 1393 pub rkey: String, 1365 1394 pub issue: Issue, 1395 + } 1396 + 1397 + #[derive(Debug, Clone, Serialize, Deserialize)] 1398 + pub struct IssueState { 1399 + pub issue: String, 1400 + pub state: String, 1366 1401 } 1367 1402 1368 1403 // Pull record value (subset)
+2 -2
crates/tangled-api/src/lib.rs
··· 2 2 3 3 pub use client::TangledClient; 4 4 pub use client::{ 5 - CreateRepoOptions, DefaultBranch, Issue, IssueRecord, Language, Languages, Pull, PullRecord, 6 - RepoRecord, Repository, Secret, 5 + CreateRepoOptions, DefaultBranch, Issue, IssueRecord, IssueState, Language, Languages, Pull, 6 + PullRecord, RepoRecord, Repository, Secret, 7 7 };
+39 -1
crates/tangled-cli/src/commands/issue.rs
··· 34 34 None 35 35 }; 36 36 37 - let items = client 37 + let mut items = client 38 38 .list_issues( 39 39 &session.did, 40 40 repo_filter_at.as_deref(), 41 41 Some(session.access_jwt.as_str()), 42 42 ) 43 43 .await?; 44 + 45 + // Filter by state if requested 46 + if let Some(state_filter) = &args.state { 47 + let state_nsid = match state_filter.as_str() { 48 + "open" => "sh.tangled.repo.issue.state.open", 49 + "closed" => "sh.tangled.repo.issue.state.closed", 50 + other => { 51 + return Err(anyhow!(format!( 52 + "unknown state '{}', expected 'open' or 'closed'", 53 + other 54 + ))) 55 + } 56 + }; 57 + 58 + // Fetch issue states 59 + let states = client 60 + .list_issue_states(&session.did, Some(session.access_jwt.as_str())) 61 + .await?; 62 + 63 + // Build map of issue AT-URI to current state 64 + let mut issue_states = std::collections::HashMap::new(); 65 + for state in states { 66 + issue_states.insert(state.issue, state.state); 67 + } 68 + 69 + // Filter issues by state 70 + items.retain(|it| { 71 + let issue_at = format!( 72 + "at://{}/sh.tangled.repo.issue/{}", 73 + it.author_did, it.rkey 74 + ); 75 + match issue_states.get(&issue_at) { 76 + Some(state) => state == state_nsid, 77 + None => state_nsid == "sh.tangled.repo.issue.state.open", // default to open 78 + } 79 + }); 80 + } 81 + 44 82 if items.is_empty() { 45 83 println!("No issues found (showing only issues you created)"); 46 84 } else {