Skip to content

Commit 58d4648

Browse files
sheehanmunimclaude
andcommitted
Merge upstream/main (5 commits, 2026-10-27) into mtcode
Follows upstream's "new design is the only design" (pingdotgg#25778): the fork drops redesignEnabled with it and keeps its own settings rows. The agent context budget takes the smaller MCP tool list from pingdotgg#25790. Also declares zod as a server dependency. @cursor/sdk 1.0.38 made zod a peer dependency, and the desktop stage's production install does not add peers, so the packaged server failed to start with "Cannot find module 'zod/v4'" (the Linux smoke test caught it and stopped the 0.1.40 release). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 parents 0674b35 + 96fc7b9 commit 58d4648

74 files changed

Lines changed: 1109 additions & 804 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/src/window/DesktopWindow.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ function getIconOption(
137137
}
138138

139139
function getInitialWindowBackgroundColor(shouldUseDarkColors: boolean): string {
140-
return shouldUseDarkColors ? "#0a0a0a" : "#ffffff";
140+
return shouldUseDarkColors ? "#000000" : "#ffffff";
141141
}
142142

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

‎apps/mobile/src/App.tsx‎

Lines changed: 50 additions & 10 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

@@ -28,12 +31,20 @@ import {
2831
presentPendingIncomingShare,
2932
startIncomingShareInbox,
3033
} from "./features/sharing/IncomingShareProvider";
34+
import {
35+
installAgentNotificationNavigation,
36+
setAgentNotificationPathname,
37+
} from "./features/agent-awareness/notificationNavigation";
38+
import {
39+
installAppShortcuts,
40+
recordAppShortcutNavigationState,
41+
} from "./features/shortcuts/appShortcutsRuntime";
3142
import {
3243
AppearancePreferencesProvider,
3344
installAppearanceRuntime,
3445
useAppearancePreferences,
3546
} from "./features/settings/appearance/AppearancePreferencesProvider";
36-
import { RootStack } from "./Stack";
47+
import { RootStack, workspaceLocationFromState } from "./Stack";
3748
import { appAtomRegistry } from "./state/atom-registry";
3849
import { subscribeComposerDraftModelReconciliation } from "./state/use-composer-drafts";
3950
import { environmentThreadShells } from "./state/threads";
@@ -61,7 +72,12 @@ import {
6172
} from "./features/workspace/workspace-selection-store";
6273
import { workspaceDetailLinkAction } from "./features/workspace/workspace-detail-selection";
6374
import { NativeLayoutMetricsProvider } from "./native/native-layout-metrics";
64-
import { attachRootNavigation, rootNavigationRef } from "./lib/rootNavigation";
75+
import {
76+
attachRootNavigation,
77+
flushRootNavigationQueue,
78+
linkToRootPath,
79+
rootNavigationRef,
80+
} from "./lib/rootNavigation";
6581
import { pruneNewTaskBranchCheckouts } from "./features/threads/NewTaskDraftRouteScreen";
6682

