@samitouri / QOS-React-1 / commits / 7f1a085b28

[DevTools] Show list of named Activities in Suspense tab (#35092)

Sebastian "Sebbie" Silbermann committed Nov 18, 2025 at 09:52 UTC 7f1a085b282d30dbb151f40c41bd53fc9045deb1
7 files changed +221 -99
packages/react-devtools-shared/src/devtools/store.js
+27
@@ -1058,6 +1058,33 @@ export default class Store extends EventEmitter<{
1058 return timeline;
1059 }
1060
1061 + getActivities(): Array<{id: Element['id'], depth: number}> {
1062 + const target: Array<{id: Element['id'], depth: number}> = [];
1063 + // TODO: Keep a live tree in the backend so we don't need to recalculate
1064 + // this each time while also including filtered Activities.
1065 + this._pushActivitiesInDocumentOrder(this.roots, target, 0);
1066 + return target;
1067 + }
1068 +
1069 + _pushActivitiesInDocumentOrder(
1070 + children: $ReadOnlyArray<Element['id']>,
1071 + target: Array<{id: Element['id'], depth: number}>,
1072 + depth: number,
1073 + ): void {
1074 + for (let i = 0; i < children.length; i++) {
1075 + const child = this._idToElement.get(children[i]);
1076 + if (child === undefined) {
1077 + continue;
1078 + }
1079 + if (child.type === ElementTypeActivity && child.nameProp !== null) {
1080 + target.push({id: child.id, depth});
1081 + this._pushActivitiesInDocumentOrder(child.children, target, depth + 1);
1082 + } else {
1083 + this._pushActivitiesInDocumentOrder(child.children, target, depth);
1084 + }
1085 + }
1086 + }
1087 +
1088 getRendererIDForElement(id: number): number | null {
1089 let current = this._idToElement.get(id);
1090 while (current !== undefined) {
packages/react-devtools-shared/src/devtools/views/Components/TreeContext.js
+6 -1
@@ -59,6 +59,7 @@ export type StateContext = {
59
60 // Activity slice
61 activityID: Element['id'] | null,
62 + activities: $ReadOnlyArray<{id: Element['id'], depth: number}>,
63
64 // Inspection element panel
65 inspectedElementID: number | null,
@@ -172,6 +173,7 @@ type State = {
173
174 // Activity slice
175 activityID: Element['id'] | null,
176 + activities: $ReadOnlyArray<{id: Element['id'], depth: number}>,
177
178 // Inspection element panel
179 inspectedElementID: number | null,
@@ -809,6 +811,7 @@ function reduceActivityState(
811 case 'HANDLE_STORE_MUTATION':
812 let {activityID} = state;
813 const [, , activitySliceIDChange] = action.payload;
814 + const activities = store.getActivities();
815 if (activitySliceIDChange === 0 && activityID !== null) {
816 activityID = null;
817 } else if (
@@ -817,10 +820,11 @@ function reduceActivityState(
820 ) {
821 activityID = activitySliceIDChange;
822 }
820 - if (activityID !== state.activityID) {
823 + if (activityID !== state.activityID || activities !== state.activities) {
824 return {
825 ...state,
826 activityID,
827 + activities,
828 };
829 }
830 }
@@ -863,6 +867,7 @@ function getInitialState({
867
868 // Activity slice
869 activityID: null,
870 + activities: store.getActivities(),
871
872 // Inspection element panel
873 inspectedElementID:
packages/react-devtools-shared/src/devtools/views/SuspenseTab/ActivityList.css
+17 -4
@@ -1,20 +1,33 @@
1 -.ActivityList {
1 +.ActivityListContaier {
2 + display: flex;
3 + flex-direction: column;
4 +}
5 +
6 +.ActivityListHeader {
7 + /* even if empty, provides layout alignment with the main view */
8 + display: flex;
9 + flex: 0 0 42px;
10 + border-bottom: 1px solid var(--color-border);
11 +}
12 +
13 +.ActivityListList {
14 cursor: default;
15 list-style-type: none;
16 margin: 0;
17 padding: 0;
18 }
19
8 -.ActivityList[data-pending-activity-slice-selection="true"] {
20 +.ActivityListList[data-pending-activity-slice-selection="true"] {
21 cursor: wait;
22 }
23
12 -.ActivityList:focus {
24 +.ActivityListList:focus {
25 outline: none;
26 }
27
28 .ActivityListItem {
29 color: var(--color-component-name);
30 + line-height: var(--line-height-data);
31 padding: 0 0.25rem;
32 user-select: none;
33 }
@@ -27,7 +40,7 @@
40 background-color: var(--color-background-inactive);
41 }
42
30 -.ActivityList:focus .ActivityListItem[aria-selected="true"] {
43 +.ActivityListList:focus .ActivityListItem[aria-selected="true"] {
44 background-color: var(--color-background-selected);
45 color: var(--color-text-selected);
46
packages/react-devtools-shared/src/devtools/views/SuspenseTab/ActivityList.js
+108 -27
@@ -15,10 +15,14 @@ import typeof {
15 SyntheticMouseEvent,
16 SyntheticKeyboardEvent,
17 } from 'react-dom-bindings/src/events/SyntheticEvent';
18 +import type Store from 'react-devtools-shared/src/devtools/store';
19
20 import * as React from 'react';
20 -import {useContext, useTransition} from 'react';
21 -import {ComponentFilterActivitySlice} from 'react-devtools-shared/src/frontend/types';
21 +import {useContext, useMemo, useTransition} from 'react';
22 +import {
23 + ComponentFilterActivitySlice,
24 + ElementTypeActivity,
25 +} from 'react-devtools-shared/src/frontend/types';
26 import styles from './ActivityList.css';
27 import {
28 TreeStateContext,
@@ -26,6 +30,8 @@ import {
30 } from '../Components/TreeContext';
31 import {useHighlightHostInstance} from '../hooks';
32 import {StoreContext} from '../context';
33 +import ButtonIcon from '../ButtonIcon';
34 +import Button from '../Button';
35
36 export function useChangeActivitySliceAction(): (
37 id: Element['id'] | null,
@@ -62,15 +68,49 @@ export function useChangeActivitySliceAction(): (
68 return changeActivitySliceAction;
69 }
70
71 +function findNearestActivityParentID(
72 + elementID: Element['id'],
73 + store: Store,
74 +): Element['id'] | null {
75 + let currentID: null | Element['id'] = elementID;
76 + while (currentID !== null) {
77 + const element = store.getElementByID(currentID);
78 + if (element === null) {
79 + return null;
80 + }
81 + if (element.type === ElementTypeActivity) {
82 + return element.id;
83 + }
84 + currentID = element.parentID;
85 + }
86 +
87 + return currentID;
88 +}
89 +
90 +function useSelectedActivityID(): Element['id'] | null {
91 + const {inspectedElementID} = useContext(TreeStateContext);
92 + const store = useContext(StoreContext);
93 + return useMemo(() => {
94 + if (inspectedElementID === null) {
95 + return null;
96 + }
97 + const nearestActivityID = findNearestActivityParentID(
98 + inspectedElementID,
99 + store,
100 + );
101 + return nearestActivityID;
102 + }, [inspectedElementID, store]);
103 +}
104 +
105 export default function ActivityList({
106 activities,
107 }: {
68 - activities: $ReadOnlyArray<Element>,
108 + activities: $ReadOnlyArray<{id: Element['id'], depth: number}>,
109 }): React$Node {
70 - const {inspectedElementID} = useContext(TreeStateContext);
110 + const {activityID, inspectedElementID} = useContext(TreeStateContext);
111 const treeDispatch = useContext(TreeDispatcherContext);
72 - // TODO: Derive from inspected element
73 - const selectedActivityID = inspectedElementID;
112 + const store = useContext(StoreContext);
113 + const selectedActivityID = useSelectedActivityID();
114 const {highlightHostInstance, clearHighlightHostInstance} =
115 useHighlightHostInstance();
116
@@ -79,8 +119,13 @@ export default function ActivityList({
119 const changeActivitySliceAction = useChangeActivitySliceAction();
120
121 function handleKeyDown(event: SyntheticKeyboardEvent) {
82 - // TODO: Implement keyboard navigation
122 switch (event.key) {
123 + case 'Escape':
124 + startActivitySliceSelection(() => {
125 + changeActivitySliceAction(null);
126 + });
127 + event.preventDefault();
128 + break;
129 case 'Enter':
130 case ' ':
131 if (inspectedElementID !== null) {
@@ -149,25 +194,61 @@ export default function ActivityList({
194 }
195
196 return (
152 - <ol
153 - role="listbox"
154 - className={styles.ActivityList}
155 - data-pending-activity-slice-selection={isPendingActivitySliceSelection}
156 - tabIndex={0}
157 - onKeyDown={handleKeyDown}>
158 - {activities.map(activity => (
159 - <li
160 - key={activity.id}
161 - role="option"
162 - aria-selected={activity.id === selectedActivityID ? 'true' : 'false'}
163 - className={styles.ActivityListItem}
164 - onClick={handleClick.bind(null, activity.id)}
165 - onDoubleClick={handleDoubleClick}
166 - onPointerOver={highlightHostInstance.bind(null, activity.id, false)}
167 - onPointerLeave={clearHighlightHostInstance}>
168 - {activity.nameProp}
169 - </li>
170 - ))}
171 - </ol>
197 + <div className={styles.ActivityListContaier}>
198 + <div className={styles.ActivityListHeader}>
199 + {activityID !== null && (
200 + // TODO: Obsolete once filtered Activities are included in this list.
201 + <Button
202 + onClick={startActivitySliceSelection.bind(
203 + null,
204 + changeActivitySliceAction.bind(null, null),
205 + )}
206 + title="Back to full tree view">
207 + <ButtonIcon type="previous" />
208 + </Button>
209 + )}
210 + </div>
211 + <ol
212 + role="listbox"
213 + className={styles.ActivityListList}
214 + data-pending-activity-slice-selection={isPendingActivitySliceSelection}
215 + tabIndex={0}
216 + onKeyDown={handleKeyDown}>
217 + {activities.map(({id, depth}) => {
218 + const activity = store.getElementByID(id);
219 + if (activity === null) {
220 + return null;
221 + }
222 + const name = activity.nameProp;
223 + if (name === null) {
224 + // This shouldn't actually happen. We only want to show activities with a name.
225 + // And hide the whole list if no named Activities are present.
226 + return null;
227 + }
228 +
229 + // TODO: Filtered Activities should have dedicated styles once we include
230 + // filtered Activities in this list.
231 + return (
232 + <li
233 + key={activity.id}
234 + role="option"
235 + aria-selected={
236 + activity.id === selectedActivityID ? 'true' : 'false'
237 + }
238 + className={styles.ActivityListItem}
239 + onClick={handleClick.bind(null, activity.id)}
240 + onDoubleClick={handleDoubleClick}
241 + onPointerOver={highlightHostInstance.bind(
242 + null,
243 + activity.id,
244 + false,
245 + )}
246 + onPointerLeave={clearHighlightHostInstance}>
247 + {'\u00A0'.repeat(depth) + name}
248 + </li>
249 + );
250 + })}
251 + </ol>
252 + </div>
253 );
254 }
packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTab.css
+1 -1
@@ -92,7 +92,7 @@
92 }
93
94 .ActivityList {
95 - flex: 0 0 var(--horizontal-resize-tree-list-percentage);
95 + flex: 0 0 var(--horizontal-resize-activity-list-percentage);;
96 border-right: 1px solid var(--color-border);
97 overflow: auto;
98 }
packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTab.js
+58 -62
@@ -6,14 +6,11 @@
6 *
7 * @flow
8 */
9 -import type {Element} from 'react-devtools-shared/src/frontend/types';
10 -
9 import * as React from 'react';
10 import {
11 useContext,
12 useEffect,
13 useLayoutEffect,
16 - useMemo,
14 useReducer,
15 useRef,
16 Fragment,
@@ -44,12 +41,13 @@ import typeof {SyntheticPointerEvent} from 'react-dom-bindings/src/events/Synthe
41 import SettingsModal from 'react-devtools-shared/src/devtools/views/Settings/SettingsModal';
42 import SettingsModalContextToggle from 'react-devtools-shared/src/devtools/views/Settings/SettingsModalContextToggle';
43 import {SettingsModalContextController} from 'react-devtools-shared/src/devtools/views/Settings/SettingsModalContext';
44 +import {TreeStateContext} from '../Components/TreeContext';
45
46 type Orientation = 'horizontal' | 'vertical';
47
48 type LayoutActionType =
51 - | 'ACTION_SET_TREE_LIST_TOGGLE'
52 - | 'ACTION_SET_TREE_LIST_HORIZONTAL_FRACTION'
49 + | 'ACTION_SET_ACTIVITY_LIST_TOGGLE'
50 + | 'ACTION_SET_ACTIVITY_LIST_HORIZONTAL_FRACTION'
51 | 'ACTION_SET_INSPECTED_ELEMENT_TOGGLE'
52 | 'ACTION_SET_INSPECTED_ELEMENT_HORIZONTAL_FRACTION'
53 | 'ACTION_SET_INSPECTED_ELEMENT_VERTICAL_FRACTION';
@@ -59,8 +57,8 @@ type LayoutAction = {
57 };
58
59 type LayoutState = {
62 - treeListHidden: boolean,
63 - treeListHorizontalFraction: number,
60 + activityListHidden: boolean,
61 + activityListHorizontalFraction: number,
62 inspectedElementHidden: boolean,
63 inspectedElementHorizontalFraction: number,
64 inspectedElementVerticalFraction: number,
@@ -97,7 +95,7 @@ function ToggleUniqueSuspenders() {
95 );
96 }
97
100 -function ToggleTreeList({
98 +function ToggleActivityList({
99 dispatch,
100 state,
101 }: {
@@ -108,13 +106,15 @@ function ToggleTreeList({
106 <Button
107 onClick={() =>
108 dispatch({
111 - type: 'ACTION_SET_TREE_LIST_TOGGLE',
109 + type: 'ACTION_SET_ACTIVITY_LIST_TOGGLE',
110 payload: null,
111 })
112 }
115 - title={state.treeListHidden ? 'Show Tree List' : 'Hide Tree List'}>
113 + title={
114 + state.activityListHidden ? 'Show Activity List' : 'Hide Activity List'
115 + }>
116 <ButtonIcon
117 - type={state.treeListHidden ? 'panel-left-open' : 'panel-left-close'}
117 + type={state.activityListHidden ? 'panel-left-open' : 'panel-left-close'}
118 />
119 </Button>
120 );
@@ -272,17 +272,6 @@ function SynchronizedScrollContainer({
272 );
273 }
274
275 -// TODO: Get this from the store directly.
276 -// The backend needs to keep a separate tree so that resuspending keeps Activity around.
277 -function useActivities(): $ReadOnlyArray<Element> {
278 - const activities = useMemo(() => {
279 - const items: Array<Element> = [];
280 - return items;
281 - }, []);
282 -
283 - return activities;
284 -}
285 -
275 function SuspenseTab(_: {}) {
276 const store = useContext(StoreContext);
277 const {hideSettings} = useContext(OptionsContext);
@@ -292,14 +281,14 @@ function SuspenseTab(_: {}) {
281 initLayoutState,
282 );
283
295 - const activities = useActivities();
284 + const {activities} = useContext(TreeStateContext);
285 // If there are no named Activity boundaries, we don't have any tree list and we should hide
286 // both the panel and the button to toggle it.
298 - const treeListDisabled = activities.length === 0;
287 + const activityListDisabled = activities.length === 0;
288
289 const wrapperTreeRef = useRef<null | HTMLElement>(null);
290 const resizeTreeRef = useRef<null | HTMLElement>(null);
302 - const resizeTreeListRef = useRef<null | HTMLElement>(null);
291 + const resizeActivityListRef = useRef<null | HTMLElement>(null);
292
293 // TODO: We'll show the recently inspected element in this tab when it should probably
294 // switch to the nearest Suspense boundary when we switch into this tab.
@@ -308,8 +297,8 @@ function SuspenseTab(_: {}) {
297 inspectedElementHidden,
298 inspectedElementHorizontalFraction,
299 inspectedElementVerticalFraction,
311 - treeListHidden,
312 - treeListHorizontalFraction,
300 + activityListHidden,
301 + activityListHorizontalFraction,
302 } = state;
303
304 useLayoutEffect(() => {
@@ -328,12 +317,12 @@ function SuspenseTab(_: {}) {
317 inspectedElementVerticalFraction * 100,
318 );
319
331 - const resizeTreeListElement = resizeTreeListRef.current;
320 + const resizeActivityListElement = resizeActivityListRef.current;
321 setResizeCSSVariable(
333 - resizeTreeListElement,
334 - 'tree-list',
322 + resizeActivityListElement,
323 + 'activity-list',
324 'horizontal',
336 - treeListHorizontalFraction * 100,
325 + activityListHorizontalFraction * 100,
326 );
327 }, []);
328 useEffect(() => {
@@ -344,8 +333,8 @@ function SuspenseTab(_: {}) {
333 inspectedElementHidden,
334 inspectedElementHorizontalFraction,
335 inspectedElementVerticalFraction,
347 - treeListHidden,
348 - treeListHorizontalFraction,
336 + activityListHidden,
337 + activityListHorizontalFraction,
338 }),
339 );
340 }, 500);
@@ -355,8 +344,8 @@ function SuspenseTab(_: {}) {
344 inspectedElementHidden,
345 inspectedElementHorizontalFraction,
346 inspectedElementVerticalFraction,
358 - treeListHidden,
359 - treeListHorizontalFraction,
347 + activityListHidden,
348 + activityListHorizontalFraction,
349 ]);
350
351 const onResizeStart = (event: SyntheticPointerEvent) => {
@@ -420,14 +409,14 @@ function SuspenseTab(_: {}) {
409 }
410 };
411
423 - const onResizeTreeList = (event: SyntheticPointerEvent) => {
412 + const onResizeActivityList = (event: SyntheticPointerEvent) => {
413 const element = event.currentTarget;
414 const isResizing = element.hasPointerCapture(event.pointerId);
415 if (!isResizing) {
416 return;
417 }
418
430 - const resizeElement = resizeTreeListRef.current;
419 + const resizeElement = resizeActivityListRef.current;
420 const wrapperElement = resizeTreeRef.current;
421
422 if (wrapperElement === null || resizeElement === null) {
@@ -443,11 +432,11 @@ function SuspenseTab(_: {}) {
432 const currentMousePosition =
433 orientation === 'horizontal' ? event.clientX - left : event.clientY - top;
434
446 - const boundaryMin = MINIMUM_TREE_LIST_SIZE;
435 + const boundaryMin = MINIMUM_ACTIVITY_LIST_SIZE;
436 const boundaryMax =
437 orientation === 'horizontal'
449 - ? width - MINIMUM_TREE_LIST_SIZE
450 - : height - MINIMUM_TREE_LIST_SIZE;
438 + ? width - MINIMUM_ACTIVITY_LIST_SIZE
439 + : height - MINIMUM_ACTIVITY_LIST_SIZE;
440
441 const isMousePositionInBounds =
442 currentMousePosition > boundaryMin && currentMousePosition < boundaryMax;
@@ -455,10 +444,15 @@ function SuspenseTab(_: {}) {
444 if (isMousePositionInBounds) {
445 const resizedElementDimension =
446 orientation === 'horizontal' ? width : height;
458 - const actionType = 'ACTION_SET_TREE_LIST_HORIZONTAL_FRACTION';
447 + const actionType = 'ACTION_SET_ACTIVITY_LIST_HORIZONTAL_FRACTION';
448 const percentage = (currentMousePosition / resizedElementDimension) * 100;
449
461 - setResizeCSSVariable(resizeElement, 'tree-list', orientation, percentage);
450 + setResizeCSSVariable(
451 + resizeElement,
452 + 'activity-list',
453 + orientation,
454 + percentage,
455 + );
456
457 dispatch({
458 type: actionType,
@@ -473,19 +467,21 @@ function SuspenseTab(_: {}) {
467 <SettingsModalContextController>
468 <div className={styles.SuspenseTab} ref={wrapperTreeRef}>
469 <div className={styles.TreeWrapper} ref={resizeTreeRef}>
476 - {treeListDisabled ? null : (
470 + {activityListDisabled ? null : (
471 <div
472 className={styles.ActivityList}
479 - hidden={treeListHidden}
480 - ref={resizeTreeListRef}>
473 + hidden={activityListHidden}
474 + ref={resizeActivityListRef}>
475 <ActivityList activities={activities} />
476 </div>
477 )}
484 - {treeListDisabled ? null : (
485 - <div className={styles.ResizeBarWrapper} hidden={treeListHidden}>
478 + {activityListDisabled ? null : (
479 + <div
480 + className={styles.ResizeBarWrapper}
481 + hidden={activityListHidden}>
482 <div
483 onPointerDown={onResizeStart}
488 - onPointerMove={onResizeTreeList}
484 + onPointerMove={onResizeActivityList}
485 onPointerUp={onResizeEnd}
486 className={styles.ResizeBar}
487 />
@@ -493,10 +489,10 @@ function SuspenseTab(_: {}) {
489 )}
490 <div className={styles.TreeView}>
491 <header className={styles.SuspenseTreeViewHeader}>
496 - {treeListDisabled ? (
492 + {activityListDisabled ? (
493 <div />
494 ) : (
499 - <ToggleTreeList dispatch={dispatch} state={state} />
495 + <ToggleActivityList dispatch={dispatch} state={state} />
496 )}
497 {store.supportsClickToInspect && (
498 <Fragment>
@@ -559,19 +555,19 @@ function SuspenseTab(_: {}) {
555 const LOCAL_STORAGE_KEY = 'React::DevTools::SuspenseTab::layout';
556 const VERTICAL_TREE_MODE_MAX_WIDTH = 600;
557 const MINIMUM_TREE_SIZE = 100;
562 -const MINIMUM_TREE_LIST_SIZE = 100;
558 +const MINIMUM_ACTIVITY_LIST_SIZE = 100;
559
560 function layoutReducer(state: LayoutState, action: LayoutAction): LayoutState {
561 switch (action.type) {
566 - case 'ACTION_SET_TREE_LIST_TOGGLE':
562 + case 'ACTION_SET_ACTIVITY_LIST_TOGGLE':
563 return {
564 ...state,
569 - treeListHidden: !state.treeListHidden,
565 + activityListHidden: !state.activityListHidden,
566 };
571 - case 'ACTION_SET_TREE_LIST_HORIZONTAL_FRACTION':
567 + case 'ACTION_SET_ACTIVITY_LIST_HORIZONTAL_FRACTION':
568 return {
569 ...state,
574 - treeListHorizontalFraction: action.payload,
570 + activityListHorizontalFraction: action.payload,
571 };
572 case 'ACTION_SET_INSPECTED_ELEMENT_TOGGLE':
573 return {
@@ -597,8 +593,8 @@ function initLayoutState(): LayoutState {
593 let inspectedElementHidden = false;
594 let inspectedElementHorizontalFraction = 0.65;
595 let inspectedElementVerticalFraction = 0.5;
600 - let treeListHidden = false;
601 - let treeListHorizontalFraction = 0.35;
596 + let activityListHidden = false;
597 + let activityListHorizontalFraction = 0.35;
598
599 try {
600 let data = localStorageGetItem(LOCAL_STORAGE_KEY);
@@ -608,8 +604,8 @@ function initLayoutState(): LayoutState {
604 inspectedElementHorizontalFraction =
605 data.inspectedElementHorizontalFraction;
606 inspectedElementVerticalFraction = data.inspectedElementVerticalFraction;
611 - treeListHidden = data.treeListHidden;
612 - treeListHorizontalFraction = data.treeListHorizontalFraction;
607 + activityListHidden = data.activityListHidden;
608 + activityListHorizontalFraction = data.activityListHorizontalFraction;
609 }
610 } catch (error) {}
611
@@ -617,8 +613,8 @@ function initLayoutState(): LayoutState {
613 inspectedElementHidden,
614 inspectedElementHorizontalFraction,
615 inspectedElementVerticalFraction,
620 - treeListHidden,
621 - treeListHorizontalFraction,
616 + activityListHidden,
617 + activityListHorizontalFraction,
618 };
619 }
620
@@ -634,7 +630,7 @@ function getTreeOrientation(
630
631 function setResizeCSSVariable(
632 resizeElement: null | HTMLElement,
637 - name: 'tree' | 'tree-list',
633 + name: 'tree' | 'activity-list',
634 orientation: null | Orientation,
635 percentage: number,
636 ): void {
packages/react-devtools-shell/src/app/Segments/index.js
+4 -4
@@ -77,13 +77,13 @@ function Root({children}: {children: React.Node}): React.Node {
77
78 export default function Segments(): React.Node {
79 return (
80 - <React.Activity name="/" mode="visible">
80 + <React.Activity name="root" mode="visible">
81 <Root>
82 - <React.Activity name="/outer/" mode="visible">
82 + <React.Activity name="outer" mode="visible">
83 <OuterSegment>
84 - <React.Activity name="/outer/inner" mode="visible">
84 + <React.Activity name="inner" mode="visible">
85 <InnerSegment>
86 - <React.Activity name="/outer/inner/page" mode="visible">
86 + <React.Activity name="slot" mode="visible">
87 <Page />
88 </React.Activity>
89 </InnerSegment>