Skip to content

Commit 538f333

Browse files
tellahoclaudenpub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6wtho
authored
feat(desktop): adding ui state into the history stack (#967)
Signed-off-by: tho <80c5f18be5aafa62cf6198c6335963ba3306b595288117c8ea2f805fc9bdc94a@sprout-oss.stage.blox.sqprod.co> Signed-off-by: Taylor Ho <[email protected]> Co-authored-by: Claude Fable 5 <[email protected]> Co-authored-by: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@sprout-oss.stage.blox.sqprod.co> Co-authored-by: tho <80c5f18be5aafa62cf6198c6335963ba3306b595288117c8ea2f805fc9bdc94a@sprout-oss.stage.blox.sqprod.co>
1 parent c22c54e commit 538f333

24 files changed

Lines changed: 1083 additions & 386 deletions

‎desktop/playwright.config.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export default defineConfig({
2020
name: "smoke",
2121
testMatch: [
2222
"**/smoke.spec.ts",
23+
"**/navigation.spec.ts",
2324
"**/channels.spec.ts",
2425
"**/badge.spec.ts",
2526
"**/channel-browser.spec.ts",

‎desktop/src/app/AppShell.tsx‎

Lines changed: 29 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ import { useProfileQuery } from "@/features/profile/hooks";
6262
import {
6363
DEFAULT_SETTINGS_SECTION,
6464
type SettingsSection,
65+
isSettingsSection,
6566
} from "@/features/settings/ui/SettingsPanels";
6667
import { HuddleBar, HuddleProvider } from "@/features/huddle";
6768
import { AppSidebar } from "@/features/sidebar/ui/AppSidebar";
@@ -95,11 +96,6 @@ export function AppShell() {
9596
const workspacesHook = useWorkspaces();
9697
const [isAddWorkspaceOpen, setIsAddWorkspaceOpen] = React.useState(false);
9798

98-
const [settingsOpen, setSettingsOpen] = React.useState(false);
99-
const [settingsSection, setSettingsSection] = React.useState<SettingsSection>(
100-
DEFAULT_SETTINGS_SECTION,
101-
);
102-
10399
const [isChannelManagementOpen, setIsChannelManagementOpen] =
104100
React.useState(false);
105101
const [searchFocusRequest, setSearchFocusRequest] = React.useState(0);
@@ -119,7 +115,9 @@ export function AppShell() {
119115
goHome,
120116
goProjects,
121117
goPulse,
118+
goSettings,
122119
goWorkflows,
120+
closeSettings,
123121
openSearchHit,
124122
} = useAppNavigation();
125123
const { canGoBack, canGoForward, goBack, goForward } =
@@ -128,6 +126,17 @@ export function AppShell() {
128126
() => deriveShellRoute(location.pathname),
129127
[location.pathname],
130128
);
129+
// Settings lives in the history stack: /settings?section=… opens it, back
130+
// (or "Back to app") returns to the previous entry — panels and all — and
131+
// reloads restore the open section from the URL.
132+
const settingsOpen = location.pathname === "/settings";
133+
const locationSearchSection = (location.search as { section?: unknown })
134+
.section;
135+
const settingsSection: SettingsSection = isSettingsSection(
136+
locationSearchSection,
137+
)
138+
? locationSearchSection
139+
: DEFAULT_SETTINGS_SECTION;
131140

132141
const startupReady = useDeferredStartup();
133142

@@ -336,7 +345,7 @@ export function AppShell() {
336345
identityQuery.data?.pubkey,
337346
notificationSettings.settings,
338347
notificationSettings.setDesktopEnabled,
339-
selectedView === "home",
348+
selectedView === "home" && !settingsOpen,
340349
getChannelReadAt,
341350
readStateVersion,
342351
highPriorityUnreadChannelIds,
@@ -420,15 +429,23 @@ export function AppShell() {
420429
const handleOpenSettings = React.useCallback(
421430
(section: SettingsSection = DEFAULT_SETTINGS_SECTION) => {
422431
setIsChannelManagementOpen(false);
423-
setSettingsSection(section);
424-
setSettingsOpen(true);
432+
void goSettings(section);
425433
},
426-
[],
434+
[goSettings],
427435
);
428436

429437
const handleCloseSettings = React.useCallback(() => {
430-
setSettingsOpen(false);
431-
}, []);
438+
closeSettings();
439+
}, [closeSettings]);
440+
441+
// Section switches rewrite the settings entry rather than stacking one
442+
// history entry per section, so back always exits settings in one step.
443+
const handleSettingsSectionChange = React.useCallback(
444+
(section: SettingsSection) => {
445+
void goSettings(section, { replace: true });
446+
},
447+
[goSettings],
448+
);
432449

433450
const handleOpenSearchResult = React.useCallback(
434451
(hit: SearchHit) => {
@@ -725,7 +742,7 @@ export function AppShell() {
725742
notificationPermission={notificationSettings.permission}
726743
notificationSettings={notificationSettings.settings}
727744
onClose={handleCloseSettings}
728-
onSectionChange={setSettingsSection}
745+
onSectionChange={handleSettingsSectionChange}
729746
onSetDesktopNotificationsEnabled={
730747
notificationSettings.setDesktopEnabled
731748
}

‎desktop/src/app/navigation/useAppNavigation.ts‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,27 @@ export function useAppNavigation() {
185185
[commitNavigation],
186186
);
187187

188+
const goSettings = React.useCallback(
189+
(section?: string, behavior?: NavigationBehavior) =>
190+
commitNavigation(
191+
{
192+
to: "/settings",
193+
search: section ? { section } : {},
194+
},
195+
behavior,
196+
),
197+
[commitNavigation],
198+
);
199+
200+
const closeSettings = React.useCallback(() => {
201+
if (canGoBack) {
202+
router.history.back();
203+
return;
204+
}
205+
206+
void goHome({ replace: true });
207+
}, [canGoBack, goHome, router.history]);
208+
188209
const closeWorkflowDetail = React.useCallback(() => {
189210
if (canGoBack) {
190211
router.history.back();
@@ -231,6 +252,7 @@ export function useAppNavigation() {
231252

232253
return {
233254
closeForumPost,
255+
closeSettings,
234256
closeWorkflowDetail,
235257
goAgents,
236258
goChannel,
@@ -239,6 +261,7 @@ export function useAppNavigation() {
239261
goProject,
240262
goProjects,
241263
goPulse,
264+
goSettings,
242265
goWorkflow,
243266
goWorkflows,
244267
openSearchHit,

‎desktop/src/app/routeTree.gen.ts‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
import { Route as rootRouteImport } from "./routes/root";
88
import { Route as workflowsRouteImport } from "./routes/workflows";
9+
import { Route as settingsRouteImport } from "./routes/settings";
910
import { Route as pulseRouteImport } from "./routes/pulse";
1011
import { Route as projectsRouteImport } from "./routes/projects";
1112
import { Route as agentsRouteImport } from "./routes/agents";
@@ -20,6 +21,11 @@ const workflowsRoute = workflowsRouteImport.update({
2021
path: "/workflows",
2122
getParentRoute: () => rootRouteImport,
2223
} as any);
24+
const settingsRoute = settingsRouteImport.update({
25+
id: "/settings",
26+
path: "/settings",
27+
getParentRoute: () => rootRouteImport,
28+
} as any);
2329
const pulseRoute = pulseRouteImport.update({
2430
id: "/pulse",
2531
path: "/pulse",
@@ -67,6 +73,7 @@ export interface FileRoutesByFullPath {
6773
"/agents": typeof agentsRoute;
6874
"/projects": typeof projectsRoute;
6975
"/pulse": typeof pulseRoute;
76+
"/settings": typeof settingsRoute;
7077
"/workflows": typeof workflowsRoute;
7178
"/channels/$channelId": typeof channelsDotchannelIdRoute;
7279
"/projects/$projectId": typeof projectsDotprojectIdRoute;
@@ -78,6 +85,7 @@ export interface FileRoutesByTo {
7885
"/agents": typeof agentsRoute;
7986
"/projects": typeof projectsRoute;
8087
"/pulse": typeof pulseRoute;
88+
"/settings": typeof settingsRoute;
8189
"/workflows": typeof workflowsRoute;
8290
"/channels/$channelId": typeof channelsDotchannelIdRoute;
8391
"/projects/$projectId": typeof projectsDotprojectIdRoute;
@@ -90,6 +98,7 @@ export interface FileRoutesById {
9098
"/agents": typeof agentsRoute;
9199
"/projects": typeof projectsRoute;
92100
"/pulse": typeof pulseRoute;
101+
"/settings": typeof settingsRoute;
93102
"/workflows": typeof workflowsRoute;
94103
"/channels/$channelId": typeof channelsDotchannelIdRoute;
95104
"/projects/$projectId": typeof projectsDotprojectIdRoute;
@@ -103,6 +112,7 @@ export interface FileRouteTypes {
103112
| "/agents"
104113
| "/projects"
105114
| "/pulse"
115+
| "/settings"
106116
| "/workflows"
107117
| "/channels/$channelId"
108118
| "/projects/$projectId"
@@ -114,6 +124,7 @@ export interface FileRouteTypes {
114124
| "/agents"
115125
| "/projects"
116126
| "/pulse"
127+
| "/settings"
117128
| "/workflows"
118129
| "/channels/$channelId"
119130
| "/projects/$projectId"
@@ -125,6 +136,7 @@ export interface FileRouteTypes {
125136
| "/agents"
126137
| "/projects"
127138
| "/pulse"
139+
| "/settings"
128140
| "/workflows"
129141
| "/channels/$channelId"
130142
| "/projects/$projectId"
@@ -137,6 +149,7 @@ export interface RootRouteChildren {
137149
agentsRoute: typeof agentsRoute;
138150
projectsRoute: typeof projectsRoute;
139151
pulseRoute: typeof pulseRoute;
152+
settingsRoute: typeof settingsRoute;
140153
workflowsRoute: typeof workflowsRoute;
141154
channelsDotchannelIdRoute: typeof channelsDotchannelIdRoute;
142155
projectsDotprojectIdRoute: typeof projectsDotprojectIdRoute;
@@ -153,6 +166,13 @@ declare module "@tanstack/react-router" {
153166
preLoaderRoute: typeof workflowsRouteImport;
154167
parentRoute: typeof rootRouteImport;
155168
};
169+
"/settings": {
170+
id: "/settings";
171+
path: "/settings";
172+
fullPath: "/settings";
173+
preLoaderRoute: typeof settingsRouteImport;
174+
parentRoute: typeof rootRouteImport;
175+
};
156176
"/pulse": {
157177
id: "/pulse";
158178
path: "/pulse";
@@ -217,6 +237,7 @@ const rootRouteChildren: RootRouteChildren = {
217237
agentsRoute: agentsRoute,
218238
projectsRoute: projectsRoute,
219239
pulseRoute: pulseRoute,
240+
settingsRoute: settingsRoute,
220241
workflowsRoute: workflowsRoute,
221242
channelsDotchannelIdRoute: channelsDotchannelIdRoute,
222243
projectsDotprojectIdRoute: projectsDotprojectIdRoute,

‎desktop/src/app/routes.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export const routes = rootRoute("root.tsx", [
44
index("index.tsx"),
55
route("/agents", "agents.tsx"),
66
route("/pulse", "pulse.tsx"),
7+
route("/settings", "settings.tsx"),
78
route("/workflows", "workflows.tsx"),
89
route("/workflows/$workflowId", "workflows.$workflowId.tsx"),
910
route("/projects", "projects.tsx"),

‎desktop/src/app/routes/ChannelRouteScreen.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ export function ChannelRouteScreen({
4242
React.useEffect(() => {
4343
let isCancelled = false;
4444

45-
if (!targetMessageId || selectedPostId) {
45+
if ((!targetMessageId && !targetThreadRootId) || selectedPostId) {
4646
setTargetMessageEvents([]);
4747
return () => {
4848
isCancelled = true;

‎desktop/src/app/routes/channels.$channelId.tsx‎

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,32 @@ import { createFileRoute } from "@tanstack/react-router";
44
import { ViewLoadingFallback } from "@/shared/ui/ViewLoadingFallback";
55

66
type ChannelRouteSearch = {
7+
agentSession?: string;
78
messageId?: string;
9+
profile?: string;
10+
profileView?: "memories" | "channels";
11+
thread?: string;
812
threadRootId?: string;
913
};
1014

15+
function nonEmptyString(value: unknown): string | undefined {
16+
return typeof value === "string" && value.length > 0 ? value : undefined;
17+
}
18+
19+
function profileViewValue(value: unknown): "memories" | "channels" | undefined {
20+
return value === "memories" || value === "channels" ? value : undefined;
21+
}
22+
1123
function validateChannelSearch(
1224
search: Record<string, unknown>,
1325
): ChannelRouteSearch {
1426
return {
15-
messageId:
16-
typeof search.messageId === "string" && search.messageId.length > 0
17-
? search.messageId
18-
: undefined,
19-
threadRootId:
20-
typeof search.threadRootId === "string" && search.threadRootId.length > 0
21-
? search.threadRootId
22-
: undefined,
27+
agentSession: nonEmptyString(search.agentSession),
28+
messageId: nonEmptyString(search.messageId),
29+
profile: nonEmptyString(search.profile),
30+
profileView: profileViewValue(search.profileView),
31+
thread: nonEmptyString(search.thread),
32+
threadRootId: nonEmptyString(search.threadRootId),
2333
};
2434
}
2535

@@ -46,7 +56,7 @@ function ChannelRouteComponent() {
4656
selectedPostId={null}
4757
targetMessageId={search.messageId ?? null}
4858
targetReplyId={null}
49-
targetThreadRootId={search.threadRootId ?? null}
59+
targetThreadRootId={search.threadRootId ?? search.thread ?? null}
5060
/>
5161
</React.Suspense>
5262
);

‎desktop/src/app/routes/index.tsx‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,21 @@ import {
1010
} from "@/features/onboarding/welcome";
1111
import { useIdentityQuery } from "@/shared/api/hooks";
1212

13+
type HomeRouteSearch = {
14+
item?: string;
15+
};
16+
17+
function validateHomeSearch(search: Record<string, unknown>): HomeRouteSearch {
18+
return {
19+
item:
20+
typeof search.item === "string" && search.item.length > 0
21+
? search.item
22+
: undefined,
23+
};
24+
}
25+
1326
export const Route = createFileRoute("/")({
27+
validateSearch: validateHomeSearch,
1428
component: HomeRouteComponent,
1529
});
1630

‎desktop/src/app/routes/pulse.tsx‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,28 @@ const PulseScreen = React.lazy(async () => {
99
return { default: module.PulseScreen };
1010
});
1111

12+
type PulseRouteSearch = {
13+
profile?: string;
14+
profileView?: "memories" | "channels";
15+
};
16+
17+
function validatePulseSearch(
18+
search: Record<string, unknown>,
19+
): PulseRouteSearch {
20+
return {
21+
profile:
22+
typeof search.profile === "string" && search.profile.length > 0
23+
? search.profile
24+
: undefined,
25+
profileView:
26+
search.profileView === "memories" || search.profileView === "channels"
27+
? search.profileView
28+
: undefined,
29+
};
30+
}
31+
1232
export const Route = createFileRoute("/pulse")({
33+
validateSearch: validatePulseSearch,
1334
component: PulseRouteComponent,
1435
});
1536

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { createFileRoute } from "@tanstack/react-router";
2+
3+
import {
4+
type SettingsSection,
5+
isSettingsSection,
6+
} from "@/features/settings/ui/SettingsPanels";
7+
8+
type SettingsRouteSearch = {
9+
section?: SettingsSection;
10+
};
11+
12+
function validateSettingsSearch(
13+
search: Record<string, unknown>,
14+
): SettingsRouteSearch {
15+
return {
16+
section: isSettingsSection(search.section) ? search.section : undefined,
17+
};
18+
}
19+
20+
export const Route = createFileRoute("/settings")({
21+
validateSearch: validateSettingsSearch,
22+
component: SettingsRouteComponent,
23+
});
24+
25+
// Settings renders at the AppShell level (it replaces the sidebar, top
26+
// chrome, and router outlet wholesale), keyed off this route's presence —
27+
// see AppShell. The outlet is unmounted while settings is open, so this
28+
// component never actually renders.
29+
function SettingsRouteComponent() {
30+
return null;
31+
}

0 commit comments

Comments
 (0)