Skip to content

Commit 802efb9

Browse files
Merge remote-tracking branch 'origin/main' into fix/issue-25636
2 parents 802f443 + 96fc7b9 commit 802efb9

86 files changed

Lines changed: 1518 additions & 1290 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎apps/desktop/package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@t3tools/desktop",
3-
"version": "0.1.2",
3+
"version": "0.1.3",
44
"private": true,
55
"type": "module",
66
"main": "dist-electron/boot.cjs",

‎apps/desktop/src/window/DesktopWindow.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ function getIconOption(
131131
}
132132

133133
function getInitialWindowBackgroundColor(shouldUseDarkColors: boolean): string {
134-
return shouldUseDarkColors ? "#0a0a0a" : "#ffffff";
134+
return shouldUseDarkColors ? "#000000" : "#ffffff";
135135
}
136136

137137
type DisplayBounds = Pick<Electron.Rectangle, "x" | "y" | "width" | "height">;

‎apps/mobile/src/App.tsx‎

Lines changed: 65 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,12 @@ import { GestureHandlerRootView } from "react-native-gesture-handler";
99
import { KeyboardProvider } from "react-native-keyboard-controller";
1010
import { SafeAreaProvider } from "react-native-safe-area-context";
1111
import {
12+
CommonActions,
13+
createPathConfigForStaticNavigation,
1214
createStaticNavigation,
1315
getActionFromState,
1416
getStateFromPath,
17+
type NavigationAction,
1518
type NavigationState,
1619
} from "@react-navigation/native";
1720

@@ -21,12 +24,24 @@ import { ThreadArrangementHost } from "./features/threads/ThreadArrangementSheet
2124
import * as Dialog from "./components/ui/Dialog";
2225
import { CloudAuthProvider } from "./features/cloud/CloudAuthProvider";
2326
import { prepareNativeShowcaseCapture } from "./features/showcase/nativeShowcaseScene";
24-
import { IncomingShareProvider } from "./features/sharing/IncomingShareProvider";
27+
import {
28+
presentPendingIncomingShare,
29+
startIncomingShareInbox,
30+
} from "./features/sharing/IncomingShareProvider";
31+
import {
32+
installAgentNotificationNavigation,
33+
setAgentNotificationPathname,
34+
} from "./features/agent-awareness/notificationNavigation";
35+
import {
36+
installAppShortcuts,
37+
recordAppShortcutNavigationState,
38+
} from "./features/shortcuts/appShortcutsRuntime";
2539
import {
2640
AppearancePreferencesProvider,
41+
installAppearanceRuntime,
2742
useAppearancePreferences,
2843
} from "./features/settings/appearance/AppearancePreferencesProvider";
29-
import { RootStack } from "./Stack";
44+
import { RootStack, workspaceLocationFromState } from "./Stack";
3045
import { appAtomRegistry } from "./state/atom-registry";
3146
import { subscribeComposerDraftModelReconciliation } from "./state/use-composer-drafts";
3247
import { environmentThreadShells } from "./state/threads";
@@ -53,7 +68,12 @@ import {
5368
} from "./features/workspace/workspace-selection-store";
5469
import { workspaceDetailLinkAction } from "./features/workspace/workspace-detail-selection";
5570
import { NativeLayoutMetricsProvider } from "./native/native-layout-metrics";
56-
import { attachRootNavigation, rootNavigationRef } from "./lib/rootNavigation";
71+
import {
72+
attachRootNavigation,
73+
flushRootNavigationQueue,
74+
linkToRootPath,
75+
rootNavigationRef,
76+
} from "./lib/rootNavigation";
5777
import { pruneNewTaskBranchCheckouts } from "./features/threads/NewTaskDraftRouteScreen";
5878

