Forge
csharpdeeb25a2
1#nullable enable
2using CascadeIDE.Features.Chat;
3using CascadeIDE.Models;
4using CascadeIDE.Models.Intercom;
5using CascadeIDE.Services;
6
7namespace CascadeIDE.Views.Chat.Skia;
8
9/// <summary>Creator: snapshot → список <see cref="ISkiaChatEntity"/> (ADR entity pipeline).</summary>
10internal static class ChatSurfaceEntityFactory
11{
12 public static IReadOnlyList<ISkiaChatEntity> Build(
13 ChatSurfaceSnapshot snapshot,
14 bool overviewMode,
15 Guid detailThreadId,
16 bool forwardHost = false,
17 IntercomFontsSettings? intercomFonts = null,
18 bool compactSideHost = false)
19 {
20 var entities = new List<ISkiaChatEntity>();
21 var hideNavChromeInFeed = forwardHost || compactSideHost;
22
23 if (!hideNavChromeInFeed)
24 AppendSpine(entities, snapshot.ProductSpine, overviewMode, forwardHost, intercomFonts);
25
26 if (snapshot.Layout.Overview.Count > 0)
27 {
28 AppendOverview(entities, snapshot, overviewMode, detailThreadId, forwardHost, hideNavChromeInFeed);
29 if (overviewMode)
30 return entities;
31 }
32
33 if (!overviewMode && snapshot.TopicPicker != TopicPickerPresentation.None)
34 {
35 var counts = ChatThreadPresentation.MessageCountsByThread(snapshot);
36 entities.Add(new SkiaChatTopicPickerEntity(
37 snapshot.TopicPicker,
38 snapshot.State.Threads,
39 counts));
40 }
41
42 AppendDetailLanes(entities, snapshot, detailThreadId, forwardHost, hideNavChromeInFeed, intercomFonts);
43 return entities;
44 }
45
46 private static void AppendSpine(
47 List<ISkiaChatEntity> entities,
48 ChatProductSpine spine,
49 bool overviewMode,
50 bool forwardHost,
51 IntercomFontsSettings? intercomFonts)
52 {
53 if (!spine.HasContent)
54 return;
55
56 var title = ChatProductSpinePresentation.ResolveLineTitle(spine);
57 var cardTitleHeight = (intercomFonts ?? IntercomFontDefaults.Intercom).ResolveCardTitleLineHeight(forwardHost);
58 if (overviewMode)
59 {
60 entities.Add(SkiaChatTopicCard.ForSpine(
61 title,
62 spine.CurrentFocus,
63 spine.Milestones,
64 spine.IncludeInAgentContext));
65 return;
66 }
67
68 entities.Add(new SkiaChatBubbleEntity(
69 D(
70 new SkiaChatBubbleSpec(
71 "Spine · " + title,
72 ChatProductSpinePresentation.FormatDetailStripFocus(spine.CurrentFocus),
73 Footer: null,
74 SkiaChatBubbleKind.SpineStrip,
75 SkiaBubbleFillRole.SpineStrip,
76 SkiaChatBodyTone.Normal,
77 IsPending: false,
78 IsSelected: false,
79 StartsBranch: false,
80 MessageIndex: null,
81 MinHeight: 44,
82 MaxBodyLines: 1,
83 GapAfter: 6,
84 Padding: 8,
85 TitleHeight: cardTitleHeight,
86 LineHeight: 15),
87 forwardHost),
88 static () => new SkiaChatHit(null, null, ResetDetailMode: true)));
89 }
90
91 private static void AppendOverview(
92 List<ISkiaChatEntity> entities,
93 ChatSurfaceSnapshot snapshot,
94 bool overviewMode,
95 Guid detailThreadId,
96 bool forwardHost,
97 bool hideNavChromeInFeed)
98 {
99 if (!overviewMode && !hideNavChromeInFeed)
100 entities.Add(OverviewBackLink(forwardHost));
101
102 foreach (var item in snapshot.Layout.Overview)
103 {
104 if (overviewMode)
105 {
106 entities.Add(SkiaChatTopicCard.ForThread(
107 ChatThreadOverviewPresentation.FormatTopicTitle(item),
108 item.Summary,
109 ChatThreadOverviewPresentation.FormatTopicBadges(item),
110 detailThreadId == item.ThreadId,
111 item.ThreadId));
112 continue;
113 }
114
115 if (hideNavChromeInFeed)
116 continue;
117
118 var fillRole = detailThreadId == item.ThreadId
119 ? SkiaBubbleFillRole.ThreadRowActive
120 : SkiaBubbleFillRole.ThreadRow;
121 entities.Add(new SkiaChatBubbleEntity(
122 D(
123 new SkiaChatBubbleSpec(
124 ChatThreadOverviewPresentation.FormatSideThreadRowTitle(item),
125 ChatThreadOverviewPresentation.FormatSideThreadRowMeta(item),
126 Footer: null,
127 SkiaChatBubbleKind.Feed,
128 fillRole,
129 SkiaChatBodyTone.Normal,
130 IsPending: false,
131 IsSelected: false,
132 StartsBranch: false,
133 MessageIndex: null),
134 forwardHost),
135 () => new SkiaChatHit(null, item.ThreadId, ResetDetailMode: false)));
136 }
137 }
138
139 private static SkiaChatBubbleEntity OverviewBackLink(bool forwardHost) =>
140 new(
141 D(
142 new SkiaChatBubbleSpec(
143 "Темы",
144 "Назад к обзору всех веток",
145 Footer: null,
146 SkiaChatBubbleKind.Feed,
147 SkiaBubbleFillRole.OverviewNav,
148 SkiaChatBodyTone.Normal,
149 IsPending: false,
150 IsSelected: false,
151 StartsBranch: false,
152 MessageIndex: null),
153 forwardHost),
154 static () => new SkiaChatHit(null, null, ResetDetailMode: true));
155
156 private static void AppendDetailLanes(
157 List<ISkiaChatEntity> entities,
158 ChatSurfaceSnapshot snapshot,
159 Guid detailThreadId,
160 bool forwardHost,
161 bool hideNavChromeInFeed,
162 IntercomFontsSettings? intercomFonts)
163 {
164 var cardTitleHeight = (intercomFonts ?? IntercomFontDefaults.Intercom).ResolveCardTitleLineHeight(forwardHost);
165 var lanes = snapshot.Layout.Lanes.OrderBy(lane => lane.Thread.Order);
166 if (detailThreadId != Guid.Empty)
167 lanes = lanes.Where(lane => lane.Thread.ThreadId == detailThreadId).OrderBy(lane => lane.Thread.Order);
168
169 foreach (var lane in lanes)
170 {
171 if (!hideNavChromeInFeed)
172 {
173 var headerRole = lane.Thread.IsActive
174 ? SkiaBubbleFillRole.ThreadHeaderActive
175 : SkiaBubbleFillRole.ThreadHeader;
176 entities.Add(new SkiaChatBubbleEntity(
177 D(
178 new SkiaChatBubbleSpec(
179 lane.Thread.Title,
180 ChatThreadOverviewPresentation.FormatThreadHeaderMeta(lane.Thread),
181 Footer: null,
182 SkiaChatBubbleKind.Feed,
183 headerRole,
184 SkiaChatBodyTone.Normal,
185 IsPending: false,
186 IsSelected: false,
187 StartsBranch: false,
188 MessageIndex: null,
189 TitleHeight: cardTitleHeight),
190 forwardHost),
191 () => new SkiaChatHit(null, lane.Thread.ThreadId, ResetDetailMode: false)));
192 }
193
194 ChatMessageVisualRole? previousMessageRole = null;
195 var feedOrdinal = 0;
196 var showFeedGutter = detailThreadId != Guid.Empty;
197 foreach (var entry in lane.Entries)
198 {
199 if (entry.Kind == ChatSurfaceEntryKind.Message)
200 {
201 if (showFeedGutter)
202 feedOrdinal++;
203 var suppressTitle = previousMessageRole == entry.VisualRole
204 && !entry.StartsBranch
205 && entry.VisualRole is ChatMessageVisualRole.User
206 or ChatMessageVisualRole.Assistant
207 or ChatMessageVisualRole.Thinking;
208 var gapAfter = suppressTitle
209 ? forwardHost ? 3f : 6f
210 : forwardHost ? 6f : 16f;
211 entities.Add(MessageEntity(
212 entry,
213 forwardHost,
214 suppressTitle,
215 gapAfter,
216 showFeedGutter ? feedOrdinal : 0,
217 intercomFonts));
218 previousMessageRole = entry.VisualRole;
219 continue;
220 }
221
222 if (entry.Kind is ChatSurfaceEntryKind.Confirmation or ChatSurfaceEntryKind.SedmCard)
223 {
224 entities.Add(ConfirmationEntity(entry, forwardHost));
225 previousMessageRole = null;
226 continue;
227 }
228 }
229 }
230 }
231
232 private static SkiaChatBubbleSpec D(in SkiaChatBubbleSpec spec, bool forwardHost) =>
233 SkiaChatDensity.Apply(spec, forwardHost);
234
235 private static ISkiaChatEntity MessageEntity(
236 ChatSurfaceEntry entry,
237 bool forwardHost,
238 bool suppressTitle,
239 float gapAfter,
240 int feedOrdinal = 0,
241 IntercomFontsSettings? intercomFonts = null)
242 {
243 if (!string.IsNullOrWhiteSpace(entry.SlashCommandPath))
244 return new SkiaChatSlashCommandEntity(entry, forwardHost, feedOrdinal, intercomFonts);
245
246 return new SkiaChatMessageFeedEntity(
247 entry,
248 forwardHost,
249 suppressTitle,
250 gapAfter,
251 feedOrdinal,
252 intercomFonts);
253 }
254
255 private static SkiaChatBubbleEntity ConfirmationEntity(ChatSurfaceEntry entry, bool forwardHost)
256 {
257 var fillRole = SkiaBubbleFillRoleMapping.FromMessageRole(entry.VisualRole);
258 return new SkiaChatBubbleEntity(
259 D(
260 new SkiaChatBubbleSpec(
261 entry.Title,
262 entry.Body,
263 Footer: null,
264 SkiaChatBubbleKind.Feed,
265 fillRole,
266 SkiaChatBodyTone.Normal,
267 IsPending: entry.IsPending,
268 entry.IsSelected,
269 StartsBranch: false,
270 MessageIndex: null,
271 GapAfter: 5,
272 Padding: 0,
273 TitleHeight: 14,
274 LineHeight: 14),
275 forwardHost),
276 static () => new SkiaChatHit(null, null, ResetDetailMode: false));
277 }
278}
279
View only · write via MCP/CIDE