Forge
csharpdeeb25a2
1using System.Text.Json;
2using System.Text.Json.Serialization;
3using Avalonia;
4using Avalonia.Controls;
5using Avalonia.Controls.ApplicationLifetimes;
6using Avalonia.VisualTree;
7using CascadeIDE.Features.UiChrome;
8using CascadeIDE.Views;
9
10namespace CascadeIDE.Cockpit.Surface;
11
12/// <summary>
13/// Слой <strong>поверхности</strong> (ADR 0036 п.4): снимок дерева контролов для <c>ide_get_ui_layout</c>.
14/// Вызывать из UI-потока, передать корень (<see cref="Window"/>). Ортогонально CDS (<see cref="CockpitSurfaceSnapshotBuilder"/>).
15/// </summary>
16public static class UiLayoutSnapshot
17{
18 private const int MaxDepth = 14;
19 /// <summary>Лимит текста в <c>content</c> для TextBlock и длинных подписей (паритет с чтением окон помимо главного).</summary>
20 private const int LayoutContentMaxChars = 480;
21
22 private static readonly JsonSerializerOptions Options = new()
23 {
24 WriteIndented = true,
25 DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
26 };
27
28 public static string BuildJson(Visual root)
29 {
30 var node = BuildNode(root, root as Visual, 0);
31 return JsonSerializer.Serialize(node, Options);
32 }
33
34 /// <summary>
35 /// Полный снимок layout для MCP: по одному дереву на каждое открытое <see cref="Window"/> (главное, окно-хост Mfd, настройки, превью).
36 /// Паритет с человеком/агентом при мультиоконности (ADR 0012, 0017).
37 /// </summary>
38 public static string BuildJsonAllWindows(Window mainWindow)
39 {
40 var list = new List<object>();
41 if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
42 {
43 foreach (var w in desktop.Windows)
44 {
45 if (w is Window win)
46 list.Add(BuildWindowEntry(win, mainWindow));
47 }
48 }
49 else
50 {
51 list.Add(BuildWindowEntry(mainWindow, mainWindow));
52 }
53
54 var payload = new Dictionary<string, object?> { ["windows"] = list };
55 return JsonSerializer.Serialize(payload, Options);
56 }
57
58 /// <summary>Роль окна для MCP (снимок layout, PNG по всем окнам).</summary>
59 public static string GetWindowRole(Window win, Window mainWindow) =>
60 ReferenceEquals(win, mainWindow)
61 ? "main"
62 : win is MfdHostWindow
63 ? "mfd_host"
64 : win is PfdHostWindow
65 ? "pfd_host"
66 : "other";
67
68 private static Dictionary<string, object?> BuildWindowEntry(Window win, Window mainWindow)
69 {
70 var role = GetWindowRole(win, mainWindow);
71
72 return new Dictionary<string, object?>
73 {
74 ["role"] = role,
75 ["window_type"] = win.GetType().Name,
76 ["title"] = win.Title ?? "",
77 ["is_active"] = win.IsActive,
78 ["root"] = BuildNode(win, win, 0)
79 };
80 }
81
82 private static object BuildNode(Visual root, Visual? visual, int depth)
83 {
84 if (visual is null || depth > MaxDepth)
85 return new { type = "?", skip = true };
86
87 var control = visual as Control;
88 var name = (visual as StyledElement)?.Name ?? "";
89 var visible = control?.IsVisible ?? true;
90 var typeName = visual.GetType().Name;
91
92 double x = 0, y = 0, w = visual.Bounds.Width, h = visual.Bounds.Height;
93 if (root is Visual rootVisual)
94 {
95 var topLeft = visual.TranslatePoint(new Point(0, 0), rootVisual);
96 if (topLeft is { } p)
97 {
98 x = p.X;
99 y = p.Y;
100 }
101 }
102
103 var content = GetContent(control);
104 var children = new List<object>();
105 if (depth < MaxDepth)
106 {
107 foreach (var child in visual.GetVisualChildren())
108 children.Add(BuildNode(root, child, depth + 1));
109 }
110
111 var node = new Dictionary<string, object?>
112 {
113 ["type"] = typeName,
114 ["name"] = name ?? "",
115 ["visible"] = visible,
116 ["bounds"] = new Dictionary<string, double> { ["x"] = Math.Round(x, 1), ["y"] = Math.Round(y, 1), ["w"] = Math.Round(w, 1), ["h"] = Math.Round(h, 1) },
117 ["content"] = content,
118 ["children"] = children.Count > 0 ? children : null
119 };
120 // Канонический id из ADR (в т.ч. eicas — канал, не якорь-колонка).
121 if (visual is AttentionZoneContainer zc)
122 node["attention_zone"] = zc.Zone.ToCanonicalId();
123 return node;
124 }
125
126 private static string? GetContent(Control? c)
127 {
128 if (c is null)
129 return null;
130 if (c is ContentControl cc && cc.Content is string s)
131 return Trunc(s, LayoutContentMaxChars);
132 if (c is ContentControl cc2 && cc2.Content is not null)
133 return Trunc(cc2.Content.ToString() ?? "", LayoutContentMaxChars);
134 if (c is TextBlock tb)
135 return Trunc(tb.Text ?? "", LayoutContentMaxChars);
136 if (c is TextBox tbx)
137 return Trunc(tbx.Text ?? "", LayoutContentMaxChars);
138 if (c is Button btn)
139 return Trunc(btn.Content?.ToString() ?? "", LayoutContentMaxChars);
140 if (c is MenuItem mi)
141 return Trunc(mi.Header?.ToString() ?? "", LayoutContentMaxChars);
142 return null;
143 }
144
145 private static string Trunc(string s, int max = 80)
146 {
147 if (string.IsNullOrEmpty(s)) return s;
148 s = s.Replace("\r", " ").Replace("\n", " ");
149 return s.Length <= max ? s : s[..max] + "...";
150 }
151}
152
View only · write via MCP/CIDE