Rust CLI for tangled
1
fork

Configure Feed

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

feat: format issue repo as handle/name with caching

Issue list now displays repo as "dunkirk.sh/thistle" instead of
"at://did:plc:.../sh.tangled.repo/rkey" for better readability.

Uses a cache to avoid repeated API calls for the same repo, making
the command fast even with many issues from the same repository.

Implementation:
- Add get_repo_by_rkey() to fetch repo record by DID and rkey
- Add resolve_did_to_handle() to convert DID to handle
- Cache repo AT-URI to formatted name mappings
- Parse AT-URI to extract DID and rkey, then resolve to handle/name

💖 Generated with Crush

Assisted-by: Claude Sonnet 4.5 via Crush <crush@charm.land>

+75 -1
+37
crates/tangled-api/src/client.rs
··· 415 415 Err(anyhow!("repo not found for owner/name")) 416 416 } 417 417 418 + pub async fn get_repo_by_rkey( 419 + &self, 420 + did: &str, 421 + rkey: &str, 422 + bearer: Option<&str>, 423 + ) -> Result<Repository> { 424 + #[derive(Deserialize)] 425 + struct GetRes { 426 + value: Repository, 427 + } 428 + let params = [ 429 + ("repo", did.to_string()), 430 + ("collection", "sh.tangled.repo".to_string()), 431 + ("rkey", rkey.to_string()), 432 + ]; 433 + let res: GetRes = self 434 + .get_json("com.atproto.repo.getRecord", &params, bearer) 435 + .await?; 436 + Ok(res.value) 437 + } 438 + 439 + pub async fn resolve_did_to_handle( 440 + &self, 441 + did: &str, 442 + bearer: Option<&str>, 443 + ) -> Result<String> { 444 + #[derive(Deserialize)] 445 + struct Res { 446 + handle: String, 447 + } 448 + let params = [("repo", did.to_string())]; 449 + let res: Res = self 450 + .get_json("com.atproto.repo.describeRepo", &params, bearer) 451 + .await?; 452 + Ok(res.handle) 453 + } 454 + 418 455 pub async fn delete_repo( 419 456 &self, 420 457 did: &str,
+38 -1
crates/tangled-cli/src/commands/issue.rs
··· 83 83 println!("No issues found (showing only issues you created)"); 84 84 } else { 85 85 println!("RKEY\tTITLE\tREPO"); 86 + 87 + // Build cache of repo AT-URIs to formatted names 88 + let mut repo_cache: std::collections::HashMap<String, String> = std::collections::HashMap::new(); 89 + 86 90 for it in items { 87 - println!("{}\t{}\t{}", it.rkey, it.issue.title, it.issue.repo); 91 + let repo_display = if let Some(cached) = repo_cache.get(&it.issue.repo) { 92 + cached.clone() 93 + } else if let Some((repo_did, repo_rkey)) = parse_repo_at_uri(&it.issue.repo) { 94 + // Fetch and format repo info 95 + let formatted = match client 96 + .get_repo_by_rkey(&repo_did, &repo_rkey, Some(session.access_jwt.as_str())) 97 + .await 98 + { 99 + Ok(repo) => { 100 + let handle = client 101 + .resolve_did_to_handle(&repo_did, Some(session.access_jwt.as_str())) 102 + .await 103 + .unwrap_or(repo_did.clone()); 104 + format!("{}/{}", handle, repo.name) 105 + } 106 + Err(_) => it.issue.repo.clone(), 107 + }; 108 + repo_cache.insert(it.issue.repo.clone(), formatted.clone()); 109 + formatted 110 + } else { 111 + it.issue.repo.clone() 112 + }; 113 + println!("{}\t{}\t{}", it.rkey, it.issue.title, repo_display); 88 114 } 89 115 } 90 116 Ok(()) 117 + } 118 + 119 + fn parse_repo_at_uri(at_uri: &str) -> Option<(String, String)> { 120 + // Parse at://did/sh.tangled.repo/rkey 121 + let without_prefix = at_uri.strip_prefix("at://")?; 122 + let parts: Vec<&str> = without_prefix.split('/').collect(); 123 + if parts.len() >= 3 && parts[1] == "sh.tangled.repo" { 124 + Some((parts[0].to_string(), parts[2].to_string())) 125 + } else { 126 + None 127 + } 91 128 } 92 129 93 130 async fn create(args: IssueCreateArgs) -> Result<()> {