Forge
csharpdeeb25a2
1#nullable enable
2using CascadeIDE.Models.AgentChat;
3using CascadeIDE.Services.Intercom;
4using CascadeIDE.ViewModels;
5
6namespace CascadeIDE.Features.Chat;
7
8public partial class ChatPanelViewModel
9{
10 private readonly SemaphoreSlim _intercomSessionReloadGate = new(1, 1);
11
12 /// <summary>Перечитать workspace, session id и историю с диска (после загрузки решения или смены корня).</summary>
13 public Task ReloadIntercomSessionFromDiskAsync() => ReloadIntercomSessionCoreAsync();
14
15 private Task ReloadIntercomSessionCoreAsync() =>
16 RunIntercomSessionReloadAsync(InitializeSessionCoreAsync);
17
18 private async Task RunIntercomSessionReloadAsync(Func<CancellationToken, Task> reloadBody)
19 {
20 await _intercomSessionReloadGate.WaitAsync().ConfigureAwait(false);
21 try
22 {
23 await reloadBody(CancellationToken.None).ConfigureAwait(false);
24 }
25 finally
26 {
27 _intercomSessionReloadGate.Release();
28 }
29 }
30
31 private async Task InitializeSessionCoreAsync(CancellationToken ct)
32 {
33 try
34 {
35 var workspaceRoot = ResolveAttachWorkspaceRoot();
36 _sessionStore.SetWorkspaceRoot(string.IsNullOrWhiteSpace(workspaceRoot) ? null : workspaceRoot);
37 _sessionId = await _sessionStore.ResolveOrCreateCurrentSessionIdAsync(ct).ConfigureAwait(false);
38 await _sessionStore.BindCurrentSessionAsync(_sessionId, ct).ConfigureAwait(false);
39 Harness.BindSession(_sessionId);
40
41 var events = await _sessionStore.ReadEventsAsync(_sessionId, ct).ConfigureAwait(false);
42 rebuildExplicitRelatesFromEvents(events);
43 ReplaceSessionEventsCache(events);
44
45 var meta = await _sessionStore.LoadOrCreateMetadataAsync(_sessionId, ct).ConfigureAwait(false);
46 var inferredMain = ChatHistoryMessageProjector.InferMainThreadId(events);
47 if (meta.MainThreadId == Guid.Empty)
48 {
49 meta = meta with { MainThreadId = inferredMain };
50 await _sessionStore.SaveMetadataAsync(meta, ct).ConfigureAwait(false);
51 }
52 else if (events.Count > 0)
53 {
54 var rowsProbe = ChatHistoryMessageProjector.Project(events, meta.MainThreadId);
55 if (rowsProbe.Count > 0 && rowsProbe.All(row => row.ThreadId != meta.MainThreadId))
56 {
57 meta = meta with { MainThreadId = inferredMain };
58 await _sessionStore.SaveMetadataAsync(meta, ct).ConfigureAwait(false);
59 }
60 }
61
62 _mainThreadId = meta.MainThreadId;
63 _activeThreadId = meta.MainThreadId;
64 _sessionSolutionPathRelative = meta.SolutionPath;
65 ThreadBranchHint = $"Ветка {_activeThreadId:N}";
66
67 await PersistSessionSolutionPathIfChangedAsync(ct).ConfigureAwait(false);
68
69 var rows = ChatHistoryMessageProjector.Project(events, _mainThreadId);
70 var forks = ChatThreadForkProjector.Project(events);
71 await UiScheduler.Default.InvokeAsync(() =>
72 {
73 ChatMessages.Clear();
74 ApplyProductSpineFromMetadata(meta);
75 ApplyThreadTitlesFromMetadata(meta);
76 _threadForks.Clear();
77 _threadForks.AddRange(forks);
78 foreach (var row in rows)
79 ChatMessages.Add(new ChatMessageViewModel(
80 row.Role,
81 row.Content,
82 row.MessageId,
83 row.ThreadId,
84 row.ParentMessageId,
85 slashCommandPath: row.SlashCommandPath,
86 slashCommandArgs: row.SlashCommandArgs,
87 slashCommandStatus: row.SlashCommandStatus,
88 attachments: row.Attachments,
89 senderWorkspaceContext: row.SenderWorkspaceContext,
90 audience: row.Audience));
91 ClarificationStatusText = rows.Count > 0
92 ? $"Восстановлено сообщений: {rows.Count}"
93 : "История Intercom пуста (новая сессия или нет событий на диске).";
94 RefreshAttachmentAnchorsForCurrentScope();
95 RefreshChatSurfaceSnapshot();
96 }).ConfigureAwait(false);
97
98 _ = StartIntercomTransportAsync(ct);
99 await Harness.OnSessionInitializedAsync(ct).ConfigureAwait(false);
100 }
101 catch (Exception ex)
102 {
103 await UiScheduler.Default.InvokeAsync(() =>
104 ClarificationStatusText = "Не удалось загрузить историю Intercom: " + ex.Message).ConfigureAwait(false);
105 }
106 }
107
108 private async Task PersistEventAsync<T>(string kind, T payload, Guid? envelopeThreadId = null)
109 {
110 try
111 {
112 var workspaceRoot = ResolveAttachWorkspaceRoot();
113 if (!string.IsNullOrWhiteSpace(workspaceRoot))
114 _sessionStore.SetWorkspaceRoot(workspaceRoot);
115
116 if (_sessionId == Guid.Empty)
117 _sessionId = await _sessionStore.ResolveOrCreateCurrentSessionIdAsync(CancellationToken.None).ConfigureAwait(false);
118
119 var tid = envelopeThreadId ?? _activeThreadId;
120 if (tid == Guid.Empty)
121 tid = _mainThreadId;
122 var ev = new ChatHistoryEvent(
123 Guid.NewGuid(),
124 _sessionId,
125 DateTimeOffset.UtcNow,
126 kind,
127 ChatHistoryJson.Serialize(payload),
128 ThreadId: tid == Guid.Empty ? null : tid.ToString("N"));
129 await _sessionStore.AppendEventAsync(ev, CancellationToken.None).ConfigureAwait(false);
130 AppendSessionEventCache(ev);
131 NotifyIntercomTransportAfterPersist(ev);
132 await _sessionStore.BindCurrentSessionAsync(_sessionId, CancellationToken.None).ConfigureAwait(false);
133 var meta = await _sessionStore.LoadOrCreateMetadataAsync(_sessionId, CancellationToken.None).ConfigureAwait(false);
134 if (meta.UpdatedAtUtc < ev.AtUtc)
135 await _sessionStore.SaveMetadataAsync(meta with { UpdatedAtUtc = ev.AtUtc }, CancellationToken.None).ConfigureAwait(false);
136 }
137 catch
138 {
139 // intentionally ignored (best-effort persistence).
140 }
141 }
142
143 private async Task PersistSessionSolutionPathIfChangedAsync(CancellationToken ct)
144 {
145 try
146 {
147 var workspace = ResolveAttachWorkspaceRoot();
148 if (string.IsNullOrWhiteSpace(workspace))
149 return;
150
151 var rel = IntercomAttachScope.ToSessionRelativeSolutionPath(_getSolutionPath?.Invoke(), workspace)
152 ?? IntercomAttachScope.TryDiscoverSolutionPathRelative(workspace)
153 ?? _sessionSolutionPathRelative;
154
155 if (string.IsNullOrWhiteSpace(rel))
156 return;
157
158 _sessionSolutionPathRelative = rel;
159 if (_sessionId == Guid.Empty)
160 return;
161
162 var meta = await _sessionStore.LoadOrCreateMetadataAsync(_sessionId, ct).ConfigureAwait(false);
163 if (string.Equals(meta.SolutionPath, rel, StringComparison.OrdinalIgnoreCase))
164 return;
165
166 var schema = meta.SchemaVersion < 2 ? 2 : meta.SchemaVersion;
167 await _sessionStore.SaveMetadataAsync(meta with { SolutionPath = rel, SchemaVersion = schema }, ct)
168 .ConfigureAwait(false);
169 }
170 catch
171 {
172 // best-effort scope metadata
173 }
174 }
175}
176
View only · write via MCP/CIDE