| 1 | #nullable enable |
| 2 | using CascadeIDE.Services; |
| 3 | |
| 4 | namespace CascadeIDE.Features.Chat; |
| 5 | |
| 6 | /// <summary>Локальные действия Intercom из slash (<c>kind=intercom</c>).</summary> |
| 7 | public static class ChatSlashIntercomActions |
| 8 | { |
| 9 | public static bool TryExecute( |
| 10 | string slashPath, |
| 11 | string? argsTail, |
| 12 | Guid selectedThreadId, |
| 13 | Action<Guid> selectThread, |
| 14 | Action<bool> setOverviewMode, |
| 15 | ChatSurfaceSnapshot snapshot, |
| 16 | out ChatSlashIntercomResult result, |
| 17 | Action<TopicPickerPresentation>? setTopicPicker = null, |
| 18 | Func<string, TopicCreateResult>? createTopicWithTitle = null, |
| 19 | Func<Guid, string, TopicRenameResult>? renameTopicWithTitle = null, |
| 20 | Func<string, string?, ChatSlashIntercomResult>? tryAttachSlash = null, |
| 21 | Func<int, int, string>? selectMessageByOrdinalRangeInDetailLane = null, |
| 22 | Func<IReadOnlyList<ParametricIntRange>, string>? selectMessagesByOrdinalRangesInDetailLane = null, |
| 23 | Func<string>? clearMessageSelectionInDetailLane = null, |
| 24 | Func<string?, string>? findMessagesForCodeRef = null, |
| 25 | Func<string?, string>? relateMessageRangeToCodeRef = null, |
| 26 | Func<string>? listMessageAnchors = null, |
| 27 | Func<string?, string>? peekAnchorById = null, |
| 28 | Func<string, string?, CancellationToken, Task<ChatSlashIntercomResult>>? runIntercomAdmin = null) |
| 29 | { |
| 30 | result = ChatSlashIntercomResult.Fail(""); |
| 31 | if (!IntentSlashCatalog.TryGetRoute(slashPath, out var route) |
| 32 | || route.ExecutionKind != ChatSlashCommandExecutionKind.LocalIntercom) |
| 33 | { |
| 34 | return false; |
| 35 | } |
| 36 | |
| 37 | if (string.IsNullOrWhiteSpace(route.IntercomHandlerId) |
| 38 | || !ChatSlashIntercomHandlers.TryExecute( |
| 39 | route.IntercomHandlerId, |
| 40 | new ChatSlashIntercomHandlers.Context( |
| 41 | argsTail, |
| 42 | selectedThreadId, |
| 43 | selectThread, |
| 44 | setOverviewMode, |
| 45 | snapshot, |
| 46 | setTopicPicker, |
| 47 | createTopicWithTitle, |
| 48 | renameTopicWithTitle, |
| 49 | tryAttachSlash, |
| 50 | selectMessageByOrdinalRangeInDetailLane, |
| 51 | selectMessagesByOrdinalRangesInDetailLane, |
| 52 | clearMessageSelectionInDetailLane, |
| 53 | findMessagesForCodeRef, |
| 54 | relateMessageRangeToCodeRef, |
| 55 | listMessageAnchors, |
| 56 | peekAnchorById, |
| 57 | runIntercomAdmin), |
| 58 | out result)) |
| 59 | { |
| 60 | result = ChatSlashIntercomResult.Fail($"Неизвестное действие: {slashPath}"); |
| 61 | return true; |
| 62 | } |
| 63 | |
| 64 | return true; |
| 65 | } |
| 66 | |
| 67 | internal static ChatSlashIntercomResult CreateTopic( |
| 68 | string? title, |
| 69 | Func<string, TopicCreateResult>? createTopicWithTitle) |
| 70 | { |
| 71 | if (createTopicWithTitle is null) |
| 72 | return ChatSlashIntercomResult.Fail("Создание тем недоступно."); |
| 73 | |
| 74 | var create = createTopicWithTitle(title ?? ""); |
| 75 | return create.Success |
| 76 | ? ChatSlashIntercomResult.Ok(create.Message) |
| 77 | : ChatSlashIntercomResult.Fail(create.Message); |
| 78 | } |
| 79 | |
| 80 | internal static ChatSlashIntercomResult RenameTopic( |
| 81 | Guid selectedThreadId, |
| 82 | string? title, |
| 83 | Func<Guid, string, TopicRenameResult>? renameTopicWithTitle) |
| 84 | { |
| 85 | if (renameTopicWithTitle is null) |
| 86 | return ChatSlashIntercomResult.Fail("Переименование тем недоступно."); |
| 87 | |
| 88 | var rename = renameTopicWithTitle(selectedThreadId, title ?? ""); |
| 89 | return rename.Success |
| 90 | ? ChatSlashIntercomResult.Ok(rename.Message) |
| 91 | : ChatSlashIntercomResult.Fail(rename.Message); |
| 92 | } |
| 93 | |
| 94 | internal static ChatSlashIntercomResult ShowTopicPicker( |
| 95 | TopicPickerPresentation mode, |
| 96 | Action<TopicPickerPresentation>? setTopicPicker, |
| 97 | Action<bool> setOverviewMode, |
| 98 | ChatSurfaceSnapshot snapshot) |
| 99 | { |
| 100 | if (setTopicPicker is null) |
| 101 | return ChatSlashIntercomResult.Fail("Интерактивный список тем недоступен. Для агента: /intercom topic list text."); |
| 102 | |
| 103 | if (snapshot.State.Threads.Count == 0) |
| 104 | return ChatSlashIntercomResult.Fail(ChatThreadPresentation.EmptyTopicsHint); |
| 105 | |
| 106 | setOverviewMode(false); |
| 107 | setTopicPicker(mode); |
| 108 | var text = mode == TopicPickerPresentation.Tree |
| 109 | ? "Дерево тем в ленте — клик по строке, чтобы открыть." |
| 110 | : "Список тем в ленте — клик по строке, чтобы открыть."; |
| 111 | return ChatSlashIntercomResult.Ok(text); |
| 112 | } |
| 113 | |
| 114 | public static ChatSlashIntercomResult OpenTopic( |
| 115 | string? query, |
| 116 | Guid selectedThreadId, |
| 117 | Action<Guid> selectThread, |
| 118 | Action<bool> setOverviewMode, |
| 119 | ChatSurfaceSnapshot snapshot) |
| 120 | { |
| 121 | if (!ChatSlashTopicResolver.TryResolve(query, snapshot, selectedThreadId, out var thread, out var error)) |
| 122 | return ChatSlashIntercomResult.Fail(error ?? "Не удалось открыть тему."); |
| 123 | |
| 124 | selectThread(thread.ThreadId); |
| 125 | setOverviewMode(false); |
| 126 | return ChatSlashIntercomResult.Ok($"Открыта тема: {thread.Title}"); |
| 127 | } |
| 128 | |
| 129 | public static ChatSlashIntercomResult OpenTopicCards(Action<bool> setOverviewMode, ChatSurfaceSnapshot snapshot) |
| 130 | { |
| 131 | setOverviewMode(true); |
| 132 | if (!snapshot.ProductSpine.HasContent) |
| 133 | return ChatSlashIntercomResult.Ok( |
| 134 | "Картотека тем. Spine пуст — /intercom spine set <текст>. Открыть тему: /intercom topic open <имя>."); |
| 135 | |
| 136 | var title = ChatProductSpinePresentation.ResolveLineTitle(snapshot.ProductSpine); |
| 137 | return ChatSlashIntercomResult.Ok( |
| 138 | $"Картотека тем: spine «{title}». /intercom topic open <имя> — открыть тему."); |
| 139 | } |
| 140 | } |
| 141 | |