6783
import "../global.css";
@@ -73,6 +89,20 @@ if (process.env.EXPO_PUBLIC_SHOWCASE === "1") {
7389
void SplashScreen.preventAutoHideAsync().catch(() => {});
7490
void hydrateWorkspaceSelections();
7591

92+
const linkingConfig = {
93+
initialRouteName: "Home",
94+
screens: createPathConfigForStaticNavigation(RootStack, { initialRouteName: "Home" }) ?? {},
95+
} as const;
96+
97+
function getAppStateFromPath(path: string, options: Parameters<typeof getStateFromPath>[1]) {
98+
return takeScheduledTaskNewLink(
99+
restoreSettingsLinkState(
100+
restoreDraftLinkState(getStateFromPath(path, options), path),
101+
settingsLinkPage(),
102+
),
103+
);
104+
}
105+
76106
const appLinking = {
77107
prefixes: [
78108
Linking.createURL("/"),
@@ -91,25 +121,33 @@ const appLinking = {
91121
// Our router handles the selection; React Navigation's type only lists its own actions.
92122
(workspaceDetailLinkAction(state) as ReturnType<typeof getActionFromState>) ??
93123
getActionFromState(state, options),
94-
getStateFromPath: (path: string, options: Parameters<typeof getStateFromPath>[1]) =>
95-
takeScheduledTaskNewLink(
96-
restoreSettingsLinkState(
97-
restoreDraftLinkState(getStateFromPath(path, options), path),
98-
settingsLinkPage(),
99-
),
100-
),
124+
getStateFromPath: getAppStateFromPath,
101125
};
102126

127+
function appLinkAction(href: string): NavigationAction {
128+
const state = getAppStateFromPath(href, linkingConfig);
129+
if (!state) throw new Error(`Failed to parse the href to a navigation state (${href}).`);
130+
return appLinking.getActionFromState(state, linkingConfig) ?? CommonActions.reset(state);
131+
}
132+
133+
function linkToAppPath(href: string): void {
134+
linkToRootPath(href, appLinkAction);
135+
}
136+
103137
const Navigation = createStaticNavigation(RootStack);
104138

105139
function handleNavigationStateChange(state: NavigationState | undefined): void {
106140
recordWorkspaceSelections(state);
107-
if (state) pruneNewTaskBranchCheckouts(state);
108141
presentPendingIncomingShare();
142+
if (!state) return;
143+
setAgentNotificationPathname(workspaceLocationFromState(state).pathname);
144+
recordAppShortcutNavigationState(state);
145+
pruneNewTaskBranchCheckouts(state);
109146
}
110147

111148
function handleNavigationReady(): void {
112149
handleNavigationStateChange(rootNavigationRef.getRootState());
150+
flushRootNavigationQueue();
113151
}
114152

115153
declare const module: { readonly hot?: FoundationHotModule } | undefined;
@@ -128,6 +166,8 @@ const appServiceDisposers = [
128166
installThreadOutboxDelivery(),
129167
installComposerAttachmentUploads(),
130168
startIncomingShareInbox(),
169+
installAgentNotificationNavigation(linkToAppPath),
170+
installAppShortcuts(linkToAppPath),
131171
];
132172
disposeOnFoundationReplace(typeof module === "undefined" ? undefined : module.hot, () => {
133173
for (const dispose of appServiceDisposers) dispose();

‎apps/mobile/src/Stack.tsx‎

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ import { getCompactBrandHeaderOptions } from "./components/CompactBrandTitle";
2525
import { getConnectName } from "./lib/branding";
2626
import { WorkspaceConnectionStatusProvider } from "./features/home/WorkspaceConnectionTitle";
2727
import { WorkspaceRootScreen } from "./features/workspace/WorkspaceRootScreen";
28+
import {
29+
recordHomeFocus,
30+
recordHomeTransitionEnd,
31+
recordHomeTransitionStart,
32+
} from "./features/home/home-route-visibility";
2833
import { AndroidWorkspaceTabsProvider } from "./features/workspace/AndroidWorkspaceTabsProvider";
2934
import {
3035
PullRequestRouteScreen,
@@ -42,9 +47,7 @@ import {
4247
type RenderFailureProps,
4348
} from "./components/RenderErrorBoundary";
4449
import { ArchivedThreadsRouteScreen } from "./features/archive/ArchivedThreadsRouteScreen";
45-
import { useAgentNotificationNavigation } from "./features/agent-awareness/notificationNavigation";
4650
import { ConnectOnboardingRouteScreen } from "./features/cloud/ConnectOnboardingRouteScreen";
47-
import { useConnectOnboardingNavigation } from "./features/cloud/connectOnboardingNavigation";
4851
import { AttachmentFileScreen } from "./features/files/AttachmentFileScreen";
4952
import { ThreadFilesTreeScreen, ThreadFileScreen } from "./features/files/ThreadFilesRouteScreen";
5053
import { AdaptiveWorkspaceLayout } from "./features/layout/AdaptiveWorkspaceLayout";
@@ -141,7 +144,6 @@ import {
141144
SettingsLegalDocumentCloseHeaderButton,
142145
SettingsLegalDocumentExternalHeaderButton,
143146
} from "./features/settings/components/SettingsLegalDocumentRouteScreen";
144-
import { useAppShortcuts } from "./features/shortcuts/useAppShortcuts";
145147
import { NATIVE_LIQUID_GLASS_SUPPORTED } from "./native/native-glass";
146148
import { deriveLayout } from "./lib/layout";
147149
import { useNativeLayoutMetrics } from "./native/native-layout-metrics";
@@ -484,7 +486,7 @@ const SETTINGS_SCREENS = {
484486
// iOS 26 header morphing requires one UINavigationBar; nested stacks make the whole screen slide.
485487
const THREAD_LINKING_PREFIX = "threads/:environmentId/:threadId";
486488

487-
function workspaceLocationFromState(state: NavigationState) {
489+
export function workspaceLocationFromState(state: NavigationState) {
488490
const routes = state.routes.filter((route) => !isWorkspaceOverlayRoute(route.name));
489491
const effectiveState =
490492
routes.length > 0 && routes.length !== state.routes.length
@@ -516,9 +518,6 @@ function RootStackLayout(props: {
516518
const routeDraftId = Array.isArray(draftParams?.draftId)
517519
? draftParams.draftId[0]
518520
: draftParams?.draftId;
519-
useAgentNotificationNavigation(workspaceLocation.pathname);
520-
useConnectOnboardingNavigation();
521-
useAppShortcuts(props.state);
522521

523522
return (
524523
<NewTaskFlowProvider active={isDraftThreadRoute(draftRoute)} routeDraftId={routeDraftId}>
@@ -603,6 +602,11 @@ const RootStackConfig = createWorkspaceStackNavigator({
603602
Home: createNativeStackScreen({
604603
screen: WorkspaceRootScreen,
605604
linking: "",
605+
listeners: {
606+
transitionStart: ({ data }) => recordHomeTransitionStart(data.closing),
607+
transitionEnd: ({ data }) => recordHomeTransitionEnd(data.closing),
608+
focus: recordHomeFocus,
609+
},
606610
options: {
607611
...GLASS_HEADER_OPTIONS,
608612
...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
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { afterEach, describe, expect, it, vi } from "vite-plus/test";
2+
3+
const navigation = vi.hoisted(() => ({
4+
navigate: vi.fn(),
5+
ready: true,
6+
queue: [] as Array<() => void>,
7+
}));
8+
const optOut = vi.hoisted(() => ({ optedOut: new Set<string>() }));
9+
10+
vi.mock("../../lib/rootNavigation", () => ({
11+
rootNavigationRef: navigation,
12+
whenRootNavigationReady: (navigate: () => void) =>
13+
navigation.ready ? navigate() : navigation.queue.push(navigate),
14+
}));
15+
vi.mock("./connectOnboardingOptOut", () => ({
16+
isConnectOnboardingOptedOut: (accountId: string) =>
17+
Promise.resolve(optOut.optedOut.has(accountId)),
18+
}));
19+
20+
import { clearConnectOnboardingRequest, requestConnectOnboarding } from "./connectOnboarding";
21+
22+
afterEach(() => {
23+
clearConnectOnboardingRequest();
24+
optOut.optedOut.clear();
25+
navigation.navigate.mockReset();
26+
navigation.ready = true;
27+
navigation.queue.length = 0;
28+
vi.useRealTimers();
29+
});
30+
31+
describe("requestConnectOnboarding", () => {
32+
it("presents the sheet after the settle delay", async () => {
33+
vi.useFakeTimers();
34+
requestConnectOnboarding("account-1");
35+
await vi.advanceTimersByTimeAsync(599);
36+
expect(navigation.navigate).not.toHaveBeenCalled();
37+
await vi.advanceTimersByTimeAsync(1);
38+
expect(navigation.navigate).toHaveBeenCalledExactlyOnceWith("ConnectOnboarding");
39+
});
40+
41+
it("drops a request cleared by sign-out before it fires", async () => {
42+
vi.useFakeTimers();
43+
requestConnectOnboarding("account-1");
44+
clearConnectOnboardingRequest();
45+
await vi.runAllTimersAsync();
46+
expect(navigation.navigate).not.toHaveBeenCalled();
47+
});
48+
49+
it("skips accounts that opted out", async () => {
50+
vi.useFakeTimers();
51+
optOut.optedOut.add("account-1");
52+
requestConnectOnboarding("account-1");
53+
await vi.runAllTimersAsync();
54+
expect(navigation.navigate).not.toHaveBeenCalled();
55+
});
56+
57+
it("presents once when a newer request replaces a pending one", async () => {
58+
vi.useFakeTimers();
59+
requestConnectOnboarding("account-1");
60+
requestConnectOnboarding("account-2");
61+
await vi.runAllTimersAsync();
62+
expect(navigation.navigate).toHaveBeenCalledOnce();
63+
});
64+
65+
it("drops a request cleared while it waits for navigation to be ready", async () => {
66+
vi.useFakeTimers();
67+
navigation.ready = false;
68+
requestConnectOnboarding("account-1");
69+
await vi.runAllTimersAsync();
70+
clearConnectOnboardingRequest();
71+
for (const navigate of navigation.queue.splice(0)) navigate();
72+
expect(navigation.navigate).not.toHaveBeenCalled();
73+
});
74+
});

0 commit comments

Comments
 (0)