Q184: the page, its sections, filters and selections survive page switches and restarts

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 5.5 committed 2026-10-09 07:46:21 -04:00
1 parent 2790d88186
commit 72cf57ce3a
27 files changed
+235 -38

No files matched your search

+9 -2
View File
@@ -1,3 +1,4 @@
import { oneOf, readViewState, seedViewState, writeViewState } from "../shared/view-state";
import { LeaveGuardContext } from "./leave-guard";
import { t } from "../i18n/i18n";
import { Button } from "../shared/ui";
@@ -6,7 +7,8 @@ import { ErrorBoundary } from "../shared/ErrorBoundary";
import { StartupScreen } from "./StartupScreen";
import type { Focus } from "../pages/types";
import { useEffect, useMemo, useState } from "react";
import { Shell, type RouteId } from "../shared/Shell";
import { Shell } from "../shared/Shell";
import { ROUTE_IDS, type RouteId } from "../shared/routes";
import { AvatarDirectoryContext, type AvatarLook } from "../shared/ui";
import { Onboarding } from "./Onboarding";
import { useSnapshot } from "./useSnapshot";
@@ -22,9 +24,11 @@ import { RunsPage } from "../pages/RunsPage";
import { SettingsPage } from "../pages/SettingsPage";
import { OfficePage } from "../pages/OfficePage";
const isRoute = oneOf(ROUTE_IDS);
export function App() {
const { snapshot, syncFailure, actions } = useSnapshot();
const [route, setRoute] = useState<RouteId>("home");
const [routeChoice, setRoute] = useState<RouteId | null>(null);
const [focus, setFocus] = useState<Focus | null>(null);
// Leaving a page with input that would be lost asks first.
const [leaving, setLeaving] = useState<{ proceed: () => void } | null>(null);
@@ -35,6 +39,7 @@ export function App() {
// Opening a page for an object remembers it; plain navigation forgets it.
const navigate = (next: RouteId, nextFocus?: Focus) => requestLeave(() => {
setRoute(next);
writeViewState("route", next);
setFocus(nextFocus ?? null);
});
const avatarDirectory = useMemo(() => {
@@ -76,6 +81,8 @@ export function App() {
// A failed manual retry is already recorded in syncFailure, which the screen shows.
return <StartupScreen failure={syncFailure} onRetry={() => void actions.reload().catch(() => undefined)} />;
}
seedViewState(snapshot.viewState);
const route = routeChoice ?? readViewState("route", isRoute) ?? "home";
// A company exists from Launch on, but its setup is unfinished until the connectors step is done; that is stored, so a restart resumes it.
const unfinished = snapshot.organizations.find((organization) => organization.setupCompletedAt === null) ?? null;
if (snapshot.organizations.length === 0 || creatingOrg || unfinished) {
+3 -1
View File
@@ -1,3 +1,4 @@
import { oneOf, usePersistentState } from "../shared/view-state";
import { approvalKindLabel } from "../i18n/labels";
import { decideEach, type BatchResult } from "./approvals/decide-each";
import { DECIDED_PAGE, decidedWindow } from "./approvals/decided-window";
@@ -33,8 +34,9 @@ const RISK_FILTERS: ReadonlyArray<{ id: RiskFilter; label: string }> = [
export function ApprovalsPage({ snapshot, actions, navigate, focus }: PageProps) {
const orgId = activeOrgId(snapshot);
const employee = requireActiveEmployee(snapshot);
const [filter, setFilter] = useState<RiskFilter>("all");
const focusedApproval = focus?.kind === "approval" ? focus.id : null;
// A linked request is never hidden by a filter left from before.
const [filter, setFilter] = usePersistentState<RiskFilter>(`${orgId}:approvals.filter`, "all", oneOf(["all", "low", "medium", "high"] as const), focusedApproval ? "all" : undefined);
const [editingId, setEditingId] = useState<string | null>(
focusedApproval && snapshot.approvals.some((item) => item.id === focusedApproval && item.state === "pending") ? focusedApproval : null,
);
+2 -1
View File
@@ -1,3 +1,4 @@
import { isIdOrNull, usePersistentState } from "../shared/view-state";
import { useLeaveGuard } from "../app/leave-guard";
import { setDrafts, useDrafts } from "./chat/draft-store";
import { useCallback, useMemo, useRef, useState, type FormEvent } from "react";
@@ -66,7 +67,7 @@ export function ChatPage({ snapshot, actions }: PageProps) {
}),
[spaces, snapshot.chatMemberships, messagesAll, snapshot.chatReadStates, employees, agents, employee.id, ceoAgentId],
);
const [selectedSpaceId, setSelectedSpaceId] = useState<string | null>(null);
const [selectedSpaceId, setSelectedSpaceId] = usePersistentState<string | null>(`${orgId}:chat.space`, null, isIdOrNull);
const [createOpen, setCreateOpen] = useState(false);
const requestLeave = useLeaveGuard();
// Text and attachment are kept per conversation, outside the page, so switching conversations or pages loses neither.
+12 -4
View File
@@ -1,4 +1,5 @@
import { useMemo, useState } from "react";
import { oneOf, usePersistentState } from "../shared/view-state";
import { useMemo } from "react";
import { Button, Icon, Segmented } from "../shared/ui";
import { TopbarActions } from "../shared/TopbarActions";
import { FILE_SCAN_STATE, type FileObjectRecord, type FileScanState } from "../../../shared/domain";
@@ -20,6 +21,13 @@ import {
type ViewMode,
} from "./files/file-helpers";
const isSection = (value: unknown): value is Section => {
if (typeof value !== "object" || value === null) return false;
const section = value as { kind?: unknown; value?: unknown };
if (section.kind === "all" || section.kind === "recent" || section.kind === "pinned") return true;
return (section.kind === "department" || section.kind === "agent") && typeof section.value === "string";
};
export function FilesPage({ snapshot, actions }: PageProps) {
const orgId = activeOrgId(snapshot);
const employees = snapshot.employees.filter((employee) => employee.orgId === orgId);
@@ -30,9 +38,9 @@ export function FilesPage({ snapshot, actions }: PageProps) {
() => snapshot.fileAccessGrants.filter((grant) => grant.orgId === orgId),
[snapshot.fileAccessGrants, orgId],
);
const [section, setSection] = useState<Section>({ kind: "all" });
const [view, setView] = useState<ViewMode>("list");
const [scanFilter, setScanFilter] = useState<FileScanState | "all">("all");
const [section, setSection] = usePersistentState<Section>(`${orgId}:files.section`, { kind: "all" }, isSection);
const [view, setView] = usePersistentState<ViewMode>(`${orgId}:files.view`, "list", oneOf(["grid", "list"] as const));
const [scanFilter, setScanFilter] = usePersistentState<FileScanState | "all">(`${orgId}:files.scan`, "all", (value): value is FileScanState | "all" => typeof value === "string");
const departments = collectDepartments(files, agents);
const filesByAgent = collectAgentFiles(files, agents);
+3 -2
View File
@@ -1,4 +1,5 @@
import { useMemo, useState } from "react";
import { oneOf, usePersistentState } from "../shared/view-state";
import { useMemo } from "react";
import { DocumentsPanel } from "./library/DocumentsPanel";
import { KnowledgePanel } from "./library/KnowledgePanel";
import type { PageProps } from "./types";
@@ -15,7 +16,7 @@ const TABS: Array<{ id: LibraryTab; label: string }> = [
export function LibraryPage({ snapshot, actions }: PageProps) {
const orgId = activeOrgId(snapshot);
const employee = requireActiveEmployee(snapshot);
const [tab, setTab] = useState<LibraryTab>("documents");
const [tab, setTab] = usePersistentState<LibraryTab>(`${orgId}:library.tab`, "documents", oneOf(["documents", "knowledge"] as const));
const documents = useMemo(() => inOrg(snapshot.documents, orgId), [snapshot.documents, orgId]);
const entries = useMemo(() => inOrg(snapshot.knowledgeEntries, orgId), [snapshot.knowledgeEntries, orgId]);
const tasks = useMemo(() => inOrg(snapshot.tasks, orgId), [snapshot.tasks, orgId]);
+6 -3
View File
@@ -1,3 +1,4 @@
import { isIdOrNull, oneOf, usePersistentState } from "../shared/view-state";
import { requestStateLabel, runTriggerLabel } from "../i18n/labels";
import { RunRequestList } from "./runs/RunRequestList";
import { useRunRequests } from "./runs/use-run-requests";
@@ -8,7 +9,8 @@ import type { PageProps } from "./types";
import { activeOrgId, formatRelative } from "./types";
import { t } from "../i18n/i18n";
type StatusFilter = "all" | "queued" | "running" | "completed" | "failed";
const STATUS_FILTERS = ["all", "queued", "running", "completed", "failed"] as const;
type StatusFilter = (typeof STATUS_FILTERS)[number];
const FILTERS: Array<{ id: StatusFilter; label: string }> = [
{ id: "all", label: t("All") },
{ id: "queued", label: t("Queued") },
@@ -61,8 +63,9 @@ export function RunsPage({ snapshot, actions, navigate, focus }: PageProps) {
const tasks = useMemo(() => snapshot.tasks.filter((t) => t.orgId === orgId), [snapshot.tasks, orgId]);
const agents = useMemo(() => snapshot.agents.filter((a) => a.orgId === orgId), [snapshot.agents, orgId]);
const providers = useMemo(() => snapshot.providers.filter((p) => p.orgId === orgId), [snapshot.providers, orgId]);
const [filter, setFilter] = useState<StatusFilter>("all");
const [selectedId, setSelectedId] = useState<string | null>(focus?.kind === "run" ? focus.id : null);
// A linked run is never hidden by a filter left from before.
const [filter, setFilter] = usePersistentState<StatusFilter>(`${orgId}:runs.filter`, "all", oneOf(STATUS_FILTERS), focus?.kind === "run" ? "all" : undefined);
const [selectedId, setSelectedId] = usePersistentState<string | null>(`${orgId}:runs.selected`, null, isIdOrNull, focus?.kind === "run" ? focus.id : undefined);
const [search, setSearch] = useState("");
const filtered = useMemo(() => {
+6 -3
View File
@@ -1,5 +1,6 @@
import { oneOf, usePersistentState } from "../shared/view-state";
import { useLeaveGuard } from "../app/leave-guard";
import { useMemo, useState } from "react";
import { useMemo } from "react";
import { ProvidersPanel } from "./settings/ProvidersPanel";
import { WorkspacePanel } from "./settings/WorkspacePanel";
import { AdvancedPanel } from "./settings/AdvancedPanel";
@@ -8,7 +9,8 @@ import { t } from "../i18n/i18n";
import type { PageProps } from "./types";
import { activeOrgId, requireActiveEmployee, requireActiveOrganization, requireUsageStats } from "./types";
type SettingsTab = "general" | "providers" | "integrations" | "advanced";
const SETTINGS_TABS = ["general", "providers", "integrations", "advanced"] as const;
type SettingsTab = (typeof SETTINGS_TABS)[number];
interface NavSpec {
id: SettingsTab;
@@ -26,7 +28,8 @@ export function SettingsPage({ snapshot, actions, focus }: PageProps) {
const orgId = activeOrgId(snapshot);
const organization = requireActiveOrganization(snapshot);
const employee = requireActiveEmployee(snapshot);
const [tab, setTab] = useState<SettingsTab>("general");
// A workspace link opens General, where workspaces are, whatever section was left open.
const [tab, setTab] = usePersistentState<SettingsTab>(`${orgId}:settings.tab`, "general", oneOf(SETTINGS_TABS), focus?.kind === "workspace" ? "general" : undefined);
const requestLeave = useLeaveGuard();
const agents = useMemo(() => snapshot.agents.filter((item) => item.orgId === orgId), [snapshot.agents, orgId]);
+2 -1
View File
@@ -1,3 +1,4 @@
import { oneOf, usePersistentState } from "../shared/view-state";
import { agentModel } from "../../../shared/agent-model";
import { useMemo, useState, type FormEvent } from "react";
import { Avatar, Button, Field, Icon, IconButton } from "../shared/ui";
@@ -24,7 +25,7 @@ export function TeamPage({ snapshot, actions }: PageProps) {
const [hireProviderId, setHireProviderId] = useState("");
const [hireModelId, setHireModelId] = useState("");
const [editAgentId, setEditAgentId] = useState<string | null>(null);
const [filter, setFilter] = useState<"all" | "human" | "agent">("all");
const [filter, setFilter] = usePersistentState<"all" | "human" | "agent">(`${orgId}:team.filter`, "all", oneOf(["all", "human", "agent"] as const));
const employees = useMemo(() => snapshot.employees.filter((item) => item.orgId === orgId), [snapshot.employees, orgId]);
const agents = useMemo(() => snapshot.agents.filter((item) => item.orgId === orgId), [snapshot.agents, orgId]);
+9 -5
View File
@@ -1,3 +1,4 @@
import { oneOf, usePersistentState } from "../shared/view-state";
import { useMemo, useState } from "react";
import { Avatar, Button, Icon, IconButton, Pill, Segmented } from "../shared/ui";
import { MenuDivider, MenuLabel, MenuRow, Popover } from "../shared/Popover";
@@ -25,7 +26,10 @@ import type { PageProps } from "./types";
import { activeOrgId, requireActiveOrganization, type Focus } from "./types";
import { t } from "../i18n/i18n";
type View = "board" | "list" | "progress";
const VIEWS = ["board", "list", "progress"] as const;
type View = (typeof VIEWS)[number];
const isScope = (value: unknown): value is Focus | null =>
value === null || (typeof value === "object" && (["goal", "project"] as unknown[]).includes((value as Focus).kind) && typeof (value as Focus).id === "string");
const COLUMNS: ReadonlyArray<{ id: TaskStatus; label: string; tone: string }> = [
{ id: TASK_STATUS.BACKLOG, label: t("Backlog"), tone: "var(--text-4)" },
@@ -42,14 +46,14 @@ const VIEW_OPTIONS: ReadonlyArray<{ id: View; label: string }> = [
export function WorkPage({ snapshot, actions, navigate, focus }: PageProps) {
const orgId = activeOrgId(snapshot);
const [view, setView] = useState<View>("board");
const [view, setView] = usePersistentState<View>(`${orgId}:work.view`, "board", oneOf(VIEWS));
const [newTaskOpen, setNewTaskOpen] = useState(false);
const [newGoalOpen, setNewGoalOpen] = useState(false);
const [newProjectOpen, setNewProjectOpen] = useState(false);
const [priorityFilter, setPriorityFilter] = useState<TaskPriority | "all">("all");
const [assigneeFilter, setAssigneeFilter] = useState<string>("all");
const [priorityFilter, setPriorityFilter] = usePersistentState<TaskPriority | "all">(`${orgId}:work.priority`, "all", oneOf(["all", "low", "normal", "high"] as const));
const [assigneeFilter, setAssigneeFilter] = usePersistentState<string>(`${orgId}:work.assignee`, "all", (value): value is string => typeof value === "string");
const [detailTaskId, setDetailTaskId] = useState<string | null>(focus?.kind === "task" ? focus.id : null);
const [scope, setScope] = useState<Focus | null>(focus && (focus.kind === "goal" || focus.kind === "project") ? focus : null);
const [scope, setScope] = usePersistentState<Focus | null>(`${orgId}:work.scope`, null, isScope, focus && (focus.kind === "goal" || focus.kind === "project") ? focus : undefined);
const [actionError, setActionError] = useState<string | null>(null);
// The board, its counts and its filters show real work; chat replies and goal checkpoints show on Messages, Home and Runs.
@@ -1,3 +1,4 @@
import { isIdOrNull, usePersistentState } from "../../shared/view-state";
import { DOCUMENT_STATUS_VALUES, documentStatusLabel } from "../../i18n/labels";
import { useEffect, useState, type FormEvent } from "react";
import type { DocumentRecord, TaskRecord } from "../../../../shared/domain";
@@ -21,7 +22,7 @@ export function DocumentsPanel({
tasks: TaskRecord[];
actions: SnapshotActions;
}) {
const [selectedId, setSelectedId] = useState<string | null>(null);
const [selectedId, setSelectedId] = usePersistentState<string | null>(`${orgId}:library.document`, null, isIdOrNull);
const selected = documents.find((doc) => doc.id === selectedId) ?? null;
const [loaded, setLoaded] = useState<{ documentId: string; versions: DocumentVersionRecord[] } | null>(null);
const [shownVersion, setShownVersion] = useState<number | null>(null);
@@ -1,3 +1,4 @@
import { isIdOrNull, oneOf, usePersistentState } from "../../shared/view-state";
import { useEffect, useState, type FormEvent } from "react";
import type { AgentRecord, GoalRecord, KnowledgeEntryRecord, ProjectRecord, TaskRecord } from "../../../../shared/domain";
import type { KnowledgeRevisionRecord, KnowledgeScopeType } from "../../../../shared/commands";
@@ -28,8 +29,8 @@ export function KnowledgePanel({
actions: SnapshotActions;
}) {
const now = new Date().toISOString();
const [filter, setFilter] = useState<EntryState>("active");
const [selectedId, setSelectedId] = useState<string | null>(null);
const [filter, setFilter] = usePersistentState<EntryState>(`${orgId}:library.knowledgeFilter`, "active", oneOf(["active", "superseded", "expired"] as const));
const [selectedId, setSelectedId] = usePersistentState<string | null>(`${orgId}:library.entry`, null, isIdOrNull);
const [loaded, setLoaded] = useState<{ entryId: string; revisions: KnowledgeRevisionRecord[] } | null>(null);
const [dialog, setDialog] = useState<"new" | "revise" | "delete" | null>(null);
const [scopeType, setScopeType] = useState<KnowledgeScopeType>("organization");
+3 -12
View File
@@ -1,3 +1,4 @@
import type { RouteId } from "./routes";
import { shortcutLabel } from "../i18n/labels";
import { isBusinessTask } from "../pages/work/task-helpers";
import { unreadBySpace } from "../pages/chat/chat-helpers";
@@ -12,18 +13,8 @@ import { SearchDialog } from "./SearchDialog";
import { TopbarSlotContext } from "./topbar-context";
import { t } from "../i18n/i18n";
export type RouteId =
| "home"
| "team"
| "work"
| "chat"
| "files"
| "library"
| "runs"
| "approvals"
| "connectors"
| "office"
| "settings";
export type { RouteId };
interface NavSpec {
id: RouteId;
+2
View File
@@ -0,0 +1,2 @@
export const ROUTE_IDS = ["home", "team", "work", "chat", "files", "library", "runs", "approvals", "connectors", "office", "settings"] as const;
export type RouteId = (typeof ROUTE_IDS)[number];
+37
View File
@@ -0,0 +1,37 @@
import { useState } from "react";
/** Where the operator was, by key; seeded once from the first snapshot and written through to the profile. */
let saved: Record<string, unknown> | null = null;
export function seedViewState(initial: Record<string, unknown>): void {
if (saved === null) saved = { ...initial };
}
export function readViewState<T>(key: string, isValid: (value: unknown) => value is T): T | undefined {
if (saved === null) throw new Error("View state was read before the first snapshot.");
const value = saved[key];
return isValid(value) ? value : undefined;
}
export function writeViewState(key: string, value: unknown): void {
if (saved === null) throw new Error("View state was written before the first snapshot.");
saved[key] = value;
void window.agentCompany.saveViewState({ key, value }).then((result) => {
if (!result.ok) console.error(`[view-state] ${key} was not remembered: ${result.error.message}`);
});
}
/** State that survives page switches and restarts; a stored value that no longer fits falls back. `override` wins when a page is opened for an object. */
export function usePersistentState<T>(key: string, fallback: T, isValid: (value: unknown) => value is T, override?: T): [T, (next: T) => void] {
const [value, setValue] = useState<T>(() => override ?? readViewState(key, isValid) ?? fallback);
const set = (next: T) => {
setValue(next);
writeViewState(key, next);
};
return [value, set];
}
export const oneOf = <T extends string>(values: readonly T[]) => (value: unknown): value is T =>
typeof value === "string" && (values as readonly string[]).includes(value);
export const isIdOrNull = (value: unknown): value is string | null => value === null || typeof value === "string";