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
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,14 @@ async function verifyProjectionFetchContract() {
}
assert(mismatchRejected, "projection identity mismatch must fail closed");

let wrongGoalRejected = false;
try {
await fetchPresentationProjection("http://127.0.0.1:8765/extension-projection", {
...matchingSurface, goal_id: "another-goal",
});
} catch (error) {wrongGoalRejected = String(error).includes("does not match");}
assert(wrongGoalRejected, "matching revision/hash must not authorize another Goal’s result");

let publicHostRejected = false;
requestedUrl = "";
try {
Expand Down
10 changes: 9 additions & 1 deletion apps/presentation/dashboard/src/data/decision-research.ts
Original file line number Diff line number Diff line change
Expand Up @@ -415,7 +415,14 @@ export async function fetchPresentationProjection(
target.searchParams.set("payload_sha256", surface.detail_ref.payload_sha256);
const response = await fetch(target.toString(), { cache: "no-store" });
if (!response.ok) {
throw new Error(`HTTP ${response.status} while loading projection`);
let detail = "";
try {
const failure: unknown = await response.json();
if (typeof failure === "object" && failure !== null && "error" in failure && typeof failure.error === "string") {
detail = `: ${failure.error.slice(0, 300)}`;
}
} catch { /* A malformed error response still clears the previous view. */ }
throw new Error(`HTTP ${response.status} while loading projection${detail}`);
}
const projection = parsePresentationProjection(await response.json());
const expectedIdentity = surface.detail_ref;
Expand All @@ -426,6 +433,7 @@ export async function fetchPresentationProjection(
|| projection.payload_sha256 !== expectedIdentity.payload_sha256
|| projection.view_schema !== surface.view_schema
|| projection.surface_kind !== surface.surface_kind
|| projection.goal_id !== surface.goal_id
) {
throw new Error("projection response does not match the requested detail_ref");
}
Expand Down
7 changes: 7 additions & 0 deletions apps/presentation/dashboard/src/data/local-status-query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,13 @@ export async function fetchPeriodicReportIndex(indexUrl: string, goalId: string)
return periodicReportIndexResponseSchema.parse(await response.json()).periodic_reports;
}

export function presentationApiUrls(payload: StatusPayload, source: ResolvedLocalStatusUrl) {
return {
indexUrl: localApiUrl(source, payload.local_dashboard_api?.presentation_surfaces_url),
detailUrl: localApiUrl(source, payload.local_dashboard_api?.presentation_detail_url),
};
}

export async function fetchPeriodicReportProjection(
detailUrl: string,
ref: PeriodicReportDetailRef,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ li[data-check="blocked"] .goal-team-check-pill { border-color: transparent; back
.goal-team-results p { font-size: 12px; line-height: 1.7; color: var(--pw-muted); }
.goal-team-results [role="alert"] { color: var(--pw-red, #b42318); }
.goal-team-results button, .goal-team-results select { min-height: 44px; padding: 8px 12px; border: 1px solid var(--pw-line, #ebebeb); border-radius: 6px; background: var(--pw-surface, #fff); color: inherit; font: inherit; cursor: pointer; }
.goal-managed-results > header button { flex-shrink: 0; white-space: nowrap; }
.goal-managed-results > header button, .goal-research-results > header button { flex-shrink: 0; white-space: nowrap; }
.goal-team-results button[aria-pressed="true"] { border-color: var(--pw-text); }
.goal-team-results button:disabled { opacity: .5; cursor: default; }
.goal-team-results summary { cursor: pointer; padding: 12px 0; font-size: 12px; min-height: 44px; }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import {useEffect, useState} from "react";
import {RefreshCw} from "lucide-react";
import {fetchPresentationProjection, type DecisionResearchView} from "../../data/decision-research";
import {parsePresentationSurfaceCollectionResponse, type PresentationSurface, type RunGoal} from "../../data/status";
import {DecisionResearchSurface} from "../../views/decision-research-surface";

export type GoalResearchApi = {indexUrl: string; detailUrl: string; goals: RunGoal[]};
type Result = {surface: PresentationSurface; view?: DecisionResearchView; error?: string};

/** The active Goal's published research; the extension index owns availability. */
export function GoalResearchResults({goalId, api, zh}: {goalId: string; api: GoalResearchApi; zh: boolean}) {
const [results, setResults] = useState<Result[]>([]);
const [busy, setBusy] = useState(true);
const [error, setError] = useState("");
const [revision, setRevision] = useState(0);
useEffect(() => {
let current = true;
setResults([]); setError(""); setBusy(true);
void (async () => {
try {
const response = await fetch(api.indexUrl, {cache: "no-store"});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const collection = parsePresentationSurfaceCollectionResponse(await response.json());
const surfaces = collection.items.filter(surface => surface.goal_id === goalId
&& surface.visibility === "public-safe"
&& surface.surface_kind === "decision_research_dashboard"
&& surface.view_schema === "decision_research_dashboard_v0");
const next = await Promise.all(surfaces.map(async (surface): Promise<Result> => {
if (surface.state !== "ready" && surface.state !== "review_due") return {surface};
try {
const projection = await fetchPresentationProjection(api.detailUrl, surface);
return {surface, view: projection.view};
} catch (failure) {return {surface, error: String(failure)};}
}));
if (current) setResults(next);
} catch (failure) {if (current) setError(String(failure));}
finally {if (current) setBusy(false);}
})();
return () => {current = false;};
}, [goalId, api.indexUrl, api.detailUrl, revision]);

return <section className="goal-team-results goal-research-results min-w-0 [overflow-wrap:anywhere]"
aria-label={zh ? "研究成果" : "Research results"} aria-busy={busy}>
<header><div><h3>{zh ? "研究成果" : "Research results"}</h3>
<p>{zh ? "查看本 Goal 已保存的研究结论、反证和待复核事项。" : "Review this Goal’s saved conclusions, counterevidence and open questions."}</p></div>
<button type="button" disabled={busy} onClick={() => {setResults([]); setRevision(value => value + 1);}}>
<RefreshCw size={14} aria-hidden="true"/>{zh ? "刷新" : "Refresh"}
</button></header>
{busy ? <p role="status">{zh ? "正在核验研究结果…" : "Verifying research results…"}</p> : null}
{error ? <p role="alert">{zh ? "无法核验研究结果;旧内容已清除。" : "Cannot verify research results; previous content was cleared."} {error}</p> : null}
{!busy && !error && !results.length ? <p>{zh ? "本 Goal 暂无可读取的公开研究成果。" : "No readable public research results for this Goal yet."}</p> : null}
{results.map(result => <DecisionResearchSurface key={`${result.surface.extension_id}:${result.surface.surface_id}`}
goals={api.goals} surface={result.surface} view={result.view} viewError={result.error}/>) }
</section>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import { ChannelHeader } from "./channel-header";
import { GoalLoopXMode } from "./goal-loopx-mode";
import { GoalTeamResults } from "./goal-team-results";
import { GoalManagedResults } from "./goal-managed-results";
import { GoalResearchResults, type GoalResearchApi } from "./goal-research-results";
import { sendLoopXMessage, type LoopXModeSnapshot } from "../../data/chat";
import { MessageActivity } from "./message-activity";
import { ChannelTimeline } from "./channel-timeline";
Expand Down Expand Up @@ -202,6 +203,7 @@ function GoalOutputsView({
teamSessionId,
goalId,
localResults,
researchApi,
}: {
active: boolean;
items: Array<Extract<WorkspaceTimelineItem, { kind: "output" }>>;
Expand All @@ -210,6 +212,7 @@ function GoalOutputsView({
teamSessionId?: string;
goalId: string;
localResults: boolean;
researchApi?: GoalResearchApi;
}) {
const { locale, t } = useWorkspaceI18n();
const [teamSnapshot, setTeamSnapshot] = useState<LoopXModeSnapshot | null>(null);
Expand Down Expand Up @@ -261,6 +264,7 @@ function GoalOutputsView({
))}
{localResults ? <GoalManagedResults goalId={goalId} zh={locale === "zh-CN"} /> : null}
</section>
{active && researchApi ? <GoalResearchResults key={`${goalId}:${researchApi.indexUrl}:${researchApi.detailUrl}`} goalId={goalId} api={researchApi} zh={locale === "zh-CN"} /> : null}
{active && teamSessionId && !teamSnapshot && !teamError ? <p className="personal-object-list-state" role="status">{t("files.checkingTeam")}</p> : null}
{active && teamSessionId && teamError ? <p className="personal-object-list-state is-error" role="alert">{t("files.teamLoadFailed")} <button type="button" onClick={() => setTeamRefresh(value => value + 1)}>{t("startup.retry")}</button></p> : null}
{active && teamConfigured && teamSessionId ? <GoalTeamResults sessionId={teamSessionId} zh={locale === "zh-CN"} refreshKey={JSON.stringify(teamSnapshot?.deliveries ?? [])} /> : null}
Expand Down Expand Up @@ -787,6 +791,7 @@ export function PersonalWorkspacePage({
managerChannelBinding,
managerRuntime,
model,
researchApi,
readOnly = false,
typedActionsRevision = 0,
selectedAgentId: controlledAgentId,
Expand All @@ -807,6 +812,7 @@ export function PersonalWorkspacePage({
managerChannelBinding?: ManagerChannelBinding | null;
managerRuntime?: ManagerRuntimeSessionReadback | null;
model: WorkspaceModel;
researchApi?: GoalResearchApi;
ownerLabel?: string;
readOnly?: boolean;
// Bumped when typed previews were stored outside this page, so the page
Expand Down Expand Up @@ -1947,7 +1953,8 @@ export function PersonalWorkspacePage({
reportState={model.periodicReports}
teamSessionId={!readOnly && selectedAgentId === "codex" ? conversationSessionId : undefined}
goalId={selectedGoal.goalId}
localResults={!readOnly && selectedGoalTab === "files"}
localResults={!readOnly && selectedGoalTab === "files"}
researchApi={researchApi}
/>),
chat: (<>
{selectedGoal && activeSessionRun?.goalId === selectedGoal.goalId ? (
Expand Down
7 changes: 7 additions & 0 deletions apps/presentation/dashboard/src/views/dashboard-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {
fetchPeriodicReportIndex,
fetchPeriodicReportProjection,
periodicReportApiUrls,
presentationApiUrls,
resolveLocalStatusUrl,
scopedStatusUrl,
} from "../data/local-status-query";
Expand Down Expand Up @@ -1289,6 +1290,8 @@ function PersonalGoalHome({
}, [payload, rows, progress, goalSubagentConfigurationEnabled, t]);
const selectedGoal = model.goals.find((goal) => goal.goalId === selectedGoalId) ?? null;
const selectedPayload = progress?.snapshots[selectedGoalId] ?? payload;
const researchSource = resolveLocalStatusUrl(statusSourceControl.activeSource.statusUrl, window.location.href);
const researchUrls = researchSource.source ? presentationApiUrls(selectedPayload, researchSource.source) : null;
const [periodicReport, setPeriodicReport] = useState<PeriodicReportProjection | null>(null);
const [periodicReportError, setPeriodicReportError] = useState<string | null>(null);
const [periodicReportLoading, setPeriodicReportLoading] = useState(false);
Expand Down Expand Up @@ -3071,6 +3074,10 @@ function PersonalGoalHome({
conversationSupportsSteering={steeringSessionIds.has(runtimeBindings[contextId]?.sessionId ?? "")}
conversationHistoryState={conversationHistory}
model={workspaceModel}
researchApi={researchUrls?.indexUrl && researchUrls.detailUrl ? {
indexUrl: researchUrls.indexUrl, detailUrl: researchUrls.detailUrl,
goals: selectedPayload.run_history.goals,
} : undefined}
readOnly={readOnly}
selectedAgentId={selectedAgent.agentId}
selectedGoalId={selectedGoal?.goalId ?? null}
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.
2 changes: 2 additions & 0 deletions examples/personal-workspace-browser-smoke.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import { turnStepsScenario } from "./personal-workspace-browser/turn-steps.mjs";
import { monitorReadbackScenario } from "./personal-workspace-browser/monitor-readback.mjs";
import { teamEvidenceScenario } from "./personal-workspace-browser/team-evidence.mjs";
import { managedGoalResultsScenario } from "./personal-workspace-browser/managed-goal-results.mjs";
import { researchResultsScenario } from "./personal-workspace-browser/research-results.mjs";
import { loopxModeScenario } from "./personal-workspace-browser/loopx-mode.mjs";
import { progressiveLoadingScenario } from "./personal-workspace-browser/progressive-loading.mjs";
import { stewardJourneyScenario } from "./personal-workspace-browser/steward-journey.mjs";
Expand Down Expand Up @@ -78,6 +79,7 @@ scenarioCatalog.push(blockedNoticeSettingsScenario);
scenarioCatalog.push(nativeChildActivityScenario);
scenarioCatalog.push(externalEvidenceReadbackScenario);
scenarioCatalog.push(configurationBackupScenario);
scenarioCatalog.push(researchResultsScenario);
const requestedScenario = process.env.LOOPX_PERSONAL_WORKSPACE_SCENARIO;
const scenarios = requestedScenario
? scenarioCatalog.filter((scenario) => scenario.id === requestedScenario)
Expand Down
6 changes: 5 additions & 1 deletion examples/personal-workspace-browser/fixture.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -391,7 +391,7 @@ function filterStatusFixtureToScope(fixture, matchesScope) {
}
}

export async function installApi(page, { goalSubagentConfigurationEnabled = true, initialActionProposals = [], managerChannelBinding = null, notificationProjection = null, progressiveWorkspace = false, runtimeAgents = null } = {}) {
export async function installApi(page, { goalSubagentConfigurationEnabled = true, initialActionProposals = [], managerChannelBinding = null, notificationProjection = null, progressiveWorkspace = false, runtimeAgents = null, presentationApi = false } = {}) {
let turnCounter = 0;
const runtime = page.__loopxRuntime ??= { actionProposals: new Map(), goalSubagentConfigurations: new Map(), larkConnections: [], messages: new Map(), sessions: new Map(), turnMessages: new Map() };
const actionProposals = runtime.actionProposals;
Expand Down Expand Up @@ -517,6 +517,10 @@ export async function installApi(page, { goalSubagentConfigurationEnabled = true
...(fixture.local_dashboard_api ?? {}),
periodic_report_index_url: "/periodic-report-workspace",
periodic_report_detail_url: "/periodic-report-workspace-projection",
...(presentationApi ? {
presentation_surfaces_url: `${typeof presentationApi === "string" ? presentationApi : ""}/extension-presentation-surfaces`,
presentation_detail_url: `${typeof presentationApi === "string" ? presentationApi : ""}/extension-projection`,
} : {}),
};
for (const directoryGoal of directoryGoalFixtures) {
if (state.deletedGoalIds.has(directoryGoal.id)) continue;
Expand Down
Loading
Loading