Forge
csharpdeeb25a2
1#nullable enable
2using System.Text.Json;
3using CascadeIDE.Models.AgentChat;
4
5namespace CascadeIDE.Features.Chat;
6
7internal static class ChatThreadForkProjector
8{
9 public static List<ChatThreadForkRecord> Project(IReadOnlyList<ChatHistoryEvent> events)
10 {
11 var list = new List<ChatThreadForkRecord>();
12 foreach (var ev in events)
13 {
14 if (!string.Equals(ev.Kind, ChatHistoryEventKind.ThreadForked, StringComparison.Ordinal))
15 continue;
16
17 if (TryParse(ev.PayloadJson, out var record))
18 list.Add(record);
19 }
20
21 return DedupeByNewThread(list);
22 }
23
24 public static List<ChatThreadForkRecord> DedupeByNewThread(IReadOnlyList<ChatThreadForkRecord> forks)
25 {
26 var byNew = new Dictionary<Guid, ChatThreadForkRecord>();
27 foreach (var fork in forks)
28 {
29 if (fork.NewThreadId == Guid.Empty)
30 continue;
31 byNew[fork.NewThreadId] = fork;
32 }
33
34 return byNew.Values.ToList();
35 }
36
37 private static bool TryParse(string payloadJson, out ChatThreadForkRecord record)
38 {
39 record = default!;
40 ChatHistoryThreadForkedPayload? payload;
41 try
42 {
43 payload = JsonSerializer.Deserialize<ChatHistoryThreadForkedPayload>(payloadJson, ChatHistoryJson.Options);
44 }
45 catch (JsonException)
46 {
47 return TryParseLegacy(payloadJson, out record);
48 }
49
50 if (payload is null)
51 return TryParseLegacy(payloadJson, out record);
52
53 if (!Guid.TryParse(payload.NewThreadId, out var newId) || newId == Guid.Empty)
54 return false;
55 if (!Guid.TryParse(payload.PreviousThreadId, out var previousId))
56 previousId = Guid.Empty;
57
58 Guid? parentMessageId = null;
59 if (!string.IsNullOrWhiteSpace(payload.ParentMessageId)
60 && Guid.TryParse(payload.ParentMessageId, out var parent)
61 && parent != Guid.Empty)
62 {
63 parentMessageId = parent;
64 }
65
66 record = new ChatThreadForkRecord(newId, previousId, parentMessageId);
67 return true;
68 }
69
70 private static bool TryParseLegacy(string payloadJson, out ChatThreadForkRecord record)
71 {
72 record = default!;
73 try
74 {
75 using var doc = JsonDocument.Parse(payloadJson);
76 var root = doc.RootElement;
77 if (!TryGetGuid(root, "new_thread_id", "NewThreadId", out var newId) || newId == Guid.Empty)
78 return false;
79 TryGetGuid(root, "previous_thread_id", "PreviousThreadId", out var previousId);
80 Guid? parentMessageId = null;
81 if (TryGetGuid(root, "parent_message_id", "ParentMessageId", out var parent) && parent != Guid.Empty)
82 parentMessageId = parent;
83 record = new ChatThreadForkRecord(newId, previousId, parentMessageId);
84 return true;
85 }
86 catch (JsonException)
87 {
88 return false;
89 }
90 }
91
92 private static bool TryGetGuid(JsonElement root, string snake, string pascal, out Guid value)
93 {
94 value = Guid.Empty;
95 if (!root.TryGetProperty(snake, out var el) && !root.TryGetProperty(pascal, out el))
96 return false;
97 var s = el.GetString();
98 return !string.IsNullOrWhiteSpace(s) && Guid.TryParse(s, out value);
99 }
100}
101
View only · write via MCP/CIDE