5979
import "../global.css";
@@ -65,6 +85,20 @@ if (process.env.EXPO_PUBLIC_SHOWCASE === "1") {
6585
void SplashScreen.preventAutoHideAsync().catch(() => {});
6686
void hydrateWorkspaceSelections();
6787

88+
const linkingConfig = {
89+
initialRouteName: "Home",
90+
screens: createPathConfigForStaticNavigation(RootStack, { initialRouteName: "Home" }) ?? {},
91+
} as const;
92+
93+
function getAppStateFromPath(path: string, options: Parameters<typeof getStateFromPath>[1]) {
94+
return takeScheduledTaskNewLink(
95+
restoreSettingsLinkState(
96+
restoreDraftLinkState(getStateFromPath(path, options), path),
97+
settingsLinkPage(),
98+
),
99+
);
100+
}
101+
68102
const appLinking = {
69103
prefixes: [Linking.createURL("/"), "t3code://", "t3code-dev://", "t3code-preview://"],
70104
config: { initialRouteName: "Home" },
@@ -77,24 +111,33 @@ const appLinking = {
77111
// Our router handles the selection; React Navigation's type only lists its own actions.
78112
(workspaceDetailLinkAction(state) as ReturnType<typeof getActionFromState>) ??
79113
getActionFromState(state, options),
80-
getStateFromPath: (path: string, options: Parameters<typeof getStateFromPath>[1]) =>
81-
takeScheduledTaskNewLink(
82-
restoreSettingsLinkState(
83-
restoreDraftLinkState(getStateFromPath(path, options), path),
84-
settingsLinkPage(),
85-
),
86-
),
114+
getStateFromPath: getAppStateFromPath,
87115
};
88116

117+
function appLinkAction(href: string): NavigationAction {
118+
const state = getAppStateFromPath(href, linkingConfig);
119+
if (!state) throw new Error(`Failed to parse the href to a navigation state (${href}).`);
120+
return appLinking.getActionFromState(state, linkingConfig) ?? CommonActions.reset(state);
121+
}
122+
123+
function linkToAppPath(href: string): void {
124+
linkToRootPath(href, appLinkAction);
125+
}
126+
89127
const Navigation = createStaticNavigation(RootStack);
90128

91129
function handleNavigationStateChange(state: NavigationState | undefined): void {
92130
recordWorkspaceSelections(state);
93-
if (state) pruneNewTaskBranchCheckouts(state);
131+
presentPendingIncomingShare();
132+
if (!state) return;
133+
setAgentNotificationPathname(workspaceLocationFromState(state).pathname);
134+
recordAppShortcutNavigationState(state);
135+
pruneNewTaskBranchCheckouts(state);
94136
}
95137

96138
function handleNavigationReady(): void {
97139
handleNavigationStateChange(rootNavigationRef.getRootState());
140+
flushRootNavigationQueue();
98141
}
99142

100143
declare const module: { readonly hot?: FoundationHotModule } | undefined;
@@ -107,10 +150,14 @@ const appServiceDisposers = [
107150
threadRefsAtom: environmentThreadShells.threadRefsAtom,
108151
shellStateAtom: environmentShell.stateAtom,
109152
}),
153+
installAppearanceRuntime(),
110154
installSubscriptionUsageRefresh(),
111155
installPermissionUpdateNotice(),
112156
installThreadOutboxDelivery(),
113157
installComposerAttachmentUploads(),
158+
startIncomingShareInbox(),
159+
installAgentNotificationNavigation(linkToAppPath),
160+
installAppShortcuts(linkToAppPath),
114161
];
115162
disposeOnFoundationReplace(typeof module === "undefined" ? undefined : module.hot, () => {
116163
for (const dispose of appServiceDisposers) dispose();
@@ -153,15 +200,13 @@ function AppContent() {
153200
{/* native-stack needs the theme's dark flag to select dark native headers. */}
154201
<GlobalVoiceInputControl>
155202
<NativeLayoutMetricsProvider>
156-
<IncomingShareProvider>
157-
<Navigation
158-
ref={attachRootNavigation}
159-
linking={appLinking}
160-
onReady={handleNavigationReady}
161-
theme={navigationTheme}
162-
onStateChange={handleNavigationStateChange}
163-
/>
164-
</IncomingShareProvider>
203+
<Navigation
204+
ref={attachRootNavigation}
205+
linking={appLinking}
206+
onReady={handleNavigationReady}
207+
theme={navigationTheme}
208+
onStateChange={handleNavigationStateChange}
209+
/>
165210
<Dialog.Host />
166211
<ThreadArrangementHost />
167212
<ProjectChipIconRasterizer />

‎apps/mobile/src/Stack.tsx‎

Lines changed: 13 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import {
1515
createNativeStackScreen,
1616
type NativeStackNavigationOptions,
1717
} from "@react-navigation/native-stack";
18-
import { useEffect, useRef, type ReactNode } from "react";
18+
import type { ReactNode } from "react";
1919
import { Platform, Pressable, ScrollView, StyleSheet, useWindowDimensions } from "react-native";
2020
import { useResolveClassNames } from "uniwind";
2121

@@ -24,6 +24,11 @@ import { androidWorkspaceDestinationAnimation } from "./lib/android-workspace-na
2424
import { getCompactBrandHeaderOptions } from "./components/CompactBrandTitle";
2525
import { WorkspaceConnectionStatusProvider } from "./features/home/WorkspaceConnectionTitle";
2626
import { WorkspaceRootScreen } from "./features/workspace/WorkspaceRootScreen";
27+
import {
28+
recordHomeFocus,
29+
recordHomeTransitionEnd,
30+
recordHomeTransitionStart,
31+
} from "./features/home/home-route-visibility";
2732
import { AndroidWorkspaceTabsProvider } from "./features/workspace/AndroidWorkspaceTabsProvider";
2833
import {
2934
PullRequestRouteScreen,
@@ -41,9 +46,7 @@ import {
4146
type RenderFailureProps,
4247
} from "./components/RenderErrorBoundary";
4348
import { ArchivedThreadsRouteScreen } from "./features/archive/ArchivedThreadsRouteScreen";
44-
import { useAgentNotificationNavigation } from "./features/agent-awareness/notificationNavigation";
4549
import { ConnectOnboardingRouteScreen } from "./features/cloud/ConnectOnboardingRouteScreen";
46-
import { useConnectOnboardingNavigation } from "./features/cloud/connectOnboardingNavigation";
4750
import { AttachmentFileScreen } from "./features/files/AttachmentFileScreen";
4851
import { ThreadFilesTreeScreen, ThreadFileScreen } from "./features/files/ThreadFilesRouteScreen";
4952
import { AdaptiveWorkspaceLayout } from "./features/layout/AdaptiveWorkspaceLayout";
@@ -138,21 +141,12 @@ import {
138141
SettingsLegalDocumentCloseHeaderButton,
139142
SettingsLegalDocumentExternalHeaderButton,
140143
} from "./features/settings/components/SettingsLegalDocumentRouteScreen";
141-
import { useAppShortcuts } from "./features/shortcuts/useAppShortcuts";
142-
import { useIncomingShare } from "./features/sharing/IncomingShareProvider";
143-
import {
144-
EMPTY_INCOMING_SHARE_PRESENTATION_STATE,
145-
transitionIncomingSharePresentation,
146-
} from "./features/sharing/incoming-share-presentation";
147144
import { NATIVE_LIQUID_GLASS_SUPPORTED } from "./native/native-glass";
148145
import { deriveLayout } from "./lib/layout";
149146
import { useNativeLayoutMetrics } from "./native/native-layout-metrics";
150147
import { nativeHeaderScrollEdgeEffects } from "./native/StackHeader";
151148
import { HORIZONTAL_FORM_SHEET_OPTIONS, formSheetDetents } from "./native/sheet-surface";
152-
import {
153-
selectWorkspaceDetail,
154-
workspaceDetailRouter,
155-
} from "./features/workspace/workspace-detail-selection";
149+
import { workspaceDetailRouter } from "./features/workspace/workspace-detail-selection";
156150

157151
const HEADER_SCROLL_EDGE_EFFECTS = nativeHeaderScrollEdgeEffects(Platform.OS, Platform.Version);
158152

@@ -481,7 +475,7 @@ const SETTINGS_SCREENS = {
481475
// iOS 26 header morphing requires one UINavigationBar; nested stacks make the whole screen slide.
482476
const THREAD_LINKING_PREFIX = "threads/:environmentId/:threadId";
483477

484-
function workspaceLocationFromState(state: NavigationState) {
478+
export function workspaceLocationFromState(state: NavigationState) {
485479
const routes = state.routes.filter((route) => !isWorkspaceOverlayRoute(route.name));
486480
const effectiveState =
487481
routes.length > 0 && routes.length !== state.routes.length
@@ -498,9 +492,6 @@ function RootStackLayout(props: {
498492
readonly children: React.ReactNode;
499493
readonly state: NavigationState;
500494
}) {
501-
const navigation = useNavigation();
502-
const { pendingShare } = useIncomingShare();
503-
const sharePresentationRef = useRef(EMPTY_INCOMING_SHARE_PRESENTATION_STATE);
504495
const path = getPathFromState(props.state, navigationPathConfig);
505496
const pathname = path.startsWith("/") ? path : `/${path}`;
506497
const workspaceLocation = workspaceLocationFromState(props.state);
@@ -516,25 +507,6 @@ function RootStackLayout(props: {
516507
const routeDraftId = Array.isArray(draftParams?.draftId)
517508
? draftParams.draftId[0]
518509
: draftParams?.draftId;
519-
useAgentNotificationNavigation(workspaceLocation.pathname);
520-
useConnectOnboardingNavigation();
521-
useAppShortcuts(props.state);
522-
useEffect(() => {
523-
const transition = transitionIncomingSharePresentation(sharePresentationRef.current, {
524-
isShareSheetPresented: isDraftThreadRoute(props.state.routes[props.state.index]),
525-
pendingShareId: pendingShare?.id ?? null,
526-
});
527-
sharePresentationRef.current = transition.state;
528-
if (!transition.shareIdToPresent) {
529-
return;
530-
}
531-
navigation.dispatch(
532-
selectWorkspaceDetail({
533-
name: "Thread",
534-
params: { draft: { incomingShareId: transition.shareIdToPresent } },
535-
}),
536-
);
537-
}, [navigation, pendingShare, props.state]);
538510

539511
return (
540512
<NewTaskFlowProvider active={isDraftThreadRoute(draftRoute)} routeDraftId={routeDraftId}>
@@ -619,6 +591,11 @@ const RootStackConfig = createWorkspaceStackNavigator({
619591
Home: createNativeStackScreen({
620592
screen: WorkspaceRootScreen,
621593
linking: "",
594+
listeners: {
595+
transitionStart: ({ data }) => recordHomeTransitionStart(data.closing),
596+
transitionEnd: ({ data }) => recordHomeTransitionEnd(data.closing),
597+
focus: recordHomeFocus,
598+
},
622599
options: {
623600
...GLASS_HEADER_OPTIONS,
624601
...FIXED_DUO_HEADER,
Lines changed: 30 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,42 @@
1-
import { useEffect, useLayoutEffect, useRef } from "react";
21
import * as Notifications from "expo-notifications";
3-
import { useLinkTo } from "@react-navigation/native";
42

53
import { setAndroidThreadOnScreen } from "./androidNotifications";
64
import { foregroundNotificationBehavior } from "./foregroundNotificationBehavior";
75
import { routeAgentNotificationResponseOnce, threadDeepLinkOnScreen } from "./notificationPayload";
86
import { consumeLastAgentNotificationResponse } from "./notificationResponseConsumer";
97

10-
export function useAgentNotificationNavigation(pathname: string): void {
11-
const linkTo = useLinkTo();
12-
const handledResponseIds = useRef(new Set<string>());
13-
const deepLinkOnScreen = useRef<string | null>(null);
14-
useLayoutEffect(() => {
15-
const thread = threadDeepLinkOnScreen(pathname);
16-
deepLinkOnScreen.current = thread;
17-
setAndroidThreadOnScreen(thread);
18-
}, [pathname]);
8+
let deepLinkOnScreen: string | null = null;
199

20-
useEffect(() => {
21-
Notifications.setNotificationHandler({
22-
handleNotification: (notification) =>
23-
Promise.resolve(foregroundNotificationBehavior(notification, deepLinkOnScreen.current)),
24-
});
25-
return () => {
26-
Notifications.setNotificationHandler(null);
27-
};
28-
}, []);
10+
/** Called with the workspace pathname on every root navigation state change. */
11+
export function setAgentNotificationPathname(pathname: string): void {
12+
deepLinkOnScreen = threadDeepLinkOnScreen(pathname);
13+
setAndroidThreadOnScreen(deepLinkOnScreen);
14+
}
2915

30-
useEffect(() => {
31-
const handleResponse = (response: Notifications.NotificationResponse): void => {
32-
routeAgentNotificationResponseOnce({
33-
handledResponseIds: handledResponseIds.current,
34-
response,
35-
navigate: linkTo,
36-
});
37-
};
16+
/**
17+
* App-lifetime notification routing: foreground presentation suppresses the
18+
* thread already on screen, and taps (including the cold-start tap) open the
19+
* notification's deep link through `navigate`.
20+
*/
21+
export function installAgentNotificationNavigation(navigate: (href: string) => void): () => void {
22+
const handledResponseIds = new Set<string>();
23+
Notifications.setNotificationHandler({
24+
handleNotification: (notification) =>
25+
Promise.resolve(foregroundNotificationBehavior(notification, deepLinkOnScreen)),
26+
});
3827

39-
const subscription = Notifications.addNotificationResponseReceivedListener(handleResponse);
40-
void consumeLastAgentNotificationResponse({
41-
getLastResponse: () => Notifications.getLastNotificationResponseAsync(),
42-
clearLastResponse: () => Notifications.clearLastNotificationResponseAsync(),
43-
handleResponse,
44-
});
28+
const handleResponse = (response: Notifications.NotificationResponse): void => {
29+
routeAgentNotificationResponseOnce({ handledResponseIds, response, navigate });
30+
};
31+
const subscription = Notifications.addNotificationResponseReceivedListener(handleResponse);
32+
void consumeLastAgentNotificationResponse({
33+
getLastResponse: () => Notifications.getLastNotificationResponseAsync(),
34+
clearLastResponse: () => Notifications.clearLastNotificationResponseAsync(),
35+
handleResponse,
36+
});
4537

46-
return () => {
47-
subscription.remove();
48-
};
49-
}, [linkTo]);
38+
return () => {
39+
subscription.remove();
40+
Notifications.setNotificationHandler(null);
41+
};
5042
}

0 commit comments

Comments
 (0)