Forge
csharpdeeb25a2
1#nullable enable
2
3namespace CascadeIDE.Features.Chat;
4
5/// <summary>Обработчики <c>kind=intercom</c> (поле <c>intercom_handler</c> в intent-catalog).</summary>
6public static class ChatSlashIntercomHandlers
7{
8 public static class Ids
9 {
10 public const string TopicOpen = "topic_open";
11 public const string TopicCards = "topic_cards";
12 public const string SpineOpen = "spine_open";
13 public const string TopicList = "topic_list";
14 public const string TopicTree = "topic_tree";
15 public const string TopicCreate = "topic_create";
16 public const string TopicRename = "topic_rename";
17 public const string AttachSelection = "attach_selection";
18 public const string AttachScope = "attach_scope";
19 public const string AttachFile = "attach_file";
20 public const string MessageSelect = "message_select";
21 public const string MessageSelectClear = "message_select_clear";
22 public const string MessageFind = "message_find";
23 public const string MessageRelate = "message_relate";
24 public const string MessageAnchorsList = "message_anchors_list";
25 public const string AnchorPeek = "anchor_peek";
26 public const string ServerStatus = "server_status";
27 public const string ServerStart = "server_start";
28 public const string ServerStop = "server_stop";
29 public const string TeamMembers = "team_members";
30 public const string TeamInvite = "team_invite";
31 public const string TeamSeedProject = "team_seed_project";
32 public const string AgentList = "agent_list";
33 public const string AgentProvision = "agent_provision";
34 public const string AgentSelect = "agent_select";
35 }
36
37 public sealed record Context(
38 string? ArgsTail,
39 Guid SelectedThreadId,
40 Action<Guid> SelectThread,
41 Action<bool> SetOverviewMode,
42 ChatSurfaceSnapshot Snapshot,
43 Action<TopicPickerPresentation>? SetTopicPicker,
44 Func<string, TopicCreateResult>? CreateTopicWithTitle,
45 Func<Guid, string, TopicRenameResult>? RenameTopicWithTitle,
46 Func<string, string?, ChatSlashIntercomResult>? TryAttachSlash,
47 Func<int, int, string>? SelectMessageByOrdinalRangeInDetailLane = null,
48 Func<IReadOnlyList<ParametricIntRange>, string>? SelectMessagesByOrdinalRangesInDetailLane = null,
49 Func<string>? ClearMessageSelectionInDetailLane = null,
50 Func<string?, string>? FindMessagesForCodeRef = null,
51 Func<string?, string>? RelateMessageRangeToCodeRef = null,
52 Func<string>? ListMessageAnchors = null,
53 Func<string?, string>? PeekAnchorById = null,
54 Func<string, string?, CancellationToken, Task<ChatSlashIntercomResult>>? RunAdmin = null);
55
56 private delegate ChatSlashIntercomResult Handler(Context context);
57
58 private static readonly IReadOnlyDictionary<string, Handler> Handlers =
59 new Dictionary<string, Handler>(StringComparer.OrdinalIgnoreCase)
60 {
61 [Ids.TopicOpen] = static ctx => ChatSlashIntercomActions.OpenTopic(
62 ctx.ArgsTail,
63 ctx.SelectedThreadId,
64 ctx.SelectThread,
65 ctx.SetOverviewMode,
66 ctx.Snapshot),
67 [Ids.TopicCards] = static ctx => ChatSlashIntercomActions.OpenTopicCards(
68 ctx.SetOverviewMode,
69 ctx.Snapshot),
70 [Ids.SpineOpen] = static ctx => ChatSlashIntercomActions.OpenTopicCards(
71 ctx.SetOverviewMode,
72 ctx.Snapshot),
73 [Ids.TopicList] = static ctx => ChatSlashIntercomActions.ShowTopicPicker(
74 TopicPickerPresentation.List,
75 ctx.SetTopicPicker,
76 ctx.SetOverviewMode,
77 ctx.Snapshot),
78 [Ids.TopicTree] = static ctx => ChatSlashIntercomActions.ShowTopicPicker(
79 TopicPickerPresentation.Tree,
80 ctx.SetTopicPicker,
81 ctx.SetOverviewMode,
82 ctx.Snapshot),
83 [Ids.TopicCreate] = static ctx => ChatSlashIntercomActions.CreateTopic(
84 ctx.ArgsTail,
85 ctx.CreateTopicWithTitle),
86 [Ids.TopicRename] = static ctx => ChatSlashIntercomActions.RenameTopic(
87 ctx.SelectedThreadId,
88 ctx.ArgsTail,
89 ctx.RenameTopicWithTitle),
90 [Ids.AttachSelection] = static ctx => executeAttach(ctx, Ids.AttachSelection),
91 [Ids.AttachScope] = static ctx => executeAttach(ctx, Ids.AttachScope),
92 [Ids.AttachFile] = static ctx => executeAttach(ctx, Ids.AttachFile),
93 [Ids.MessageSelect] = static ctx => executeMessageSelect(ctx),
94 [Ids.MessageSelectClear] = static ctx => executeMessageSelectClear(ctx),
95 [Ids.MessageFind] = static ctx => executeMessageFind(ctx),
96 [Ids.MessageRelate] = static ctx => executeMessageRelate(ctx),
97 [Ids.MessageAnchorsList] = static ctx => executeMessageAnchorsList(ctx),
98 [Ids.AnchorPeek] = static ctx => executeAnchorPeek(ctx),
99 [Ids.ServerStatus] = static ctx => executeAdmin(ctx, Ids.ServerStatus),
100 [Ids.ServerStart] = static ctx => executeAdmin(ctx, Ids.ServerStart),
101 [Ids.ServerStop] = static ctx => executeAdmin(ctx, Ids.ServerStop),
102 [Ids.TeamMembers] = static ctx => executeAdmin(ctx, Ids.TeamMembers),
103 [Ids.TeamInvite] = static ctx => executeAdmin(ctx, Ids.TeamInvite),
104 [Ids.TeamSeedProject] = static ctx => executeAdmin(ctx, Ids.TeamSeedProject),
105 [Ids.AgentList] = static ctx => executeAdmin(ctx, Ids.AgentList),
106 [Ids.AgentProvision] = static ctx => executeAdmin(ctx, Ids.AgentProvision),
107 [Ids.AgentSelect] = static ctx => executeAdmin(ctx, Ids.AgentSelect),
108 };
109
110 private static ChatSlashIntercomResult executeAttach(Context ctx, string handlerId)
111 {
112 if (ctx.TryAttachSlash is null)
113 return ChatSlashIntercomResult.Fail("Attach недоступен в этой сессии.");
114 return ctx.TryAttachSlash(handlerId, ctx.ArgsTail);
115 }
116
117 private static ChatSlashIntercomResult executeMessageSelect(Context ctx)
118 {
119 var tail = ctx.ArgsTail?.Trim() ?? "";
120 if (!ParametricSegmentListParser.TryParse(tail, out var segments, out var parseError))
121 return ChatSlashIntercomResult.Fail(parseError);
122
123 if (segments.Count > 1)
124 {
125 if (ctx.SelectMessagesByOrdinalRangesInDetailLane is null)
126 return ChatSlashIntercomResult.Fail("Выбор сообщений (multi-range) недоступен.");
127
128 var multiResult = ctx.SelectMessagesByOrdinalRangesInDetailLane(segments);
129 if (!string.Equals(multiResult, "OK", StringComparison.Ordinal))
130 return ChatSlashIntercomResult.Fail(multiResult);
131
132 var labels = segments.Select(static s =>
133 s.Start == s.End ? $"#{s.Start}" : $"#{s.Start}–#{s.End}");
134 return ChatSlashIntercomResult.Ok($"Выбрано: {string.Join(", ", labels)}.");
135 }
136
137 if (ctx.SelectMessageByOrdinalRangeInDetailLane is null)
138 return ChatSlashIntercomResult.Fail("Выбор сообщения недоступен.");
139
140 var range = segments[0];
141 var result = ctx.SelectMessageByOrdinalRangeInDetailLane(range.Start, range.End);
142 if (!string.Equals(result, "OK", StringComparison.Ordinal))
143 return ChatSlashIntercomResult.Fail(result);
144
145 var text = range.Start == range.End
146 ? $"Выбрано сообщение #{range.Start}."
147 : $"Выбран диапазон #{range.Start}–#{range.End} (активно #{range.End}).";
148 return ChatSlashIntercomResult.Ok(text);
149 }
150
151 private static ChatSlashIntercomResult executeMessageSelectClear(Context ctx)
152 {
153 if (ctx.ClearMessageSelectionInDetailLane is null)
154 return ChatSlashIntercomResult.Fail("Сброс выбора сообщений недоступен.");
155
156 var tail = ctx.ArgsTail?.Trim() ?? "";
157 if (tail.Length > 0)
158 return ChatSlashIntercomResult.Fail("Ожидается «/intercom message select clear» без аргументов.");
159
160 var result = ctx.ClearMessageSelectionInDetailLane();
161 return string.Equals(result, "OK", StringComparison.Ordinal)
162 ? ChatSlashIntercomResult.Ok("Подсветка сообщений в ветке сброшена.")
163 : ChatSlashIntercomResult.Fail(result);
164 }
165
166 private static ChatSlashIntercomResult executeMessageFind(Context ctx)
167 {
168 if (ctx.FindMessagesForCodeRef is null)
169 return ChatSlashIntercomResult.Fail("Поиск сообщений по коду недоступен.");
170
171 var result = ctx.FindMessagesForCodeRef(ctx.ArgsTail);
172 return result.StartsWith("Связанные сообщения:", StringComparison.Ordinal)
173 ? ChatSlashIntercomResult.Ok(result)
174 : ChatSlashIntercomResult.Fail(result);
175 }
176
177 private static ChatSlashIntercomResult executeMessageRelate(Context ctx)
178 {
179 if (ctx.RelateMessageRangeToCodeRef is null)
180 return ChatSlashIntercomResult.Fail("Relate недоступен.");
181
182 var result = ctx.RelateMessageRangeToCodeRef(ctx.ArgsTail);
183 return result.StartsWith("Связь сообщений", StringComparison.Ordinal)
184 ? ChatSlashIntercomResult.Ok(result)
185 : ChatSlashIntercomResult.Fail(result);
186 }
187
188 private static ChatSlashIntercomResult executeMessageAnchorsList(Context ctx)
189 {
190 if (ctx.ListMessageAnchors is null)
191 return ChatSlashIntercomResult.Fail("Список якорей недоступен.");
192
193 return ChatSlashIntercomResult.Ok(ctx.ListMessageAnchors());
194 }
195
196 private static ChatSlashIntercomResult executeAdmin(Context ctx, string handlerId)
197 {
198 if (ctx.RunAdmin is null)
199 return ChatSlashIntercomResult.Fail("Intercom admin недоступен в этой сессии.");
200
201 return ctx.RunAdmin(handlerId, ctx.ArgsTail, CancellationToken.None)
202 .ConfigureAwait(false)
203 .GetAwaiter()
204 .GetResult();
205 }
206
207 private static ChatSlashIntercomResult executeAnchorPeek(Context ctx)
208 {
209 if (ctx.PeekAnchorById is null)
210 return ChatSlashIntercomResult.Fail("Peek якоря недоступен.");
211
212 var result = ctx.PeekAnchorById(ctx.ArgsTail);
213 if (result.Contains("не найден", StringComparison.OrdinalIgnoreCase)
214 || result.Contains("Укажи id", StringComparison.OrdinalIgnoreCase)
215 || result.Contains("8 hex", StringComparison.OrdinalIgnoreCase))
216 {
217 return ChatSlashIntercomResult.Fail(result);
218 }
219
220 return ChatSlashIntercomResult.Ok(result);
221 }
222
223 public static bool IsKnown(string handlerId) => Handlers.ContainsKey(handlerId);
224
225 public static bool TryExecute(string handlerId, Context context, out ChatSlashIntercomResult result)
226 {
227 if (Handlers.TryGetValue(handlerId, out var handler))
228 {
229 result = handler(context);
230 return true;
231 }
232
233 result = ChatSlashIntercomResult.Fail("");
234 return false;
235 }
236}
237
View only · write via MCP/CIDE