Forge
csharpdeeb25a2
1#nullable enable
2
3using System.Text.Json;
4using CascadeIDE.Models.AgentChat;
5using CascadeIDE.Models.Intercom;
6using CascadeIDE.Services;
7using CascadeIDE.Services.Intercom;
8
9namespace CascadeIDE.Features.Chat;
10
11public partial class ChatPanelViewModel
12{
13 private IReadOnlyList<IntercomMessageRangeRelatedProjector.ExplicitRelate> _explicitMessageRangeRelates = [];
14
15 /// <summary>Найти сообщения в активной ветке по коду (ADR 0137: inferred + explicit relate).</summary>
16 public string FindMessagesForCodeRef(string? codeRefTail)
17 {
18 var editor = BuildAttachEditorSnapshot();
19 var workspace = ResolveAttachWorkspaceRoot();
20 var solution = ResolveAttachSolutionPath();
21 if (!IntercomCodeRefParser.TryParse(
22 codeRefTail,
23 editor,
24 workspace,
25 solution,
26 out var query,
27 out var parseError,
28 ResolveAttachIndexDirectoryRelative()))
29 {
30 return parseError;
31 }
32
33 if (!TryGetActiveDetailLaneMessageIndices(out var indices))
34 return "В активной ветке нет сообщений.";
35
36 return IntercomCorrespondenceOperations.FormatFindResult(
37 IntercomCorrespondenceOperations.ExecuteFind(
38 query,
39 workspace,
40 IsChatOverviewMode,
41 indices.Count,
42 buildLaneMessages(indices),
43 IntercomMessageRangeRelatedProjector.ForThread(_explicitMessageRangeRelates, _activeThreadId),
44 SelectMessageByOrdinalRangeInDetailLane));
45 }
46
47 /// <summary>
48 /// Явно связать диапазон gutter-сообщений с кодом; пишет <see cref="ChatHistoryEventKind.MessageRangeRelated"/> (ADR 0137).
49 /// </summary>
50 public string RelateMessageRangeToCodeRef(string? relateTail)
51 {
52 if (!IntercomMessageRelateArgs.TryParse(relateTail, out var segments, out var codeRefTail, out var parseError))
53 return parseError;
54
55 if (IsChatOverviewMode)
56 return "Открой тему (detail): /intercom topic open или клик по карточке.";
57
58 if (_activeThreadId == Guid.Empty)
59 return "Нет активной ветки.";
60
61 if (!TryGetActiveDetailLaneMessageIndices(out var indices))
62 return "В активной ветке нет сообщений.";
63
64 var ordinalSegments = segments
65 .Select(s => new ChatHistoryMessageOrdinalSegment(s.Start, s.End))
66 .ToList();
67
68 if (!IntercomMessageRangeRelatedSupport.TryValidateSegmentsInLane(ordinalSegments, indices.Count, out var rangeError))
69 return rangeError;
70
71 var selectResult = segments.Count == 1
72 ? SelectMessageByOrdinalRangeInDetailLane(segments[0].Start, segments[0].End)
73 : SelectMessagesByOrdinalRangesInDetailLane(segments);
74
75 if (!string.Equals(selectResult, "OK", StringComparison.Ordinal))
76 return selectResult;
77
78 var editor = BuildAttachEditorSnapshot();
79 var workspace = ResolveAttachWorkspaceRoot();
80 var solution = ResolveAttachSolutionPath();
81 if (!IntercomCodeRefParser.TryResolveAnchor(
82 codeRefTail,
83 editor,
84 workspace,
85 solution,
86 out var anchor,
87 out var anchorError,
88 ResolveAttachIndexDirectoryRelative()))
89 {
90 return anchorError;
91 }
92
93 var payload = IntercomMessageRangeRelatedSupport.CreatePayload(
94 _activeThreadId.ToString("N"),
95 ordinalSegments,
96 anchor,
97 "slash");
98
99 _ = PersistEventAsync(ChatHistoryEventKind.MessageRangeRelated, payload, _activeThreadId);
100 appendExplicitRelateInMemory(payload);
101
102 var label = IntercomMessageRangeRelatedSupport.FormatOrdinalSummary(ordinalSegments);
103 return $"Связь сообщений {label} с кодом записана ({anchor.DisplayLabel ?? anchor.File}).";
104 }
105
106 /// <summary>JSON для MCP <c>intercom.messages_for_code</c>.</summary>
107 public string FindMessagesForCodeRefFromMcp(IReadOnlyDictionary<string, JsonElement>? args)
108 {
109 var editor = BuildAttachEditorSnapshot();
110 var workspace = ResolveAttachWorkspaceRoot();
111 var solution = ResolveAttachSolutionPath();
112 if (!IntercomCodeRefParser.TryParseFromMcp(args, editor, workspace, solution, out var query, out var parseError))
113 return JsonSerializer.Serialize(new { error = "parse", message = parseError });
114
115 if (!TryGetActiveDetailLaneMessageIndices(out var indices))
116 return JsonSerializer.Serialize(new { error = "empty_lane", message = "В активной ветке нет сообщений." });
117
118 var result = IntercomCorrespondenceOperations.ExecuteFind(
119 query,
120 workspace,
121 IsChatOverviewMode,
122 indices.Count,
123 buildLaneMessages(indices),
124 IntercomMessageRangeRelatedProjector.ForThread(_explicitMessageRangeRelates, _activeThreadId),
125 SelectMessageByOrdinalRangeInDetailLane);
126 if (result.Error is { } err)
127 return JsonSerializer.Serialize(new { error = err.Kind, message = err.Message });
128
129 return JsonSerializer.Serialize(new
130 {
131 query = new
132 {
133 file = query.File,
134 line_start = query.LineStart,
135 line_end = query.LineEnd,
136 member_key = query.MemberKey,
137 anchor_id = query.ResolvedAnchor?.Id,
138 },
139 hits = result.Hits!.Select(h => new
140 {
141 ordinal = h.Ordinal,
142 message_index = h.MessageIndex,
143 message_id = h.MessageId.ToString("N"),
144 match_kind = h.MatchKind,
145 }),
146 selected_ordinal = result.SelectedOrdinal,
147 branch_message_count = result.BranchMessageCount,
148 });
149 }
150
151 /// <summary>JSON для MCP <c>intercom.message_relate</c>.</summary>
152 public string RelateMessageRangeToCodeRefFromMcp(IReadOnlyDictionary<string, JsonElement>? args)
153 {
154 if (args is null)
155 return JsonSerializer.Serialize(new { error = "parse", message = "Отсутствуют аргументы." });
156
157 if (!TryParseOrdinalSegmentsFromMcp(args, out var ordinalSegments, out var parametricSegments, out var segmentParseError))
158 {
159 return JsonSerializer.Serialize(new { error = "parse", message = segmentParseError });
160 }
161
162 var editor = BuildAttachEditorSnapshot();
163 var workspace = ResolveAttachWorkspaceRoot();
164 var solution = ResolveAttachSolutionPath();
165 if (!IntercomCodeRefParser.TryResolveAnchorFromMcp(
166 args,
167 editor,
168 workspace,
169 solution,
170 out var anchor,
171 out var anchorError,
172 ResolveAttachIndexDirectoryRelative()))
173 {
174 return JsonSerializer.Serialize(new { error = "parse", message = anchorError });
175 }
176
177 if (IsChatOverviewMode)
178 return JsonSerializer.Serialize(new { error = "overview_mode", message = "Открой detail-ветку." });
179
180 if (_activeThreadId == Guid.Empty)
181 return JsonSerializer.Serialize(new { error = "no_thread", message = "Нет активной ветки." });
182
183 if (!TryGetActiveDetailLaneMessageIndices(out var indices))
184 {
185 return JsonSerializer.Serialize(new { error = "empty_lane", message = "В активной ветке нет сообщений." });
186 }
187
188 if (!IntercomMessageRangeRelatedSupport.TryValidateSegmentsInLane(ordinalSegments, indices.Count, out var rangeError))
189 {
190 return JsonSerializer.Serialize(new { error = "range", message = rangeError });
191 }
192
193 var selectResult = parametricSegments.Count == 1
194 ? SelectMessageByOrdinalRangeInDetailLane(parametricSegments[0].Start, parametricSegments[0].End)
195 : SelectMessagesByOrdinalRangesInDetailLane(parametricSegments);
196
197 if (!string.Equals(selectResult, "OK", StringComparison.Ordinal))
198 {
199 return JsonSerializer.Serialize(new { error = "range", message = selectResult });
200 }
201
202 var payload = IntercomMessageRangeRelatedSupport.CreatePayload(
203 _activeThreadId.ToString("N"),
204 ordinalSegments,
205 anchor,
206 "mcp");
207
208 _ = PersistEventAsync(ChatHistoryEventKind.MessageRangeRelated, payload, _activeThreadId);
209 appendExplicitRelateInMemory(payload);
210
211 return JsonSerializer.Serialize(new
212 {
213 ok = true,
214 thread_id = payload.ThreadId,
215 start_ordinal = payload.StartOrdinal,
216 end_ordinal = payload.EndOrdinal,
217 ordinal_segments = ordinalSegments.Select(s => new
218 {
219 start_ordinal = s.StartOrdinal,
220 end_ordinal = s.EndOrdinal,
221 }),
222 code_ref = new
223 {
224 id = anchor.Id,
225 file = anchor.File,
226 member_key = anchor.MemberKey,
227 attachment_shape = anchor.AttachmentShape,
228 line_start = anchor.LineStart,
229 line_end = anchor.LineEnd,
230 },
231 });
232 }
233
234 private static bool TryParseOrdinalSegmentsFromMcp(
235 IReadOnlyDictionary<string, JsonElement> args,
236 out IReadOnlyList<ChatHistoryMessageOrdinalSegment> ordinalSegments,
237 out IReadOnlyList<ParametricIntRange> parametricSegments,
238 out string error)
239 {
240 ordinalSegments = [];
241 parametricSegments = [];
242 error = "";
243
244 if (args.TryGetValue("range_expr", out var rangeExpr)
245 && rangeExpr.ValueKind == JsonValueKind.String
246 && !string.IsNullOrWhiteSpace(rangeExpr.GetString()))
247 {
248 if (!ParametricSegmentListParser.TryParse(rangeExpr.GetString(), out parametricSegments, out error))
249 return false;
250
251 ordinalSegments = ToOrdinalSegments(parametricSegments);
252 return true;
253 }
254
255 if (args.TryGetValue("ordinal_segments", out var segmentsEl)
256 && segmentsEl.ValueKind == JsonValueKind.Array)
257 {
258 var list = new List<ChatHistoryMessageOrdinalSegment>();
259 foreach (var item in segmentsEl.EnumerateArray())
260 {
261 if (item.ValueKind != JsonValueKind.Object)
262 {
263 error = "ordinal_segments: каждый элемент — объект { start_ordinal, end_ordinal }.";
264 return false;
265 }
266
267 if (!item.TryGetProperty("start_ordinal", out var startEl)
268 || !item.TryGetProperty("end_ordinal", out var endEl)
269 || !startEl.TryGetInt32(out var start)
270 || !endEl.TryGetInt32(out var end))
271 {
272 error = "ordinal_segments: укажи start_ordinal и end_ordinal (integer ≥ 1).";
273 return false;
274 }
275
276 list.Add(new ChatHistoryMessageOrdinalSegment(start, end));
277 }
278
279 if (list.Count == 0)
280 {
281 error = "ordinal_segments не может быть пустым.";
282 return false;
283 }
284
285 ordinalSegments = list;
286 parametricSegments = list
287 .Select(s => new ParametricIntRange(s.StartOrdinal, s.EndOrdinal))
288 .ToList();
289 return true;
290 }
291
292 var startOrdinal = McpCommandJsonArgs.OptionalInt32(args, "start_ordinal");
293 var endOrdinal = McpCommandJsonArgs.OptionalInt32(args, "end_ordinal") ?? startOrdinal;
294 if (startOrdinal is null or < 1 || endOrdinal is null or < 1 || endOrdinal < startOrdinal)
295 {
296 error = "Укажи start_ordinal (1-based) и опционально end_ordinal, либо range_expr / ordinal_segments для disjoint.";
297 return false;
298 }
299
300 ordinalSegments = [new ChatHistoryMessageOrdinalSegment(startOrdinal.Value, endOrdinal.Value)];
301 parametricSegments = [new ParametricIntRange(startOrdinal.Value, endOrdinal.Value)];
302 return true;
303 }
304
305 private static IReadOnlyList<ChatHistoryMessageOrdinalSegment> ToOrdinalSegments(
306 IReadOnlyList<ParametricIntRange> segments) =>
307 segments.Select(s => new ChatHistoryMessageOrdinalSegment(s.Start, s.End)).ToList();
308
309 private void rebuildExplicitRelatesFromEvents(IReadOnlyList<ChatHistoryEvent> events) =>
310 _explicitMessageRangeRelates = IntercomMessageRangeRelatedProjector.Project(events);
311
312 private void appendExplicitRelateInMemory(ChatHistoryMessageRangeRelatedPayload payload)
313 {
314 if (!Guid.TryParse(payload.ThreadId, out var threadId) || threadId == Guid.Empty)
315 return;
316
317 var list = _explicitMessageRangeRelates.ToList();
318 list.Add(new IntercomMessageRangeRelatedProjector.ExplicitRelate(
319 threadId,
320 payload.StartOrdinal,
321 payload.EndOrdinal,
322 IntercomMessageRangeRelatedSupport.ResolveSegments(payload),
323 payload.CodeRef,
324 payload.Source));
325 _explicitMessageRangeRelates = list;
326 }
327
328 private IReadOnlyList<IntercomMessageCodeCorrespondenceProjector.LaneMessage> buildLaneMessages(
329 IReadOnlyList<int> indices)
330 {
331 var list = new List<IntercomMessageCodeCorrespondenceProjector.LaneMessage>();
332 for (var i = 0; i < indices.Count; i++)
333 {
334 var messageIndex = indices[i];
335 if (messageIndex < 0 || messageIndex >= ChatMessages.Count)
336 continue;
337
338 var m = ChatMessages[messageIndex];
339 list.Add(new IntercomMessageCodeCorrespondenceProjector.LaneMessage(
340 i + 1,
341 messageIndex,
342 m.MessageId,
343 m.Attachments));
344 }
345
346 return list;
347 }
348}
349
View only · write via MCP/CIDE