Forge
csharpdeeb25a2
1#nullable enable
2using System.Text.Json;
3using CascadeIDE.Models.AgentChat;
4using CascadeIDE.Services;
5using CommunityToolkit.Mvvm.ComponentModel;
6
7namespace CascadeIDE.Features.Chat;
8
9public partial class ChatPanelViewModel
10{
11 private bool _suppressProductSpinePersistence;
12
13 [ObservableProperty]
14 private string _productSpineLineTitle = "";
15
16 [ObservableProperty]
17 private string _productSpineCurrentFocus = "";
18
19 [ObservableProperty]
20 private string _productSpineMilestonesText = "";
21
22 [ObservableProperty]
23 private bool _includeProductSpineInAgentContext;
24
25 partial void OnProductSpineLineTitleChanged(string value) => OnProductSpineFieldChanged();
26
27 partial void OnProductSpineCurrentFocusChanged(string value) => OnProductSpineFieldChanged();
28
29 partial void OnProductSpineMilestonesTextChanged(string value) => OnProductSpineFieldChanged();
30
31 partial void OnIncludeProductSpineInAgentContextChanged(bool value) => OnProductSpineFieldChanged();
32
33 public string ToggleProductSpineInAgentContext()
34 {
35 IncludeProductSpineInAgentContext = !IncludeProductSpineInAgentContext;
36 return IncludeProductSpineInAgentContext ? "ProductSpineInAgentContext=on" : "ProductSpineInAgentContext=off";
37 }
38
39 public string GetProductSpineJson()
40 {
41 var spine = BuildProductSpine();
42 return JsonSerializer.Serialize(new
43 {
44 line_title = spine.LineTitle,
45 current_focus = spine.CurrentFocus,
46 milestones = spine.Milestones,
47 include_in_agent_context = spine.IncludeInAgentContext,
48 has_content = spine.HasContent
49 }, ChatPanelJson);
50 }
51
52 public string SetProductSpineFromMcp(IReadOnlyDictionary<string, JsonElement>? args)
53 {
54 if (args is null || args.Count == 0)
55 return "No spine fields provided";
56
57 var hasAny = false;
58 _suppressProductSpinePersistence = true;
59 try
60 {
61 if (args.ContainsKey("line_title"))
62 {
63 ProductSpineLineTitle = McpCommandJsonArgs.String(args, "line_title") ?? "";
64 hasAny = true;
65 }
66
67 if (args.ContainsKey("current_focus"))
68 {
69 ProductSpineCurrentFocus = McpCommandJsonArgs.String(args, "current_focus") ?? "";
70 hasAny = true;
71 }
72
73 if (args.ContainsKey("milestones"))
74 {
75 ProductSpineMilestonesText = McpCommandJsonArgs.String(args, "milestones") ?? "";
76 hasAny = true;
77 }
78
79 if (McpCommandJsonArgs.OptionalBool(args, "include_in_agent_context") is { } include)
80 {
81 IncludeProductSpineInAgentContext = include;
82 hasAny = true;
83 }
84 }
85 finally
86 {
87 _suppressProductSpinePersistence = false;
88 }
89
90 if (!hasAny)
91 return "No spine fields provided";
92
93 _ = PersistProductSpineMetadataAsync();
94 RefreshChatSurfaceSnapshot();
95 return "OK";
96 }
97
98 private ChatProductSpine BuildProductSpine() =>
99 new(
100 ProductSpineLineTitle.Trim(),
101 ProductSpineCurrentFocus.Trim(),
102 ChatProductSpine.ParseMilestonesText(ProductSpineMilestonesText),
103 IncludeProductSpineInAgentContext);
104
105 private string ApplyProductSpineToOutboundMessage(string input)
106 {
107 var prefix = BuildProductSpine().BuildAgentContextPrefix();
108 return prefix is null ? input : prefix + Environment.NewLine + input;
109 }
110
111 private void OnProductSpineFieldChanged()
112 {
113 RefreshChatSurfaceSnapshot();
114 if (!_suppressProductSpinePersistence)
115 _ = PersistProductSpineMetadataAsync();
116 }
117
118 private void ApplyProductSpineFromMetadata(ChatSessionMetadata meta)
119 {
120 _suppressProductSpinePersistence = true;
121 try
122 {
123 ProductSpineLineTitle = string.IsNullOrWhiteSpace(meta.ProductSpineLineTitle)
124 ? ResolveDefaultProductSpineLineTitle()
125 : meta.ProductSpineLineTitle.Trim();
126 ProductSpineCurrentFocus = meta.ProductSpineCurrentFocus?.Trim() ?? "";
127 ProductSpineMilestonesText = meta.ProductSpineMilestones?.Trim() ?? "";
128 IncludeProductSpineInAgentContext = meta.ProductSpineIncludeInAgentContext;
129 }
130 finally
131 {
132 _suppressProductSpinePersistence = false;
133 }
134 }
135
136 private string ResolveDefaultProductSpineLineTitle()
137 {
138 var root = _getWorkspaceRoot().Trim();
139 if (string.IsNullOrEmpty(root))
140 return ChatProductSpinePresentation.DefaultLineTitle;
141 try
142 {
143 return new DirectoryInfo(root).Name;
144 }
145 catch
146 {
147 return ChatProductSpinePresentation.DefaultLineTitle;
148 }
149 }
150
151 private async Task PersistProductSpineMetadataAsync()
152 {
153 try
154 {
155 var meta = await _sessionStore.LoadOrCreateMetadataAsync(_sessionId, CancellationToken.None).ConfigureAwait(false);
156 var updated = meta with
157 {
158 UpdatedAtUtc = DateTimeOffset.UtcNow,
159 ProductSpineLineTitle = string.IsNullOrWhiteSpace(ProductSpineLineTitle) ? null : ProductSpineLineTitle.Trim(),
160 ProductSpineCurrentFocus = string.IsNullOrWhiteSpace(ProductSpineCurrentFocus) ? null : ProductSpineCurrentFocus.Trim(),
161 ProductSpineMilestones = string.IsNullOrWhiteSpace(ProductSpineMilestonesText) ? null : ProductSpineMilestonesText.Trim(),
162 ProductSpineIncludeInAgentContext = IncludeProductSpineInAgentContext
163 };
164 await _sessionStore.SaveMetadataAsync(updated, CancellationToken.None).ConfigureAwait(false);
165 }
166 catch
167 {
168 // best-effort
169 }
170 }
171}
172
View only · write via MCP/CIDE