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 CommandCount:usize = 0;
25
26	let mut TerminalCount:usize = 0;
27
28	let mut TerminalDataBytes:usize = 0;
29
30	// ── Tree views ────────────────────────────────────────────────────────
31	if let Ok(TreeViews) = RunTime.Environment.ApplicationState.Feature.TreeViews.ActiveTreeViews.lock() {
32		for (ViewId, Dto) in TreeViews.iter() {
33			let Payload = serde_json::json!({
34				"viewId": ViewId,
35				"options": {
36					"canSelectMany": Dto.CanSelectMany,
37					"showCollapseAll": Dto.HasHandleDrag,
38					"title": Dto.Title.clone().unwrap_or_default(),
39				},
40			});
41
42			if ApplicationHandle.emit("sky://tree-view/create", Payload).is_ok() {
43				TreeViewCount += 1;
44			}
45		}
46	}
47
48	// ── SCM providers ─────────────────────────────────────────────────────
49	// Pre-DTO-Identifier-field DTOs default `Identifier` to "" (serde
50	// default); fall back to "git" - the only SCM provider in production
51	// today is `vscode.git` and a stale state file with empty id is the
52	// realistic upgrade-path mismatch.
53	if let Ok(ScmProviders) = RunTime
54		.Environment
55		.ApplicationState
56		.Feature
57		.Markers
58		.SourceControlManagementProviders
59		.lock()
60	{
61		for (Handle, Dto) in ScmProviders.iter() {
62			let RootUriStr = Dto
63				.RootURI
64				.as_ref()
65				.and_then(|V| V.get("external").or_else(|| V.get("path")))
66				.and_then(serde_json::Value::as_str)
67				.unwrap_or("")
68				.to_string();
69
70			let ScmId = if Dto.Identifier.is_empty() {
71				"git".to_string()
72			} else {
73				Dto.Identifier.clone()
74			};
75
76			let Payload = serde_json::json!({
77				"scmId": ScmId,
78				"label": Dto.Label,
79				"rootUri": RootUriStr,
80				"extensionId": "",
81				"handle": *Handle,
82			});
83
84			if ApplicationHandle.emit("sky://scm/register", Payload).is_ok() {
85				ScmCount += 1;
86			}
87		}
88	}
89
90	// ── Extension commands ────────────────────────────────────────────────
91	// Emit ONE batched event with the whole array. Per-command emits
92	// (one per registered command, ~1000+ during extension boot) saturate
93	// Tauri's shared WKWebView IPC channel and starve keystroke delivery.
94	// SkyBridge accepts `{ commands: [...] }` or `{ id, commandId, kind }`.
95	if let Ok(Commands) = RunTime.Environment.ApplicationState.Extension.Registry.CommandRegistry.lock() {
96		let mut Batch:Vec<serde_json::Value> = Vec::new();
97
98		for (CommandId, Handler) in Commands.iter() {
99			use crate::Environment::CommandProvider::CommandHandler;
100
101			let Kind = match Handler {
102				CommandHandler::Native(_) => continue,
103
104				CommandHandler::Proxied { .. } => "extension",
105			};
106
107			Batch.push(serde_json::json!({
108				"id": CommandId,
109				"commandId": CommandId,
110				"kind": Kind,
111			}));
112		}
113
114		if !Batch.is_empty() {
115			let Count = Batch.len();
116
117			if ApplicationHandle
118				.emit("sky://command/register", serde_json::json!({ "commands": Batch }))
119				.is_ok()
120			{
121				CommandCount = Count;
122			}
123		}
124	}
125
126	// ── Terminals + buffered stdout ───────────────────────────────────────
127	// Each active terminal needs its `create` event AND any buffered stdout
128	// the PTY reader produced before SkyBridge was up. Without this, the
129	// shell's first prompt is silently dropped and the user sees an empty
130	// terminal pane until they type.
131	if let Ok(Terminals) = RunTime.Environment.ApplicationState.Feature.Terminals.ActiveTerminals.lock() {
132		for (TerminalId, Arc) in Terminals.iter() {
133			let (Name, Pid) = if let Ok(State) = Arc.lock() {
134				(State.Name.clone(), State.OSProcessIdentifier.unwrap_or(0))
135			} else {
136				(String::new(), 0)
137			};
138
139			let CreatePayload = serde_json::json!({
140				"id": *TerminalId,
141				"name": Name,
142				"pid": Pid,
143			});
144
145			if ApplicationHandle.emit("sky://terminal/create", CreatePayload).is_ok() {
146				TerminalCount += 1;
147			}
148		}
149	}
150
151	for (TerminalId, Bytes) in crate::Environment::TerminalProvider::Fn() {
152		let DataString = String::from_utf8_lossy(&Bytes).to_string();
153
154		TerminalDataBytes += Bytes.len();
155
156		let _ = ApplicationHandle.emit(
157			"sky://terminal/data",
158			serde_json::json!({ "id": TerminalId, "data": DataString }),
159		);
160	}
161
162	crate::dev_log!(
163		"sky-emit",
164		"[SkyEmit] replay-events tree-views={} scm={} commands={} terminals={} terminal-bytes={}",
165		TreeViewCount,
166		ScmCount,
167		CommandCount,
168		TerminalCount,
169		TerminalDataBytes
170	);
171
172	Ok(serde_json::json!({
173		"treeViews": TreeViewCount,
174		"scmProviders": ScmCount,
175		"commands": CommandCount,
176		"terminals": TerminalCount,
177		"terminalDataBytes": TerminalDataBytes,
178	}))
179}