Forge
csharpdeeb25a2
1#nullable enable
2
3using System.Net.Http;
4using System.Text.Json;
5using CascadeIDE.Features.Workspace;
6
7namespace CascadeIDE.Features.Forge.Lens;
8
9/// <summary>Forge Lens: issues/MR с code anchors для CRS (ADR 0158, FORGE-ADR-0003).</summary>
10public static class ForgeLensCorrespondenceClient
11{
12 public const string Provenance = "forge_lens";
13
14 private static readonly JsonSerializerOptions JsonOptions = new() { PropertyNameCaseInsensitive = true };
15
16 public static IReadOnlyList<ForgeLensCorrespondenceItem> TryLoadForFile(
17 RepositoryWorkspaceToml? workspaceToml,
18 string workspaceRoot,
19 string repoRelativeFile)
20 {
21 var config = ResolveConfig(workspaceToml, workspaceRoot);
22 if (config is null)
23 return [];
24
25 var normalizedFile = repoRelativeFile.Replace('\\', '/').Trim();
26 if (normalizedFile.Length == 0)
27 return [];
28
29 try
30 {
31 using var http = new HttpClient { BaseAddress = new Uri(config.BaseUrl), Timeout = TimeSpan.FromSeconds(2) };
32 var apiToken = ForgeLensCredentialResolver.ResolveApiToken(config.BaseUrl, config.ApiTokenEnv);
33 if (!string.IsNullOrEmpty(apiToken))
34 http.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", apiToken);
35
36 var path = $"/api/v1/repos/{Uri.EscapeDataString(config.Repo)}/lens?file={Uri.EscapeDataString(normalizedFile)}";
37 using var response = http.GetAsync(path).GetAwaiter().GetResult();
38 if (!response.IsSuccessStatusCode)
39 return [];
40
41 using var stream = response.Content.ReadAsStream();
42 var lens = JsonSerializer.Deserialize<ForgeLensApiResponse>(stream, JsonOptions);
43 if (lens is null)
44 return [];
45
46 var items = new List<ForgeLensCorrespondenceItem>();
47 foreach (var issue in lens.Issues)
48 {
49 items.Add(new ForgeLensCorrespondenceItem(
50 $"Issue #{issue.Number}: {issue.Title}",
51 issue.IssueUrl,
52 issue.Status,
53 issue.Number,
54 IsMergeRequest: false));
55 }
56
57 foreach (var mr in lens.MergeRequests)
58 {
59 items.Add(new ForgeLensCorrespondenceItem(
60 $"MR !{mr.Number}: {mr.Title}",
61 mr.MrUrl,
62 mr.Status,
63 mr.Number,
64 IsMergeRequest: true));
65 }
66
67 return items;
68 }
69 catch
70 {
71 return [];
72 }
73 }
74
75 private static ForgeLensConfig? ResolveConfig(RepositoryWorkspaceToml? workspaceToml, string workspaceRoot)
76 {
77 _ = workspaceRoot;
78 var fromToml = workspaceToml?.Workspace?.Forge;
79 var baseUrl = (fromToml?.BaseUrl ?? "").Trim().TrimEnd('/');
80 var repo = (fromToml?.Repo ?? "").Trim();
81 if (baseUrl.Length == 0 || repo.Length == 0)
82 return null;
83
84 return new ForgeLensConfig(baseUrl, repo, fromToml?.ApiTokenEnv);
85 }
86
87 private sealed record ForgeLensConfig(string BaseUrl, string Repo, string? ApiTokenEnv);
88
89 private sealed class ForgeLensApiResponse
90 {
91 public List<ForgeLensIssueDto> Issues { get; set; } = [];
92 public List<ForgeLensMrDto> MergeRequests { get; set; } = [];
93 }
94
95 private sealed class ForgeLensIssueDto
96 {
97 public int Number { get; set; }
98 public string Title { get; set; } = "";
99 public string Status { get; set; } = "";
100 public string IssueUrl { get; set; } = "";
101 }
102
103 private sealed class ForgeLensMrDto
104 {
105 public int Number { get; set; }
106 public string Title { get; set; } = "";
107 public string Status { get; set; } = "";
108 public string MrUrl { get; set; } = "";
109 }
110}
111
112public sealed record ForgeLensCorrespondenceItem(
113 string DisplayTitle,
114 string Url,
115 string Status,
116 int Number,
117 bool IsMergeRequest);
118
View only · write via MCP/CIDE