@@ -53,7 +53,15 @@ class MainActivity : FragmentActivity() {
5353 private var openFileHandled = false
5454 // Set when a deep link was present but failed validation (malformed id/path). Surfaced
5555 // as a toast from onResume so the user knows the link was rejected, not silently ignored.
56+ // On a warm start (onNewIntent while already resumed) the toast is shown immediately from
57+ // handleIntent, since onResume is NOT re-called on an already-resumed activity — without
58+ // that the toast would stay pending until the next pause/resume cycle (e.g. user
59+ // backgrounds and returns), making the rejected link look silently ignored.
5660 private var showInvalidLinkToast = false
61+ // True between onResume and onPause. Used by handleIntent to decide whether to fire the
62+ // invalid-link toast immediately (warm start) or defer it to onResume (cold start, where
63+ // a toast from onCreate could flash before the first frame is drawn).
64+ private var isResumed = false
5765 // Cold-start prompts (crash report, notification rationale) rendered as Compose M3
5866 // dialogs. Hoisted as Activity-level mutableState so the non-composable trigger logic
5967 // (onCreate/onResume) can flip them and the Compose tree (inside OpencodeTheme) reads
@@ -244,19 +252,34 @@ class MainActivity : FragmentActivity() {
244252
245253 override fun onResume() {
246254 super.onResume()
255+ isResumed = true
247256 // Re-check on resume: an Activity recreated by a config change not in configChanges
248257 // (e.g. locale) has savedInstanceState != null, so onCreate skipped the request. This
249258 // ensures the user is prompted once per Activity instance when still ungranted rather
250259 // than silently losing all notifications.
251260 maybeRequestNotificationPermission()
252261 // Surface a rejected deep link once, so a malformed opencode:// link doesn't appear
253− // to be silently ignored. Cleared after firing so a rotation doesn't re-toast.
262+ // to be silently ignored. Cleared after firing so a rotation doesn't re-toast. This
263+ // covers the cold-start path (handleIntent ran from onCreate, before the activity was
264+ // resumed, so it deferred the toast here). Warm-start rejections fire immediately from
265+ // handleIntent and never set this flag.
254266 if (showInvalidLinkToast) {
255267 showInvalidLinkToast = false
256− android.widget.Toast.makeText(this, R.string.deep_link_invalid, android.widget.Toast.LENGTH_SHORT).show()
268+ showInvalidLinkToastNow()
257269 }
258270 }
259271
272+ override fun onPause() {
273+ super.onPause()
274+ isResumed = false
275+ }
276+
277+ /** Fire the invalid-deep-link toast. Extracted so the cold-start (onResume) and warm-start
278+ * (handleIntent) paths share a single implementation. */
279+ private fun showInvalidLinkToastNow() {
280+ android.widget.Toast.makeText(this, R.string.deep_link_invalid, android.widget.Toast.LENGTH_SHORT).show()
281+ }
282+
260283 /** Capture text shared from another app so it can be prefilled into a session draft,
261284 * and session ids from notification taps / deep links so we can open them. */
262285 private fun handleIntent(intent: Intent?) {
@@ -333,7 +356,11 @@ class MainActivity : FragmentActivity() {
333356 // The link carried a session host but no valid id (or a blank/traversal
334357 // attempt). Surface a toast so the user knows the link was rejected,
335358 // rather than silently no-op'ing (which reads as "the app did nothing").
336− if (!seg.isNullOrBlank()) showInvalidLinkToast = true
359+ // On a warm start (already resumed) fire immediately; on a cold start
360+ // defer to onResume so the toast doesn't race the first frame.
361+ if (!seg.isNullOrBlank()) {
362+ if (isResumed) showInvalidLinkToastNow() else showInvalidLinkToast = true
363+ }
337364 // Mark the link handled even on rejection, so a process-death-and-restore
338365 // (which re-delivers the same intent) doesn't re-fire the toast on every
339366 // restore — the rejection is final, not a transient state to retry.
@@ -348,7 +375,10 @@ class MainActivity : FragmentActivity() {
348375 if (!openFileHandled) {
349376 rawPath?.takeIf { isValidFilePath(it) }
350377 ?.let { container.requestOpenFile(it); openFileHandled = true } ?: run {
351− if (!rawPath.isNullOrBlank()) showInvalidLinkToast = true
378+ if (!rawPath.isNullOrBlank()) {
379+ // Same warm-vs-cold logic as the session branch above.
380+ if (isResumed) showInvalidLinkToastNow() else showInvalidLinkToast = true
381+ }
352382 // Mark handled on rejection too, mirroring the session branch —
353383 // otherwise a process-death restore re-fires the invalid-link toast.
354384 openFileHandled = true