Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions desktop/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export default defineConfig({
name: "smoke",
testMatch: [
"**/smoke.spec.ts",
"**/navigation.spec.ts",
"**/channels.spec.ts",
"**/badge.spec.ts",
"**/channel-browser.spec.ts",
Expand Down
41 changes: 29 additions & 12 deletions desktop/src/app/AppShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ import { useProfileQuery } from "@/features/profile/hooks";
import {
DEFAULT_SETTINGS_SECTION,
type SettingsSection,
isSettingsSection,
} from "@/features/settings/ui/SettingsPanels";
import { HuddleBar, HuddleProvider } from "@/features/huddle";
import { AppSidebar } from "@/features/sidebar/ui/AppSidebar";
Expand Down Expand Up @@ -177,11 +178,6 @@ export function AppShell() {
const workspacesHook = useWorkspaces();
const [isAddWorkspaceOpen, setIsAddWorkspaceOpen] = React.useState(false);

const [settingsOpen, setSettingsOpen] = React.useState(false);
const [settingsSection, setSettingsSection] = React.useState<SettingsSection>(
DEFAULT_SETTINGS_SECTION,
);

const [isChannelManagementOpen, setIsChannelManagementOpen] =
React.useState(false);
const [searchFocusRequest, setSearchFocusRequest] = React.useState(0);
Expand All @@ -199,7 +195,9 @@ export function AppShell() {
goHome,
goProjects,
goPulse,
goSettings,
goWorkflows,
closeSettings,
openSearchHit,
} = useAppNavigation();
const { canGoBack, canGoForward, goBack, goForward } =
Expand All @@ -208,6 +206,17 @@ export function AppShell() {
() => deriveShellRoute(location.pathname),
[location.pathname],
);
// Settings lives in the history stack: /settings?section=… opens it, back
// (or "Back to app") returns to the previous entry — panels and all — and
// reloads restore the open section from the URL.
const settingsOpen = location.pathname === "/settings";
Comment thread
tellaho marked this conversation as resolved.
const locationSearchSection = (location.search as { section?: unknown })
.section;
const settingsSection: SettingsSection = isSettingsSection(
locationSearchSection,
)
? locationSearchSection
: DEFAULT_SETTINGS_SECTION;

const startupReady = useDeferredStartup();

Expand Down Expand Up @@ -407,7 +416,7 @@ export function AppShell() {
identityQuery.data?.pubkey,
notificationSettings.settings,
notificationSettings.setDesktopEnabled,
selectedView === "home",
selectedView === "home" && !settingsOpen,
getChannelReadAt,
readStateVersion,
highPriorityUnreadChannelIds,
Expand Down Expand Up @@ -491,15 +500,23 @@ export function AppShell() {
const handleOpenSettings = React.useCallback(
(section: SettingsSection = DEFAULT_SETTINGS_SECTION) => {
setIsChannelManagementOpen(false);
setSettingsSection(section);
setSettingsOpen(true);
void goSettings(section);
},
[],
[goSettings],
);

const handleCloseSettings = React.useCallback(() => {
setSettingsOpen(false);
}, []);
closeSettings();
}, [closeSettings]);

// Section switches rewrite the settings entry rather than stacking one
// history entry per section, so back always exits settings in one step.
const handleSettingsSectionChange = React.useCallback(
(section: SettingsSection) => {
void goSettings(section, { replace: true });
},
[goSettings],
);

const handleOpenSearchResult = React.useCallback(
(hit: SearchHit) => {
Expand Down Expand Up @@ -782,7 +799,7 @@ export function AppShell() {
notificationPermission={notificationSettings.permission}
notificationSettings={notificationSettings.settings}
onClose={handleCloseSettings}
onSectionChange={setSettingsSection}
onSectionChange={handleSettingsSectionChange}
onSetDesktopNotificationsEnabled={
notificationSettings.setDesktopEnabled
}
Expand Down
23 changes: 23 additions & 0 deletions desktop/src/app/navigation/useAppNavigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,27 @@ export function useAppNavigation() {
[commitNavigation],
);

const goSettings = React.useCallback(
(section?: string, behavior?: NavigationBehavior) =>
commitNavigation(
{
to: "/settings",
search: section ? { section } : {},
},
behavior,
),
[commitNavigation],
);

const closeSettings = React.useCallback(() => {
if (canGoBack) {
router.history.back();
return;
}

void goHome({ replace: true });
}, [canGoBack, goHome, router.history]);

const closeWorkflowDetail = React.useCallback(() => {
if (canGoBack) {
router.history.back();
Expand Down Expand Up @@ -231,6 +252,7 @@ export function useAppNavigation() {

return {
closeForumPost,
closeSettings,
closeWorkflowDetail,
goAgents,
goChannel,
Expand All @@ -239,6 +261,7 @@ export function useAppNavigation() {
goProject,
goProjects,
goPulse,
goSettings,
goWorkflow,
goWorkflows,
openSearchHit,
Expand Down
21 changes: 21 additions & 0 deletions desktop/src/app/routeTree.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import { Route as rootRouteImport } from "./routes/root";
import { Route as workflowsRouteImport } from "./routes/workflows";
import { Route as settingsRouteImport } from "./routes/settings";
import { Route as pulseRouteImport } from "./routes/pulse";
import { Route as projectsRouteImport } from "./routes/projects";
import { Route as agentsRouteImport } from "./routes/agents";
Expand All @@ -20,6 +21,11 @@ const workflowsRoute = workflowsRouteImport.update({
path: "/workflows",
getParentRoute: () => rootRouteImport,
} as any);
const settingsRoute = settingsRouteImport.update({
id: "/settings",
path: "/settings",
getParentRoute: () => rootRouteImport,
} as any);
const pulseRoute = pulseRouteImport.update({
id: "/pulse",
path: "/pulse",
Expand Down Expand Up @@ -67,6 +73,7 @@ export interface FileRoutesByFullPath {
"/agents": typeof agentsRoute;
"/projects": typeof projectsRoute;
"/pulse": typeof pulseRoute;
"/settings": typeof settingsRoute;
"/workflows": typeof workflowsRoute;
"/channels/$channelId": typeof channelsDotchannelIdRoute;
"/projects/$projectId": typeof projectsDotprojectIdRoute;
Expand All @@ -78,6 +85,7 @@ export interface FileRoutesByTo {
"/agents": typeof agentsRoute;
"/projects": typeof projectsRoute;
"/pulse": typeof pulseRoute;
"/settings": typeof settingsRoute;
"/workflows": typeof workflowsRoute;
"/channels/$channelId": typeof channelsDotchannelIdRoute;
"/projects/$projectId": typeof projectsDotprojectIdRoute;
Expand All @@ -90,6 +98,7 @@ export interface FileRoutesById {
"/agents": typeof agentsRoute;
"/projects": typeof projectsRoute;
"/pulse": typeof pulseRoute;
"/settings": typeof settingsRoute;
"/workflows": typeof workflowsRoute;
"/channels/$channelId": typeof channelsDotchannelIdRoute;
"/projects/$projectId": typeof projectsDotprojectIdRoute;
Expand All @@ -103,6 +112,7 @@ export interface FileRouteTypes {
| "/agents"
| "/projects"
| "/pulse"
| "/settings"
| "/workflows"
| "/channels/$channelId"
| "/projects/$projectId"
Expand All @@ -114,6 +124,7 @@ export interface FileRouteTypes {
| "/agents"
| "/projects"
| "/pulse"
| "/settings"
| "/workflows"
| "/channels/$channelId"
| "/projects/$projectId"
Expand All @@ -125,6 +136,7 @@ export interface FileRouteTypes {
| "/agents"
| "/projects"
| "/pulse"
| "/settings"
| "/workflows"
| "/channels/$channelId"
| "/projects/$projectId"
Expand All @@ -137,6 +149,7 @@ export interface RootRouteChildren {
agentsRoute: typeof agentsRoute;
projectsRoute: typeof projectsRoute;
pulseRoute: typeof pulseRoute;
settingsRoute: typeof settingsRoute;
workflowsRoute: typeof workflowsRoute;
channelsDotchannelIdRoute: typeof channelsDotchannelIdRoute;
projectsDotprojectIdRoute: typeof projectsDotprojectIdRoute;
Expand All @@ -153,6 +166,13 @@ declare module "@tanstack/react-router" {
preLoaderRoute: typeof workflowsRouteImport;
parentRoute: typeof rootRouteImport;
};
"/settings": {
id: "/settings";
path: "/settings";
fullPath: "/settings";
preLoaderRoute: typeof settingsRouteImport;
parentRoute: typeof rootRouteImport;
};
"/pulse": {
id: "/pulse";
path: "/pulse";
Expand Down Expand Up @@ -217,6 +237,7 @@ const rootRouteChildren: RootRouteChildren = {
agentsRoute: agentsRoute,
projectsRoute: projectsRoute,
pulseRoute: pulseRoute,
settingsRoute: settingsRoute,
workflowsRoute: workflowsRoute,
channelsDotchannelIdRoute: channelsDotchannelIdRoute,
projectsDotprojectIdRoute: projectsDotprojectIdRoute,
Expand Down
1 change: 1 addition & 0 deletions desktop/src/app/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export const routes = rootRoute("root.tsx", [
index("index.tsx"),
route("/agents", "agents.tsx"),
route("/pulse", "pulse.tsx"),
route("/settings", "settings.tsx"),
route("/workflows", "workflows.tsx"),
route("/workflows/$workflowId", "workflows.$workflowId.tsx"),
route("/projects", "projects.tsx"),
Expand Down
2 changes: 1 addition & 1 deletion desktop/src/app/routes/ChannelRouteScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export function ChannelRouteScreen({
React.useEffect(() => {
let isCancelled = false;

if (!targetMessageId || selectedPostId) {
if ((!targetMessageId && !targetThreadRootId) || selectedPostId) {
setTargetMessageEvents([]);
return () => {
isCancelled = true;
Expand Down
28 changes: 19 additions & 9 deletions desktop/src/app/routes/channels.$channelId.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,32 @@ import { createFileRoute } from "@tanstack/react-router";
import { ViewLoadingFallback } from "@/shared/ui/ViewLoadingFallback";

type ChannelRouteSearch = {
agentSession?: string;
messageId?: string;
profile?: string;
profileView?: "memories" | "channels";
thread?: string;
threadRootId?: string;
};

function nonEmptyString(value: unknown): string | undefined {
return typeof value === "string" && value.length > 0 ? value : undefined;
}

function profileViewValue(value: unknown): "memories" | "channels" | undefined {
return value === "memories" || value === "channels" ? value : undefined;
}

function validateChannelSearch(
search: Record<string, unknown>,
): ChannelRouteSearch {
return {
messageId:
typeof search.messageId === "string" && search.messageId.length > 0
? search.messageId
: undefined,
threadRootId:
typeof search.threadRootId === "string" && search.threadRootId.length > 0
? search.threadRootId
: undefined,
agentSession: nonEmptyString(search.agentSession),
messageId: nonEmptyString(search.messageId),
profile: nonEmptyString(search.profile),
profileView: profileViewValue(search.profileView),
thread: nonEmptyString(search.thread),
threadRootId: nonEmptyString(search.threadRootId),
};
}

Expand All @@ -46,7 +56,7 @@ function ChannelRouteComponent() {
selectedPostId={null}
targetMessageId={search.messageId ?? null}
targetReplyId={null}
targetThreadRootId={search.threadRootId ?? null}
targetThreadRootId={search.threadRootId ?? search.thread ?? null}
/>
</React.Suspense>
);
Expand Down
14 changes: 14 additions & 0 deletions desktop/src/app/routes/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,21 @@ import {
} from "@/features/onboarding/welcome";
import { useIdentityQuery } from "@/shared/api/hooks";

type HomeRouteSearch = {
item?: string;
};

function validateHomeSearch(search: Record<string, unknown>): HomeRouteSearch {
return {
item:
typeof search.item === "string" && search.item.length > 0
? search.item
: undefined,
};
}

export const Route = createFileRoute("/")({
validateSearch: validateHomeSearch,
component: HomeRouteComponent,
});

Expand Down
21 changes: 21 additions & 0 deletions desktop/src/app/routes/pulse.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,28 @@ const PulseScreen = React.lazy(async () => {
return { default: module.PulseScreen };
});

type PulseRouteSearch = {
profile?: string;
profileView?: "memories" | "channels";
};

function validatePulseSearch(
search: Record<string, unknown>,
): PulseRouteSearch {
return {
profile:
typeof search.profile === "string" && search.profile.length > 0
? search.profile
: undefined,
profileView:
search.profileView === "memories" || search.profileView === "channels"
? search.profileView
: undefined,
};
}

export const Route = createFileRoute("/pulse")({
validateSearch: validatePulseSearch,
component: PulseRouteComponent,
});

Expand Down
31 changes: 31 additions & 0 deletions desktop/src/app/routes/settings.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { createFileRoute } from "@tanstack/react-router";

import {
type SettingsSection,
isSettingsSection,
} from "@/features/settings/ui/SettingsPanels";

type SettingsRouteSearch = {
section?: SettingsSection;
};

function validateSettingsSearch(
search: Record<string, unknown>,
): SettingsRouteSearch {
return {
section: isSettingsSection(search.section) ? search.section : undefined,
};
}

export const Route = createFileRoute("/settings")({
validateSearch: validateSettingsSearch,
component: SettingsRouteComponent,
});

// Settings renders at the AppShell level (it replaces the sidebar, top
// chrome, and router outlet wholesale), keyed off this route's presence —
// see AppShell. The outlet is unmounted while settings is open, so this
// component never actually renders.
function SettingsRouteComponent() {
return null;
}
Loading