Skip to content

Conversation

@dotproto
Copy link
Member

@dotproto dotproto commented Apr 24, 2025

This proposal introduces a tab.focus() method and supporting tab namespace properties.

Status

This is the first draft of the proposal. It is not entirely contiguous, but initial feedback is welcome.

@hanguokai
Copy link
Member

Thanks for the proposal! Besides the method tab.focus(), I think we also need a method to determine whether the current area has obtained the focus or where the current focus is.

@dotproto
Copy link
Member Author

When writing the first draft of this proposal I didn't have a clear picture for how we should handle errors in foucs() calls. As a result, I accidentally mixed up the concepts of rejecting with an Error and resolving with an object that signaled that the operation could not be completed. I'd appreciate feedback or recommendations on how to approach this.

@dotproto
Copy link
Member Author

dotproto commented Apr 24, 2025

I think we also need a method to determine whether the current area has obtained the focus or where the current focus is.

My intent was that the promise returned by the focus() call should signal the success/failure of the request. As mentioned in my last comment, though, the current draft mixes up rejecting with an error and resolving with a code to signal success/failure.

@hanguokai, can you describe a use case for wanting to query the browser's current focus? I didn't see a need for that in the use case that motivated the proposal.

@qupig

This comment was marked as outdated.

@qupig
Copy link

qupig commented Apr 24, 2025

@dotproto Preliminary review of the proposal, I'm sorry I object to the API design.

Such a call is very strange and cumbersome:

browser.tabs.focus({area: "sidebar"})

The main reasons are:

  • The browser.tabs namespace should not be used to manipulate sidebar interactions; it is not intuitive and makes it difficult for developers to locate the API.
  • Especially when we have a corresponding browser.sidebarAction namespace.
  • The sidebar area (i.e. the secondary UI you mentioned) is not actually part of the tab scope, it is not exactly related or corresponding to the current page.

I think possible alternative designs are:

  • Use a new namespace, such as browser.focus instead of browser.tabs.focus.
  • Implement the focus method in various namespaces, namely: browser.tabs.focus(), browser.sidebarAction.focus(), chrome.sidePanel.focus(), browser.omnibox.focus() etc.

I tend to the latter because it's intuitive enough and simple enough that you don't actually need to pass in any parameters.

The second problem has to be mentioned here, that is, we should not pass in options such as windowId, tabId in this API, because that is the job of tabs.update() or windows.update(). The API should only be used for focus switching on the current tab of the current window.

The third point is that above I also mentioned browser.omnibox.focus(), cause I think that in addition to tab/page, sidebar, we might also need to shift the focus to omnibox, but I don't have a specific use case for that at the moment.

Comment on lines +161 to +165
## Security and Privacy

