Forge
csharpdeeb25a2
1#nullable enable
2
3using CascadeIDE.Services;
4
5namespace CascadeIDE.Features.Chat;
6
7/// <summary>Индекс slash-маршрутов из <see cref="IntentSlashCatalog"/> (ADR 0119, 0150).</summary>
8internal static class SlashRouteCatalogIndex
9{
10 private static readonly Lazy<Snapshot> Lazy = new(static () => Build(IntentSlashCatalog.SlashRoutes));
11
12 public static bool TryGetRoute(string slashPath, out SlashRouteEntry route)
13 {
14 if (Lazy.Value.ByPath.TryGetValue(IntentSlashCatalog.NormalizeSlashPath(slashPath), out route))
15 return true;
16
17 return CascadeIDE.Features.Forge.Infrastructure.ForgeSlashCatalogOverlay.TryGetRoute(slashPath, out route);
18 }
19
20 public static bool IsKnownIntercomInnerVerb(string group, string verb)
21 {
22 if (string.IsNullOrWhiteSpace(group) || string.IsNullOrWhiteSpace(verb))
23 return false;
24
25 return Lazy.Value.IntercomInnerVerbs.Contains((group.ToLowerInvariant(), verb.ToLowerInvariant()));
26 }
27
28 public static SlashArgTailKind GetArgTailKind(string slashPath)
29 {
30 if (CascadeIDE.Features.Forge.Infrastructure.ForgeSlashCatalogOverlay.TryGetRoute(slashPath, out var overlayRoute))
31 return overlayRoute.ArgTailKindExplicit ?? SlashArgTailKind.None;
32
33 var key = IntentSlashCatalog.NormalizeSlashPath(slashPath);
34 if (SlashRouteCatalogPathsGenerated.TryGetArgTailKind(key, out var generated))
35 return (SlashArgTailKind)generated;
36
37 if (Lazy.Value.ArgTailKind.TryGetValue(key, out var kind))
38 return kind;
39
40 return SlashArgTailKind.None;
41 }
42
43 public static bool RouteRequiresArgTail(string slashPath) =>
44 GetArgTailKind(slashPath) == SlashArgTailKind.Required;
45
46 public static bool AcceptsOptionalArgTail(string slashPath) =>
47 GetArgTailKind(slashPath) == SlashArgTailKind.Optional;
48
49 public static bool TryGetIntercomHandler(string slashPath, out string? handlerId)
50 {
51 handlerId = null;
52 if (!TryGetRoute(slashPath, out var route))
53 return false;
54
55 handlerId = route.IntercomHandlerId;
56 return !string.IsNullOrWhiteSpace(handlerId);
57 }
58
59 private static Snapshot Build(IReadOnlyDictionary<string, SlashRouteEntry> routes)
60 {
61 var byPath = new Dictionary<string, SlashRouteEntry>(routes, StringComparer.OrdinalIgnoreCase);
62 var intercomVerbs = new HashSet<(string Group, string Verb)>();
63 var argTailKind = new Dictionary<string, SlashArgTailKind>(StringComparer.OrdinalIgnoreCase);
64 var paths = routes.Keys.OrderBy(static p => p.Length).ToList();
65
66 foreach (var path in paths)
67 {
68 if (!path.StartsWith("/intercom ", StringComparison.OrdinalIgnoreCase))
69 continue;
70
71 var body = path["/intercom ".Length..];
72 var space = body.IndexOf(' ');
73 if (space < 0)
74 continue;
75
76 var group = body[..space];
77 var rest = body[(space + 1)..].Trim();
78 var verbSpace = rest.IndexOf(' ');
79 var verb = verbSpace < 0 ? rest : rest[..verbSpace];
80 if (verb.Length > 0)
81 intercomVerbs.Add((group.ToLowerInvariant(), verb.ToLowerInvariant()));
82 }
83
84 foreach (var path in paths)
85 argTailKind[path] = inferArgTailKind(path, byPath, paths);
86
87 return new Snapshot(byPath, intercomVerbs, argTailKind);
88 }
89
90 /// <summary>Fallback, если в TOML нет <c>arg_tail</c> (ADR 0150: в bundled catalog — всегда явно).</summary>
91 private static SlashArgTailKind inferArgTailKind(
92 string path,
93 IReadOnlyDictionary<string, SlashRouteEntry> byPath,
94 IReadOnlyList<string> allPaths)
95 {
96 if (!byPath.TryGetValue(path, out var route))
97 return SlashArgTailKind.None;
98
99 if (route.ArgTailKindExplicit is { } explicitKind)
100 return explicitKind;
101
102 if (route.AutoRunOnCommit && !route.AutoRunRequiresArgs)
103 return SlashArgTailKind.None;
104
105 if (route.Completion != SlashCompletionKind.None)
106 return SlashArgTailKind.Required;
107
108 var prefix = path + " ";
109 if (allPaths.Any(p => p.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)))
110 return SlashArgTailKind.Required;
111
112 if (path.EndsWith(" open", StringComparison.OrdinalIgnoreCase)
113 && !path.Contains("dialog", StringComparison.OrdinalIgnoreCase))
114 {
115 return SlashArgTailKind.Required;
116 }
117
118 if (!string.IsNullOrWhiteSpace(route.CommandId)
119 && IdeCommandsArgs.TryGetArgs(route.CommandId, out var args)
120 && args.Length > 0
121 && args.All(static a => !a.Required))
122 {
123 return SlashArgTailKind.Optional;
124 }
125
126 return SlashArgTailKind.None;
127 }
128
129 private sealed record Snapshot(
130 Dictionary<string, SlashRouteEntry> ByPath,
131 HashSet<(string Group, string Verb)> IntercomInnerVerbs,
132 Dictionary<string, SlashArgTailKind> ArgTailKind);
133}
134
View only · write via MCP/CIDE