Skip to main content

Mountain/IPC/WindServiceHandlers/Sky/
ReplayEvents.rs

1//! Wire method: `sky:replay-events`.
2//! Called by SkyBridge after every `sky://*` Tauri listener is installed.
3//! Mountain → Sky `app.emit()` events are NOT buffered: any emit fired before
4//! the listener was registered is silently dropped. In the bundled-electron
5//! profile, extension activation starts ~580 log lines before the Sky bundle
6//! finishes booting (~1995 lines). Without replay, all tree-view + SCM
7//! register events are lost and the Activity Bar comes up empty.
8//!
9//! Replays: tree-views, SCM providers, extension commands, active terminals
10//! (including buffered stdout from before SkyBridge's listeners were up).
11
12use std::sync::Arc;
13
14use serde_json::Value;
15use tauri::{AppHandle, Emitter};
16
17use crate::RunTime::ApplicationRunTime::ApplicationRunTime;
18
19pub async fn Fn(ApplicationHandle:AppHandle, RunTime:Arc<ApplicationRunTime>) -> Result<Value, String> {
20	let mut TreeViewCount:usize = 0;
21
22	let mut ScmCount:usize = 0;
23
24	let mut ScmGroupCount:usize = 0;
25
26	let mut ScmResourceUpdateCount:usize = 0;
27
28	let mut CommandCount:usize = 0;
29
30	let mut TerminalCount:usize = 0;
31
32	let mut TerminalDataBytes:usize = 0;
33
34	// ── Tree views ────────────────────────────────────────────────────────
35	if let Ok(TreeViews) = RunTime.Environment.ApplicationState.Feature.TreeViews.ActiveTreeViews.lock() {
36		for (ViewId, Dto) in TreeViews.iter() {
37			let Payload = serde_json::json!({
38				"viewId": ViewId,
39				"options": {
40					"canSelectMany": Dto.CanSelectMany,
41					"showCollapseAll": Dto.HasHandleDrag,
42					"title": Dto.Title.clone().unwrap_or_default(),
43				},
44			});
45
46			if ApplicationHandle.emit("sky://tree-view/create", Payload).is_ok() {
47				TreeViewCount += 1;
48			}
49		}
50	}
51
52	// ── SCM providers ─────────────────────────────────────────────────────
53	// Pre-DTO-Identifier-field DTOs default `Identifier` to "" (serde
54	// default); fall back to "git" - the only SCM provider in production
55	// today is `vscode.git` and a stale state file with empty id is the
56	// realistic upgrade-path mismatch.
57	if let Ok(ScmProviders) = RunTime
58		.Environment
59		.ApplicationState
60		.Feature
61		.Markers
62		.SourceControlManagementProviders
63		.lock()
64	{
65		for (Handle, Dto) in ScmProviders.iter() {
66			let RootUriStr = Dto
67				.RootURI
68				.as_ref()
69				.and_then(|V| V.get("external").or_else(|| V.get("path")))
70				.and_then(serde_json::Value::as_str)
71				.unwrap_or("")
72				.to_string();
73
74			let ScmId = if Dto.Identifier.is_empty() {
75				"git".to_string()
76			} else {
77				Dto.Identifier.clone()
78			};
79
80			let Payload = serde_json::json!({
81				"scmId": ScmId,
82				"label": Dto.Label,
83				"rootUri": RootUriStr,
84				"extensionId": "",
85				"handle": *Handle,
86			});
87
88			if ApplicationHandle.emit("sky://scm/register", Payload).is_ok() {
89				ScmCount += 1;
90			}
91		}
92	}
93
94	// ── SCM resource groups ───────────────────────────────────────────────
95	// Cocoon's `createResourceGroup(GroupId, Label)` mints
96	// `GroupHandle = "${ProviderHandle}/${GroupId}"` and fires
97	// `register_scm_resource_group` to Mountain. Replay must reconstruct the
98	// same handle so InstallScm's `ScmShimByHandle`/`ScmShimRegistry` lookup
99	// resolves the same shim that the live wire path would. Without this
100	// replay leg the workbench shows the provider header but zero groups,
101	// because `sky://scm/registerGroup` was emitted before SkyBridge's
102	// listener was up and Tauri events are not buffered.
103	//
104	// We resolve `scmId` from the providers map (defaulting to "git" if
105	// `Identifier` is empty - matches the provider-replay fallback above).
106	let ProviderIdentifierByHandle:std::collections::HashMap<u32, String> = if let Ok(ScmProviders) = RunTime
107		.Environment
108		.ApplicationState
109		.Feature
110		.Markers
111		.SourceControlManagementProviders
112		.lock()
113	{
114		ScmProviders
115			.iter()
116			.map(|(Handle, Dto)| {
117				let Id = if Dto.Identifier.is_empty() {
118					"git".to_string()
119				} else {
120					Dto.Identifier.clone()
121				};
122
123				(*Handle, Id)
124			})
125			.collect()
126	} else {
127		std::collections::HashMap::new()
128	};
129
130	if let Ok(ScmGroups) = RunTime
131		.Environment
132		.ApplicationState
133		.Feature
134		.Markers
135		.SourceControlManagementGroups
136		.lock()
137	{
138		for (ProviderHandle, GroupsByID) in ScmGroups.iter() {
139			let ScmId = ProviderIdentifierByHandle
140				.get(ProviderHandle)
141				.cloned()
142				.unwrap_or_else(|| "git".to_string());
143
144			for (GroupId, GroupDto) in GroupsByID.iter() {
145				let GroupHandle = format!("{}/{}", ProviderHandle, GroupId);
146
147				let Payload = serde_json::json!({
148					"scmId": ScmId,
149					"scmHandle": *ProviderHandle,
150					"groupHandle": GroupHandle,
151					"groupId": GroupId,
152					"label": GroupDto.Label,
153				});
154
155				if ApplicationHandle.emit("sky://scm/registerGroup", Payload).is_ok() {
156					ScmGroupCount += 1;
157				}
158			}
159		}
160	}
161
162	// ── SCM resource updates ──────────────────────────────────────────────
163	// After group registration, replay the most recent resource snapshot for
164	// each (provider, group) so the workbench's tree populates with the
165	// extension's current working-tree state without waiting for the next
166	// `update_scm_group` to land. Without this the panel stays empty until
167	// the user makes a file change that triggers a fresh group update.
168	if let Ok(ScmResources) = RunTime
169		.Environment
170		.ApplicationState
171		.Feature
172		.Markers
173		.SourceControlManagementResources
174		.lock()
175	{
176		for (ProviderHandle, GroupsByID) in ScmResources.iter() {
177			let ScmId = ProviderIdentifierByHandle
178				.get(ProviderHandle)
179				.cloned()
180				.unwrap_or_else(|| "git".to_string());
181
182			for (GroupId, ResourceList) in GroupsByID.iter() {
183				let GroupHandle = format!("{}/{}", ProviderHandle, GroupId);
184
185				let Payload = serde_json::json!({
186					"scmHandle": *ProviderHandle,
187					"providerId": ScmId,
188					"groupHandle": GroupHandle,
189					"groupId": GroupId,
190					"resourceStates": ResourceList,
191				});
192
193				if ApplicationHandle.emit("sky://scm/updateGroup", Payload).is_ok() {
194					ScmResourceUpdateCount += 1;
195				}
196			}
197		}
198	}
199
200	// ── Extension commands ────────────────────────────────────────────────
201	// Emit ONE batched event with the whole array. Per-command emits
202	// (one per registered command, ~1000+ during extension boot) saturate
203	// Tauri's shared WKWebView IPC channel and starve keystroke delivery.
204	// SkyBridge accepts `{ commands: [...] }` or `{ id, commandId, kind }`.
205	if let Ok(Commands) = RunTime.Environment.ApplicationState.Extension.Registry.CommandRegistry.lock() {
206		let mut Batch:Vec<serde_json::Value> = Vec::new();
207
208		for (CommandId, Handler) in Commands.iter() {
209			use crate::Environment::CommandProvider::CommandHandler;
210
211			let Kind = match Handler {
212				CommandHandler::Native(_) => continue,
213
214				CommandHandler::Proxied { .. } => "extension",
215			};
216
217			Batch.push(serde_json::json!({
218				"id": CommandId,
219				"commandId": CommandId,
220				"kind": Kind,
221			}));
222		}
223
224		if !Batch.is_empty() {
225			let Count = Batch.len();
226
227			if ApplicationHandle
228				.emit("sky://command/register", serde_json::json!({ "commands": Batch }))
229				.is_ok()
230			{
231				CommandCount = Count;
232			}
233		}
234	}
235
236	// ── Terminals + buffered stdout ───────────────────────────────────────
237	// Each active terminal needs its `create` event AND any buffered stdout
238	// the PTY reader produced before SkyBridge was up. Without this, the
239	// shell's first prompt is silently dropped and the user sees an empty
240	// terminal pane until they type.
241	if let Ok(Terminals) = RunTime.Environment.ApplicationState.Feature.Terminals.ActiveTerminals.lock() {
242		for (TerminalId, Arc) in Terminals.iter() {
243			let (Name, Pid) = if let Ok(State) = Arc.lock() {
244				(State.Name.clone(), State.OSProcessIdentifier.unwrap_or(0))
245			} else {
246				(String::new(), 0)
247			};
248
249			let CreatePayload = serde_json::json!({
250				"id": *TerminalId,
251				"name": Name,
252				"pid": Pid,
253			});
254
255			if ApplicationHandle.emit("sky://terminal/create", CreatePayload).is_ok() {
256				TerminalCount += 1;
257			}
258		}
259	}
260
261	for (TerminalId, Bytes) in crate::Environment::TerminalProvider::Fn() {
262		let DataString = String::from_utf8_lossy(&Bytes).to_string();
263
264		TerminalDataBytes += Bytes.len();
265
266		let _ = ApplicationHandle.emit(
267			"sky://terminal/data",
268			serde_json::json!({ "id": TerminalId, "data": DataString }),
269		);
270	}
271
272	crate::dev_log!(
273		"sky-emit",
274		"[SkyEmit] replay-events tree-views={} scm={} scm-groups={} scm-resource-updates={} commands={} terminals={} \
275		 terminal-bytes={}",
276		TreeViewCount,
277		ScmCount,
278		ScmGroupCount,
279		ScmResourceUpdateCount,
280		CommandCount,
281		TerminalCount,
282		TerminalDataBytes
283	);
284
285	Ok(serde_json::json!({
286		"treeViews": TreeViewCount,
287		"scmProviders": ScmCount,
288		"scmGroups": ScmGroupCount,
289		"scmResourceUpdates": ScmResourceUpdateCount,
290		"commands": CommandCount,
291		"terminals": TerminalCount,
292		"terminalDataBytes": TerminalDataBytes,
293	}))
294}