Focus management presents a moderate risk to end users. While changing the user agent’s current focus does not grant access to any data in itself, it greatly increases the risk that a user will perform an unintended action. When combined with [user activation](https://html.spec.whatwg.org/multipage/interaction.html#tracking-user-activation), this capability can be used to receive escalated permissions or to perform a restricted operation without the user’s express permission.

As such, this proposal intentionally does not entertain the possibility of allowing extensions to focus user agent UI surfaces such as an extension’s action button. When considering future expansion of focus management capabilities, the authors recommend gating the ability to focus dangerous such UI surfaces behind a permission with a warning.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The security concern here is a misinterpretation of user activation.

Because we actually already have the relevant APIs for opening the extensions popup or sidebar:

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And these APIs have the user activation restriction applied already:

browser.action.openPopup();
Uncaught (in promise) Error: action.openPopup may only be called from a user input handler
browser.sidebarAction.open();
Uncaught (in promise) Error: sidebarAction.open may only be called from a user input handler

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There have been a few discussions in this group about removing the user activation requirement from action.openPopup(), sidebarAction.open(), and sidePanel.open(). To the best of my knowledge browsers are aligned on removing the requirement from action.openPopup(), but I'm not sure there's consensus around the sidebar concepts.

To be clear, toggling the visibility of the sidebar and popup are not the concern here. User interactions with browser controlled extension UI surfaces apply a user activation (also refereed to as a user gesture) flag in the event handler. Many web and extension APIs and are also gated behind user activation, such as invoking permissions.request() to request an optional (or disabled) permission.

More importantly, though, user interactions with browser controlled extension UI surfaces also grant activeTab. This gives the extension temporary host permission access to the page on which the grant occured, which includes the ability to inject scripts into a page. In situations where an extension doesn't request activeTab but does request broad host permissions, users can use browser settings to configure the extension to only "run on click." In this mode, extensions behave similarly to the activeTab: they will only inject content scripts when an appropriate trigger is invoked by the user and the host permissions granted are revoked after navigation.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, that explains the broader considerations.
Because it initially sounds like preventing the open/focus popup (action button).
It should be mentioned that if intend to focus only the tab-top-window, it is unnecessary to obtain host permissions.

@qupig
Copy link

qupig commented Apr 24, 2025

Another issue is like what I mentioned in my initial comment in #693 (comment), some of the current APIs automatically shift focus when we call them, and we should probably add options to those APIs to prevent focus shifting.

Although we could for example do so (to keep focus still on sidebar):

await browser.tabs.update(tabId, { active: true }); // focus automatically shifts to `tab` (page)
await browser.sidebarAction.focus(); // manually shift the focus back to `sidebar`

But it has two focus transfer, which can be unexpected or delayed, ultimately reducing the user experience.

So a better alternative might be add a focus property in updateProperties:

browser.tabs.update(tabId, { active: true, focus: false });

Note

{active: true} will not make tab to gain focus if the current window does not have focus.
Therefore, there are differences in the uses and purposes of the two.

And we may want to add corresponding property to tabs.Tab, which may fulfill the needs of, say, @hanguokai.

And I think this maybe a better alternative to the above browser.tabs.focus(), because it represents the concrete tab rather than the vague plural notion of tabs. But one issue that needs attention and consideration is that when set {focus: true}, {active: true} may also should be automatically specified at the same time, otherwise the result may be unexpected.

@carlosjeurissen
Copy link
Contributor

carlosjeurissen commented Apr 24, 2025

Currently, calling sidebar.open() does not directly focus the opened sidebar. If we do not want to change this default behaviour for opening a sidebar, we can consider passing focus as an option to the open method as such:

browser.sidebar.open({
  focus: true,
});

This has a number of benefits:

  • Assures the sidebar will be open (i.e. no error when calling while the sidebar is closed)
  • sidePanel.open and sidebarAction.open currently already require user activation
  • Already comes with tabId/windowId and a lot of related logic
  • Solves awkward chaining of calls to sidebar.open(), followed by tabs.focus({area: "sidebar"})
  • Will not be awkwardly under the tabs namespace

Considering action.openPopup() already allows focussing like this I do not see any new attack vectors.

It could even be argued focussing the sidebar after sidebar.open() should be the default and expected behaviour. Which, for potential backwards compatibility, we could make the default in a unified browser.sidebar.open() method.

@qupig
Copy link

qupig commented Apr 24, 2025

@carlosjeurissen

Assures the sidebar will be open (i.e. no error when calling while the sidebar is closed)

Why does it cause an error when sidebar is closed? Do you want to express that it is called again when it is already opened?

Already comes with tabId/windowId and a lot of related logic

What are you referring to?

Solves awkward chaining of calls to sidebar.open(), followed by tabs.focus({area: "sidebar"})

Actually I don't think it's awkward:

await browser.sidebarAction.open();
await browser.sidebarAction.focus();

But I don't object to adding options to the open() method, it has some convenience.

It could even be argued focussing the sidebar after sidebar.open() should be the default and expected behaviour.

I can see the logic of the argument, but I can also immediately think of some negative examples, such as if sidebar is just used to show some information, then it does not need to have to get focus.

@dotproto
Copy link
Member Author

dotproto commented Apr 25, 2025

I think in the Async and Promise model, "rejected" means error/failed, "resolved/fulfilled" means success, you should never return an error code in the "resolved results" to signal failure.

If there must be an error code there, put it in the "rejected reason".

The W3C TAG's Writing Promise-Using Specifications states that "rejection reasons must be Error instances" (4.1.2.) and "rejections must be used for exceptional situations" (4.1.3.). That provides some pretty strong guidance on how rejections should be handled.

The thing I am unsure about is what failure cases should be considered exceptional. I think it makes sense to reject if the provided tabId doesn't match any open tabs or the area provided isn't supported by the browser. I'm less sure about rejecting when the target can't be focused because the browser has a modal open. I'm not sure if there are other cases I haven't yet considered where the UI surface isn't targeted but rejecting isn't appropriate.

@dotproto Preliminary review of the proposal, I'm sorry I object to the API design.

No need to apologize. We're here to share and discuss feedback :)

I agree with the critiques you made of exposing this capability on the tabs namespace. As @rdcronin noted a few years back, that namespace is currently a bit of a junk drawer and we should try to avoid dumping more stuff into it.

I had this method in tabs because I was originally targeting moving focus between a sidebar and the main document of a tab. Since both concepts are directly linked to a given tab, it seemed appropriate enough namespace to start. As I iterated on it and the method became more generic, I agree that this no longer feels like the right namespace.

I hadn't given the idea of creating different focus() methods for each namespace much consideration. I idea did come up briefly, but at the time I was thinking about designing for the future and I wasn't confident that the pattern of having distinct namespaces for each matching UI surface would hold.

That said, the longer I sit with this idea the more I like it. It also addresses something that's bothered me about my currently proposed approach: if the options object grows to accommodate the needs of arbitrary UI surfaces, it may become very difficult for developers (and maintainers) to reason about. Namespace specific focus() methods would greatly simply the common set of properties and make it much easier to tailor new targeting controls to specific UI surfaces.

The second problem has to be mentioned here, that is, we should not pass in options such as windowId, tabId in this API, because that is the job of tabs.update() or windows.update(). The API should only be used for focus switching on the current tab of the current window.

I was envisioning the currently proposed tabs.focus() method in part as a more direct way focus a given window and tab. Personally, I find it a bit annoying that to focus a different window and tab you currently have to do something like this:

const focusTab = async (tabId) => {
  const tab = await browser.tabs.update(tabId, { active: true });
  browser.windows.update(tab.windowId, { focused: true });
}

If we have namespace specific methods that only operate on the current window and tab, this would pattern would become more cumbersome:

const focusTabSidebar = async (tabId) => {
  const tab = await browser.tabs.update(tabId, { active: true });
  browser.windows.update(tab.windowId, { focused: true });
  if (browser.sidebarAction) {
    await browser.sidebarAction.open();
    await browser.sidebarAction.focus();
  } else {
    await browser.sidePanel.open();
    await browser.sidePanel.focus();
  }
}

This could be simplified in the future if we align on a common sidebar namespace as has been floated in previous discussion, but it's still a decent number of steps for what should be a relatively simple operation. And this doesn't include error handling.

The third point is that above I also mentioned browser.omnibox.focus(), cause I think that in addition to tab/page, sidebar, we might also need to shift the focus to omnibox, but I don't have a specific use case for that at the moment.

I touched on this idea in the Future Work > Ancillary UI surfaces section of the proposal. We also briefly discussed it in today's WECG meeting. @Rob--W raised serious concerns about allowing extensions to focus the address bar, but we didn't have time to begin exploring those concerns.

@dotproto
Copy link
Member Author

@carlosjeurissen, I see how adding a focus option to browser.sidebar.open() could be useful. That said, I worry that using this as the main way of focusing the sidebar could lead to accidental navigation of the sidebar page. That in turn could lead to unexpected refreshes, loss of interaction state, or even data loss. I think we'd still need a way to focus the sidebar non-destructively.


Assures the sidebar will be open (i.e. no error when calling while the sidebar is closed)

Why does it cause an error when sidebar is closed? Do you want to express that it is called again when it is already opened?

That's basically the behavior described in the current proposal (highlight added for clarity):

tabs.FocusArea

...
sidebar (value: "sidebar") – The extension’s sidePanel or sidebarAction page. The extension’s sidebar page for a given window and tab. When focusing this area, the extension's sidebar will be activated if it is open. If not, the focus() call will fail.


Already comes with tabId/windowId and a lot of related logic

What are you referring to?

The sidePanel.open() and sidePanel.setOptions() methods allow callers to set per-tab sidebar pages. That's why the current proposal includes the optional windowId and tabId properties in tabs.focus()'s options object.

@qupig
Copy link

qupig commented Apr 25, 2025

The W3C TAG's ...
The thing I am unsure about is what failure cases should be considered exceptional. I think it makes sense to reject if the provided tabId doesn't match any open tabs or the area provided isn't supported by the browser. I'm less sure about rejecting when the target can't be focused because the browser has a modal open. I'm not sure if there are other cases I haven't yet considered where the UI surface isn't targeted but rejecting isn't appropriate.

Thanks for citing the specification, but I think it is exactly in line with my understanding.

In this particular case, when you call the focus() method, if the desired object successfully gains focus, it is resolved successfully (regardless of whether it has already gained focus before), and if it does not get focus, it is rejected. I think this is clear to me. Maybe my previous expression was not clear enough.

Otherwise, it is a nightmare for developers, both "to catch error when throw" and "to check the return value when resolved" to confirm whether foucs is successful.

I was envisioning the currently proposed tabs.focus() method in part as a more direct way focus a given window and tab. Personally, I find it a bit annoying that to focus a different window and tab you currently have to do something like this...

Simply put, I agree.
But I was mean, unless you are going to design a new and well-developed API set and intend to replace and deprecate some old ones, there are multiple overlapping methods that may not be a good result.

If we have namespace specific methods that only operate on the current window and tab, this would pattern would become more cumbersome...

I think there are several problems here:

  • We may need to retain flexibility, such as focusing a part of the window but not focusing the window itself, then when the user actively switches to the window, the focus will be activated in the correct position.
  • While I can see the benefits of simplifying these steps, and for simple purposes I like it, it is important to consider that some operations are not strictly bound.
  • The misalignment of browser.sidebarAction and chrome.sidePanel is a significant problem here, and I'm not sure if there are other issues that track this.

I touched on this idea in the Future Work > Ancillary UI surfaces section of the proposal. We also briefly discussed it in today's WECG meeting. @Rob--W raised serious concerns about allowing extensions to focus the address bar, but we didn't have time to begin exploring those concerns.

Yes I saw this in later reading. I really didn't think of a use case at the moment, but it is still necessary to consider this possibility in design. So maybe it's not worth too much attention before fully considering the use cases and risks.


That's basically the behavior described in the current proposal (highlight added for clarity):

Thanks, this explains my question, which makes sense for the focus operation, I was limited to the open method before.

The sidePanel.open() and sidePanel.setOptions() methods allow callers to set per-tab sidebar pages. That's why the current proposal includes the optional windowId and tabId properties in tabs.focus()'s options object.

OK, thanks and that explains part of it.

I think the concept of user intuition is that one interaction (such as a click) will only get a certain part/element visible in the current screen to focus, or switch to another view.
In other words, it would sound weird if a click focuses on a part/element in a currently invisible view.
I hope this explains why I was expected it to only work on the visible part of the current window (tab, sidebar etc.).

In fact, I am disappointed with the processing of the first click of the current non-activated window, different performances in different applications and systems.
In some cases, the click will only activate the window instead of focusing elements and triggering events, while in others, it will not, even in the same window of the same application, there still different processing situations.
In other words, when I click on an element in a non-activated window, I don't know if I could to click once to interact, or if I have to click a second time to make a difference.

So I can see the benefits of this for powerful automation and convenience. And if this is the case, you may need to design all the advanced and complex focus management systems separately in browser.focus namespace and may require explicit permission from users because it sounds unexpected to most users.

But I think this seems a little bit beyond what we are currently trying to solve.

@jsuiker
Copy link

jsuiker commented Apr 27, 2025

*** waiting with bated breath ***

@carlosjeurissen
Copy link
Contributor

In this particular case, when you call the focus() method, if the desired object successfully gains focus, it is resolved successfully (regardless of whether it has already gained focus before), and if it does not get focus, it is rejected. I think this is clear to me. Maybe my previous expression was not clear enough.

Otherwise, it is a nightmare for developers, both "to catch error when throw" and "to check the return value when resolved" to confirm whether focus is successful.

An example of a web API which resolves even on some errors is the Fetch API. Which can resolve while the server responds with a 404 error.

In extension space, APIs like permissions.request() resolve even when the user rejected said permissions.

As for if this API should reject or resolve when attempting to focus, we can look at existing behaviour in other APIs like action.openPopup(). Currently in Chrome, when a sign-in prompt like "sign in to x.com with google.com" is open, calling action.openPopup() rejects with a generic "Failed to open popup." Error. So rejecting on failure of .focus() seems to make sense compared to existing behaviour.

In general it seems blocking UI patterns like that needs some more consideration and documentation. For example, with these Google sign-in prompts, extension popups can not be opened not even when the user clicks an extension icon. Which might be a bug @oliverdunk.

@qupig
Copy link

qupig commented Apr 28, 2025

@carlosjeurissen Thanks for providing more references.

An example of a web API which resolves even on some errors is the Fetch API. Which can resolve while the server responds with a 404 error.

In this case, fetching data about the error page may be expected behavior, so reject is obviously inappropriate. It's not a error of the fetch API, it completes the "fetching" task, doesn't it?

In extension space, APIs like permissions.request() resolve even when the user rejected said permissions.

I think in this case it's because it will be called multiple times during the extension lifecycle, it's more of a query to return rather than a task to resolve. In particular, it only actually pops up the authorization dialog to the user the first time.

And the focus() here is a specific one-time task, and I couldn't see a situation where it fails without the need to rejected.

In any case, thanks you all for putting forward these references and I will think more carefully and critically about expressing my views in the future.

@dotproto
Copy link
Member Author

dotproto commented May 1, 2025

In this case, fetching data about the error page may be expected behavior, so reject is obviously inappropriate. It's not a error of the fetch API, it completes the "fetching" task, doesn't it?

That's true. On the other hand, if you try try to perform a cross origin request and the response doesn't have the right CORS headers set the fetch() call will throw at TypeError. You can see in action by navigating to https://example.com, then opening the browsers developer tools and executing the following snippet in the console: await fetch('https://example.org').


Between the discussion here and some offline followup conversations I had with browser engineers, it seems like the best path forward is to revise this proposal to use per-namespace focus() methods. I'm hoping to tackle this in the next few days.

@qupig
Copy link

qupig commented May 2, 2025

Between the discussion here and some offline followup conversations I had with browser engineers, it seems like the best path forward is to revise this proposal to use per-namespace focus() methods. I'm hoping to tackle this in the next few days.

Thank you for all your work and it's great to see it continue to be pushed forward.

I think "focus management" will have more advanced and meticulous improvements in the future, not only at the browser level, but also at the system/window level.

But as far as solving the problem we need right now, per-namespace focus() methods (except tabs.focus()) would be a relatively easy and gentle path.

Note that tabs.focus() should still probably be avoided because it is not clear which tab is, and tabs does not represent the current page part. If the double-column page is implemented in the future, the problem will be more obvious.

For this, I still think that using focus property in updateProperties is a viable alternative in the tabs namespace, which is more in line with current API logic. #817 (comment)

@qupig
Copy link

qupig commented May 7, 2025

I'm now more of the opinion that adding a focus property in updateProperties and the focused property to tabs.Tab is a good alternative in for the browser.tabs namespace.

Currently we could check if a stored tabId is now focused in two ways:

const tabId = 1;
const focusedTab = await browser.tabs.query({active: true, lastFocusedWindow: true})
if(focusedTab.id === tabId) {
	const tab = await browser.tabs.get(tabId);
	// tab...
}

or

const tabId = 1;
const tab = await browser.tabs.get(tabId);
const win = await browser.windows.get(tab.windowId);
if (win.focused && tab.active) {
	// tab...
}

They're all a bit troublesome, and that doesn't include error handling.

Why can't we directly:

const tabId = 1;
const tab = await browser.tabs.get(tabId);
if (tab.focused) {
	// tab...
}

More to the point, it solves the problems I mentioned above without API breaking changes: #817 (comment)

@dotproto
Copy link
Member Author

dotproto commented May 9, 2025

Note that tabs.focus() should still probably be avoided because it is not clear which tab is, and tabs does not represent the current page part. If the double-column page is implemented in the future, the problem will be more obvious.

For this, I still think that using focus property in updateProperties is a viable alternative in the tabs namespace, which is more in line with current API logic. #817 (comment)

If we're going for set of API changes and only introducing a focus() method on the sidebarAction/sidePanel namespace, then I can see tabs.update() + a new property on updateProperties making sense. I think introducing tabs.focus() makes more sense once we have 2 or more other namespaces with focus methods. At that point we start having a consistency issue where focusing a specific document in a tab uses a different pattern than everything else.

At the moment, I think the path we take for the first revision is contingent on how likely we are to either introduce a focus() method on at least one other namespace APIs in the next ~3-5 years or want to expose more advanced focus control for a given tab (target sub-frames or another document in the same tab). If that seems reasonably likely, then we should lean into the pattern now. If it seems less than moderately, then we should tweak the tabs.update() method to better serve the known use cases.

More to the point, it solves the problems I mentioned above without API breaking changes: #817 (comment)

Sorry, I didn't follow this comment. How does introducing a focus() method break create breaking changes?

@qupig
Copy link

qupig commented May 9, 2025

@dotproto Thanks for the reply.

a focus property in updateProperties

First of all, I'm sorry for the point I wrote in all the comments above. I should use focused instead of focus property for consistency with the existing properties in the browser.windows namespace.

I think introducing tabs.focus() makes more sense once we have 2 or more other namespaces with focus methods. At that point we start having a consistency issue where focusing a specific document in a tab uses a different pattern than everything else.

I admit this creates inconsistencies with other namespaces, but this is due to the complexity of the browser.tabs APIs itself. And we've already done this in the browser.windows namespace:

await browser.windows.update( windowId, { focused: true } );

So do we also need to implement browser.windows.focus() to replace it to maintain consistency? (This would be a potential breaking change)

I think there is inconsistency of tabs and windows because of the current demand, they are already more complex than other namespaces.

If other namespaces have similar complexity and have appropriate places to place, I think maybe similar tweaks make sense, for example I've seen that firefox now has a double column sidebar (vertical tabs and extension panel), and to be honest I'm not sure how it will handle focus.

or want to expose more advanced focus control for a given tab (target sub-frames or another document in the same tab). If that seems reasonably likely, then we should lean into the pattern now. If it seems less than moderately, then we should tweak the tabs.update() method to better serve the known use cases.

In fact, if we want to use tabs.focus() for more complex purposes, we will have consistency issue anyway. Because for other namespaces, we are not considering any complex use cases at the moment.

More importantly, if we want to consider more meticulous focus management rather than just switching focus between different blocks (tab, sidebar, etc.), that is, if we want to consider focus management inside the block (like frames or even elements you said), I strongly believe that we should use the brand new namespace browser.focus to specialize in all focus management tasks. It fits your original proposal, just not under the tabs namespace anymore.

Another point is that I'm not sure if a use case like focus sub-frames is currently something to consider or necessary.
Because I think once we get the tab block focused, we could always control the focus within the tab/page by executing the content scripts. (e.g. iframe.contentWindow.focus();)

I think the current focus is mainly to switch focus between different blocks (tab, sidebar, etc.), which is something we currently lack. It is something we cannot do at the moment.

In other words, I think what we need to urgently address at this point should just be refining the current APIs to provide a way to switch focus between blocks (tab, sidebar, etc.), whether it's _namesapce_.focus() or tabs.update(tabId, {focused: true}).

In fact I don't even mind having both tabs.focus() and tabs.update(tabId, {focused: false}), these are all mainly solving the issues existing in the current APIs.

After this is done, we can consider using the new namespace browser.focus to implement any advanced and unified focus management and gradually replace existing APIs in the future, and achieve all the desired consistency, which will be a huge proposal that requires more consideration. Unless you want to do it now, I don't think it's realistic at the moment. I think advanced focus management may be demanding in the future, such as the development of MCP. It should be in the brand new namespace and requires a separate permission.

Sorry, I didn't follow this comment. How does introducing a focus() method break create breaking changes?

Still the use case I mentioned above, how do we use tabs.focus() to prevent the now-existing force focus switch to the tab block in tabs.update()? How do we prevent the call to tabs.update() from making sure the focus remains at sidebar instead of doing one or more focus switches? I can't see how to do this without doing breaking changes besides from adding new properties in updateProperties, this is the easiest way to solve the current problem I think.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants