Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -600,4 +600,18 @@ for (const key of ["home.firstGoal.title", "home.firstGoal.lead", "home.firstGoa
assert.equal((i18n.match(new RegExp(`"${key.replace(/\./g, "\\.")}":`, "g")) ?? []).length, 2, `${key} exists in both locales`);
}

// Load boundaries: settings are a separate chunk that arrives on demand and
// the secondary routes are not part of the workspace's first load. Each lazy
// site keeps a visible loading state. Team results stay eager: their list
// must load exactly once when a configured team appears, and a lazy mount
// shifted that timing (see the loopx-mode scenario).
const router = source("../../router.tsx");
assert.match(page, /const WorkspaceSettingsPage = lazy\(\(\) => import\("\.\/workspace-settings-page"\)/, "Settings load on demand");
assert.doesNotMatch(page, /^import \{ WorkspaceSettingsPage \}/m, "No eager import undoes the boundary");
assert.match(page, /^import \{ GoalTeamResults \} from "\.\/goal-team-results";/m, "Team results load eagerly so their single list request keeps its timing");
for (const route of ["./views/frontstage-developer-page", "./views/benchmark-study-page", "./features/personal-workspace/answer-report-page"]) {
assert.match(router, new RegExp(`lazy\\(\\(\\) => import\\("${route.replace(/[./]/g, (ch) => `\\${ch}`)}"\\)`), `${route} loads on demand`);
}
assert.equal((page.match(/<Suspense fallback=\{<p className="personal-lazy-loading" role="status">/g) ?? []).length, 1, "The lazy settings site shows a loading status");

console.log("personal workspace drawer contract smoke passed");
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
} from "../../../../../../loopx/control_plane/presentation/action_review_plan.js";
import { refreshAttention } from "./attention-details";
import { teamPlanAssignments, teamPlanAppliedLine, teamPlanAppliedOutcome, teamPlanFields, teamPlanGoalId, teamPlanLaneCount, teamPlanReceiptGapLanes, teamPlanTodoIds } from "./team-plan-preview";
import { useEffect, useMemo, useRef, useState, type ClipboardEvent as ReactClipboardEvent, type ReactNode } from "react";
import { Suspense, lazy, useEffect, useMemo, useRef, useState, type ClipboardEvent as ReactClipboardEvent, type ReactNode } from "react";
import { AlertCircle, Bot, CalendarClock, FileText, ListPlus, MessageCircleQuestion, Paperclip, Plus, RefreshCw, Send, X } from "lucide-react";

import {
Expand Down Expand Up @@ -84,7 +84,9 @@ import { goalWorkKind } from "./goal-activity";
import { WorkspaceActionForm, type WorkspaceActionDraft } from "./workspace-action-form";
import { GoalActivityChip, GoalIdentityMark } from "./goal-activity-view";
import { ManagerBrief } from "./manager-brief";
import { WorkspaceSettingsPage } from "./workspace-settings-page";
// Loaded on demand: settings and their editors are a separate chunk that
// arrives when the owner first opens Settings, not on every workspace visit.
const WorkspaceSettingsPage = lazy(() => import("./workspace-settings-page").then((module) => ({ default: module.WorkspaceSettingsPage })));
import { UsageStatisticsNotice } from "./usage-statistics-notice";
import { readWorkspaceTheme, writeWorkspaceTheme, type WorkspaceTheme } from "./workspace-theme";
import { compareProposalRecency } from "./proposal-recency";
Expand Down Expand Up @@ -2091,6 +2093,7 @@ export function PersonalWorkspacePage({
}

const settingsPage = settingsOpen ? (
<Suspense fallback={<p className="personal-lazy-loading" role="status">{t("common.loading")}</p>}>
<WorkspaceSettingsPage
callbacks={effectiveDrawerCallbacks}
focusGoalConnection={Boolean(selection?.kind === "settings" && selection.goalId)}
Expand All @@ -2103,6 +2106,7 @@ export function PersonalWorkspacePage({
onThemeChange={updateTheme}
theme={theme}
/>
</Suspense>
) : null;

const composerSuggestions = !selectedWorkspaceRef && (!conversationOpen || !conversationMessages.length) ? <details className="personal-composer-tools" key={conversationKey} onKeyDown={(event) => {
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

22 changes: 15 additions & 7 deletions apps/presentation/dashboard/src/router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,19 @@ import {
import { z } from "zod";

import { DashboardPage } from "./views/dashboard-page";
import { FrontstageDeveloperPage } from "./views/frontstage-developer-page";
import { useEffect } from "react";
import { Suspense, lazy, useEffect } from "react";
import { resolveLocalStatusUrl } from "./data/local-status-query";
import { BenchmarkStudyPage } from "./views/benchmark-study-page";
import { AnswerReportPage } from "./features/personal-workspace/answer-report-page";

// Secondary routes load on demand. The workspace is what every visit opens;
// contributor projections, benchmark studies and answer reports arrive as
// their own chunks only when such a route is entered.
const FrontstageDeveloperPage = lazy(() => import("./views/frontstage-developer-page").then((module) => ({ default: module.FrontstageDeveloperPage })));
const BenchmarkStudyPage = lazy(() => import("./views/benchmark-study-page").then((module) => ({ default: module.BenchmarkStudyPage })));
const AnswerReportPage = lazy(() => import("./features/personal-workspace/answer-report-page").then((module) => ({ default: module.AnswerReportPage })));

function RouteLoading() {
return <main className="p-8" role="status">加载中… / Loading…</main>;
}

const searchSchema = z.object({
goalId: z.string().optional().default(""),
Expand Down Expand Up @@ -84,7 +92,7 @@ export const dashboardRoute = createRoute({
component: () => {
const search = dashboardRoute.useSearch();
return search.reportSessionId && search.reportMessageId
? <AnswerReportPage sessionId={search.reportSessionId} messageId={search.reportMessageId} statusUrl={search.statusUrl}/>
? <Suspense fallback={<RouteLoading />}><AnswerReportPage sessionId={search.reportSessionId} messageId={search.reportMessageId} statusUrl={search.statusUrl}/></Suspense>
: <DashboardPage/>;
},
});
Expand Down Expand Up @@ -112,14 +120,14 @@ export const frontstageDeveloperRoute = createRoute({
export const projectionDeveloperRoute = createRoute({
getParentRoute: () => rootRoute,
path: "/developers/projections",
component: FrontstageDeveloperPage,
component: () => <Suspense fallback={<RouteLoading />}><FrontstageDeveloperPage /></Suspense>,
});

export const benchmarkStudyRoute = createRoute({
getParentRoute: () => rootRoute,
path: "/benchmarks/study",
validateSearch: (search) => benchmarkStudySearchSchema.parse(search),
component: BenchmarkStudyPage,
component: () => <Suspense fallback={<RouteLoading />}><BenchmarkStudyPage /></Suspense>,
});

const routeTree = rootRoute.addChildren([
Expand Down
12 changes: 12 additions & 0 deletions docs/development/frontend-delivery.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,18 @@ can require a rebuild. Text inputs normalize CRLF so a CI-built bundle remains
valid on Windows; delivered bytes are always hashed exactly. This is integrity
and freshness metadata, not a signature or a replacement for release attestations.

### Load boundaries

The workspace route is what every visit opens, so it loads eagerly. Settings
and its editors, the contributor projection page, the benchmark study page and
the answer report page are dynamic imports: each is its own chunk fetched the
first time it is shown, behind a visible loading status. Keep new secondary
surfaces behind the same kind of boundary rather than adding them to the entry
chunk, and keep a surface eager when a scenario pins the timing of its first
read (team results must list exactly once when a configured team appears). The bundle manifest records every chunk,
and the one-upgrade asset retention already covers an old tab importing a
deferred chunk after a delivery.

### Goal sidebar order

Goal dragging and keyboard move controls save presentation order through
Expand Down
Loading