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 @@ -10,7 +10,7 @@ await build({ configFile: false, logLevel: "silent", build: {
lib: { entry: resolve("src/data/workspace-progressive-status.ts"), formats: ["es"], fileName: () => "loader.mjs" },
rolldownOptions: { external: ["zod"] },
} });
const { loadWorkspaceGoalSnapshots, directoryStatusPayload, reusableGoalSnapshots, workspaceReadPlan } = await import(pathToFileURL(resolve(outDir, "loader.mjs")).href);
const { loadWorkspaceGoalSnapshots, directoryStatusPayload, reusableGoalSnapshots, scopeWorkspaceDirectory, workspaceReadPlan } = await import(pathToFileURL(resolve(outDir, "loader.mjs")).href);
const directory = { ok: true, schema_version: "loopx_workspace_directory_v1", registry_revision: "r1",
goals: [{ id: "alpha", display_name: "Alpha", activation_state: "active", registry_member: true }] };

Expand Down Expand Up @@ -53,6 +53,21 @@ assert.deepEqual(partial.requestedDirectory.goals.map((goal) => goal.id), ["beta
assert.deepEqual(workspaceReadPlan(null, earlier.directory, "missing").snapshots, {},
"a different source cannot retain the previous source's data");
const retentionChecks = 12;
const related = (id, parent_goal_id) => ({ ...entry(id), parent_goal_id });
const forest = { ...directory, goals: [entry("parent"), related("child", "parent"),
related("grandchild", "child"), entry("daily"), related("orphan", "missing"),
related("cycle-a", "cycle-b"), related("cycle-b", "cycle-a")] };
const subtree = scopeWorkspaceDirectory(forest, "parent");
assert.deepEqual(subtree.goals.map(g => g.id), ["parent", "child", "grandchild"],
"the read frontier uses the same actual recursive hierarchy as the view");
assert.equal(subtree.registry_revision, forest.registry_revision, "the original source revision remains authoritative");
assert.equal(scopeWorkspaceDirectory(forest), forest, "unscoped reading preserves the existing directory exactly");
assert.deepEqual(scopeWorkspaceDirectory(forest, "missing").goals, [], "a missing root cannot expose an unrelated tree");
assert.deepEqual(scopeWorkspaceDirectory(forest, "cycle-a").goals.map(g => g.id), ["cycle-a"],
"cyclic organization cannot pull another root into a subtree");
const scopedRefresh = workspaceReadPlan({ directory: forest, snapshots: { daily: { private: true } } }, subtree);
assert.deepEqual(scopedRefresh.snapshots, {}, "another subtree's retained state leaves the read model");
assert.deepEqual(scopedRefresh.requestedDirectory.goals.map(g => g.id), ["parent", "child", "grandchild"]);
const access = { error_code: "workspace_status_access_denied" };
const original = { fetch, setTimeout, clearTimeout };
const deadline = {};
Expand Down Expand Up @@ -87,6 +102,19 @@ async function check(name, respond, expected, attempts, verify = () => {}) {
}
const json = (body, status = 500) => Response.json(body, { status });
try {
const readGoals = [];
globalThis.fetch = async (url) => {
const id = new URL(url).searchParams.get("goal_id");
readGoals.push(id);
return json({ ...directoryStatusPayload({ ...subtree, goals: subtree.goals.filter(g => g.id === id) }),
workspace_registry_revision: subtree.registry_revision }, 200);
};
const loaded = [];
await loadWorkspaceGoalSnapshots("http://status/status.json", "http://status", scopedRefresh.requestedDirectory,
(id, _payload, error) => { assert.equal(error, null); loaded.push(id); }, () => true, () => "daily");
assert.deepEqual(readGoals.sort(), ["child", "grandchild", "parent"],
"even an out-of-scope preferred Goal must not broaden actual state reads");
assert.equal(loaded.length, 3);
await check("access", () => json(access), ["access"], 1);
for (const body of [null, {}, [], { error_code: "future_code" }, { error_code: 5 }, { error: "permission denied" }]) {
await check("unknown envelope", () => json(body), ["service"], 3);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { z } from "zod";
import { parseStatusPayload, type StatusPayload } from "./status";
import { goalSubtree } from "../features/personal-workspace/goal-hierarchy";

/** The source facts a discovery read can state without opening runtime state; older services omit it. */
const directoryContractSchema = z.object({
Expand Down Expand Up @@ -31,6 +32,20 @@ export type WorkspaceProgress = {
errors: Record<string, WorkspaceLoadError>;
};

/** Narrow only this source's discovered Goals before reading their state.
* Organization is not an access grant: an absent root stays absent, and the
* existing source membership/revision checks still validate every response. */
export function scopeWorkspaceDirectory(
directory: WorkspaceDirectory,
rootGoalId?: string | null,
): WorkspaceDirectory {
if (!rootGoalId) return directory;
const included = new Set(goalSubtree(directory.goals.map(goal => ({
goalId: goal.id, parentGoalId: goal.parent_goal_id,
})), rootGoalId).map(goal => goal.goalId));
return { ...directory, goals: directory.goals.filter(goal => included.has(goal.id)) };
}

/**
* Snapshots a same-source read may keep visible while it reconciles.
*
Expand Down
49 changes: 36 additions & 13 deletions apps/presentation/dashboard/src/views/dashboard-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {compactWorkspaceText as compactShareText, workspaceAgentTodoFromItem} fr
import type { GoalAcceptanceObservation } from "../data/goal-acceptance-observation";
import { attentionDetails, attentionDetailsFromSnapshot, sourceAttention } from "../features/personal-workspace/attention-details";
import type { AttentionDetails } from "../features/personal-workspace/attention-details";
import { directoryStatusPayload, fetchWorkspaceDirectory, loadWorkspaceGoalSnapshots, workspaceReadPlan, type WorkspaceProgress, type WorkspaceLoadError, type WorkspaceReadScope } from "../data/workspace-progressive-status";
import { directoryStatusPayload, fetchWorkspaceDirectory, loadWorkspaceGoalSnapshots, scopeWorkspaceDirectory, workspaceReadPlan, type WorkspaceProgress, type WorkspaceLoadError, type WorkspaceReadScope } from "../data/workspace-progressive-status";
import { useEffect, useMemo, useRef, useState } from "react";
import { CircleAlert, Moon, RefreshCw, Sun } from "lucide-react";
import type { DigestAttentionSummary } from "../../../../../loopx/control_plane/presentation/goal_workspace_digest.js";
Expand Down Expand Up @@ -3260,6 +3260,9 @@ export function DashboardPage() {
const progressiveAbortRef = useRef<AbortController | null>(null);
const preferredGoalRef = useRef(search.goalId);
preferredGoalRef.current = search.goalId;
const rootGoalRef = useRef(search.rootGoalId);
rootGoalRef.current = search.rootGoalId;
const requestedRootRef = useRef<string | undefined>(undefined);
const [payload, setPayload] = useState<StatusPayload>(exampleStatusPayload);
const [source, setSource] = useState<DataSource>({ kind: "example", label: "bundled example" });
const [statusSourceCatalog, setStatusSourceCatalog] = useState(() => {
Expand Down Expand Up @@ -3416,6 +3419,10 @@ export function DashboardPage() {
selectionRevision: options.selectionRevision,
});
if (!request) return;
const requestRoot = rootGoalRef.current;
requestedRootRef.current = requestRoot;
const canCommit = () => rootGoalRef.current === requestRoot
&& statusRequestCanCommit(statusRequestFenceRef.current, request);
progressiveAbortRef.current?.abort();
const progressiveAbort = new AbortController();
progressiveAbortRef.current = progressiveAbort;
Expand All @@ -3428,9 +3435,12 @@ export function DashboardPage() {
setGoalArchiveLoadState({ error: null, phase: "idle" });
}
try {
const directory = await fetchWorkspaceDirectory(trimmed, window.location.href).catch(() => null);
if (!statusRequestCanCommit(statusRequestFenceRef.current, request)) return;
if (directory) {
const discovered = await fetchWorkspaceDirectory(trimmed, window.location.href).catch(() => null);
if (!canCommit()) return;
if (discovered) {
// Root views must not open unrelated Goals just to hide them later.
// Keep discovery metadata and the source's original revision fence.
const directory = scopeWorkspaceDirectory(discovered, requestRoot);
// Keep valid snapshots on screen during a refresh. Only an explicit
// partial read may skip them; lifecycle identity is not data freshness.
const { snapshots, requestedDirectory } = workspaceReadPlan(
Expand All @@ -3445,34 +3455,35 @@ export function DashboardPage() {
setGoalArchiveLoadState({ error: null, phase: "loading" });
await loadWorkspaceGoalSnapshots(trimmed, window.location.href, requestedDirectory,
(id, snapshot, error) => {
if (!canCommit()) return;
if (error === "revision") directoryChanged = true;
setProgress((current) => {
if (!current) return current;
if (!canCommit() || !current) return current;
const snapshots = { ...current.snapshots };
const errors = { ...current.errors };
if (snapshot) { snapshots[id] = snapshot; delete errors[id]; }
else if (error) { delete snapshots[id]; errors[id] = error; }
return { ...current, snapshots, errors };
});
},
() => statusRequestCanCommit(statusRequestFenceRef.current, request),
canCommit,
() => preferredGoalRef.current,
progressiveAbort.signal,
);
if (directoryChanged && (options.resyncAttempt ?? 0) < 1
&& statusRequestCanCommit(statusRequestFenceRef.current, request)) {
&& canCommit()) {
await loadFromUrl(trimmed, { resyncAttempt: 1 });
return;
}
if (statusRequestCanCommit(statusRequestFenceRef.current, request)) {
if (canCommit()) {
setGoalArchiveLoadState({ error: null, phase: "ready" });
}
return;
}
const nextPayload = await fetchStatusPayload(
scopedStatusUrl(trimmed, "active", window.location.href),
);
if (!statusRequestCanCommit(statusRequestFenceRef.current, request)) return;
if (!canCommit()) return;
setProgress(null);
request.registryRevision = nextPayload.goal_projection?.registry_revision ?? null;
if (!await commitLoadedStatus(trimmed, nextPayload, request)) return;
Expand All @@ -3483,13 +3494,13 @@ export function DashboardPage() {
}
loadGoalArchive(trimmed, request, options.resyncAttempt ?? 0);
} catch (error) {
if (!statusRequestIsCurrent(statusRequestFenceRef.current, request)) return;
if (!canCommit()) return;
if (!background) setLoadError(formatStatusError(error));
// A confirmed action keeps its receipt while its caller reports that
// the independent status read needs retrying. Passive refreshes stay quiet.
if (options.rejectOnError) throw error;
} finally {
if (!background && statusRequestIsCurrent(statusRequestFenceRef.current, request)) {
if (!background && canCommit()) {
setIsLoading(false);
}
}
Expand Down Expand Up @@ -3570,6 +3581,17 @@ export function DashboardPage() {
};

useEffect(() => {
if (requestedRootRef.current !== search.rootGoalId && !exampleModeRequested) {
// Re-scope an in-flight source selection as well as a loaded source.
// Older root responses cannot populate the newly selected subtree.
// A history entry restores source and root together, so the route's
// restored source outranks the source that is still loaded. Only when
// the route names no source does the loaded source act as the fallback.
const target = requestedStatusUrl
?? (search.statusUrl.trim() || (source.kind === "url" ? source.label : ""));
void loadFromUrl(target || defaultGlobalStatusUrl);
return;
}
const trimmedStatusUrl = search.statusUrl.trim();
if (trimmedStatusUrl) {
if (suppressedStatusUrlRef.current === trimmedStatusUrl) {
Expand All @@ -3595,9 +3617,10 @@ export function DashboardPage() {
if (source.kind === "example") {
void loadFromUrl(defaultGlobalStatusUrl);
}
}, [exampleModeRequested, requestedStatusUrl, search.statusUrl, source.kind, source.label]);
}, [exampleModeRequested, requestedStatusUrl, search.statusUrl, search.rootGoalId, source.kind, source.label]);

useEffect(() => {
if (isLoading && search.rootGoalId) return;
if (search.statusUrl && source.kind === "example") {
return;
}
Expand All @@ -3622,7 +3645,7 @@ export function DashboardPage() {
}),
});
}
}, [goalArchiveLoadState.phase, goalRows, navigate, search.goalId, search.statusUrl, source.kind]);
}, [goalArchiveLoadState.phase, goalRows, isLoading, navigate, search.goalId, search.rootGoalId, search.statusUrl, source.kind]);

useEffect(() => {
if (!progress || isLoading || !search.goalId || source.kind !== "url") return;
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading