diff --git a/apps/presentation/dashboard/src/data/chat.ts b/apps/presentation/dashboard/src/data/chat.ts
index 238724e826..57b13fc2ff 100644
--- a/apps/presentation/dashboard/src/data/chat.ts
+++ b/apps/presentation/dashboard/src/data/chat.ts
@@ -1,3 +1,4 @@
+import {sourceReadApiUrl} from "./local-status-query";
import type {OracleEvidence} from "./goal-acceptance-observation";
import { HANDOFF_MODES, EXECUTION_HANDOFF_MODES, type ExecutionHandoffMode } from "../../../../../loopx/control_plane/coordination/handoff_mode_vocabulary.js";
import { normalizeGoalDraft } from "../../../../../loopx/control_plane/collaboration/goal_draft.js";
@@ -2673,9 +2674,9 @@ const exploreResultPageSchema = z.object({
})),
});
export type ExploreResultPage = z.infer;
-export async function fetchExploreResults(goalId: string, cursor?: string) {
+export async function fetchExploreResults(goalId: string, cursor?: string, readSourceUrl?: string) {
const query = new URLSearchParams({goal_id: goalId, ...(cursor ? {cursor} : {})});
- const page = exploreResultPageSchema.parse(await requestJson(`/api/chat/explore-results?${query}`));
+ const page = exploreResultPageSchema.parse(await requestJson(sourceReadApiUrl(readSourceUrl, `/api/chat/explore-results?${query}`)));
if (page.goal_id !== goalId) throw new Error("Explore result Goal mismatch");
return page;
}
diff --git a/apps/presentation/dashboard/src/features/personal-workspace/channel-header.tsx b/apps/presentation/dashboard/src/features/personal-workspace/channel-header.tsx
index 635f44dae5..06369bdded 100644
--- a/apps/presentation/dashboard/src/features/personal-workspace/channel-header.tsx
+++ b/apps/presentation/dashboard/src/features/personal-workspace/channel-header.tsx
@@ -46,6 +46,8 @@ export function ChannelHeader({
workspaceGrantLabel,
originalConversationTitle,
requestInventory,
+ researchControl,
+ composerVisible = true,
}: {
agents: WorkspaceAgentOption[];
agentWorkActive?: boolean;
@@ -56,6 +58,8 @@ export function ChannelHeader({
workspaceGrantLabel?: string | null;
originalConversationTitle?: string;
requestInventory?: ReactNode;
+ researchControl?: ReactNode;
+ composerVisible?: boolean;
managerChannelBinding?: ManagerChannelBinding | null;
managerChatOpen?: boolean;
managerRuntime?: ManagerRuntimeSessionReadback | null;
@@ -164,7 +168,7 @@ export function ChannelHeader({
const runtimeControl = scopeUnavailable || agentWorkActive ? null : readOnlySourceLabel ? (
{selectedGoal?.title === readOnlySourceLabel ? null : readOnlySourceLabel}{t("common.readOnly")}
- ) : <>
+ ) : selectedGoal && !composerVisible ? null : <>
{ordinaryConversation ?
+
+ {researchControl}
{runtimeControl}
) : !scopeUnavailable ? (
diff --git a/apps/presentation/dashboard/src/features/personal-workspace/context-help.tsx b/apps/presentation/dashboard/src/features/personal-workspace/context-help.tsx
index b0ab105b1f..58c73076f3 100644
--- a/apps/presentation/dashboard/src/features/personal-workspace/context-help.tsx
+++ b/apps/presentation/dashboard/src/features/personal-workspace/context-help.tsx
@@ -30,17 +30,21 @@ export function ContextHelp({label, children}: {label: string; children: ReactNo
};
useEffect(() => () => clearTimeout(timer.current), []);
useEffect(() => {
- if (!open) return;
+ // toggle is queued by the browser. Escape can arrive while the visible
+ // popover has not yet updated React state; guard the actual DOM state.
const escape = (event: KeyboardEvent) => {
if (event.key === "Escape" && panel.current?.matches(":popover-open")) {
event.preventDefault(); event.stopPropagation(); dismiss();
}
};
window.addEventListener("keydown", escape, true);
+ return () => window.removeEventListener("keydown", escape, true);
+ }, []);
+ useEffect(() => {
+ if (!open) return;
window.addEventListener("resize", place);
window.addEventListener("scroll", place, true);
return () => {
- window.removeEventListener("keydown", escape, true);
window.removeEventListener("resize", place);
window.removeEventListener("scroll", place, true);
};
diff --git a/apps/presentation/dashboard/src/features/personal-workspace/goal-explore-results.css b/apps/presentation/dashboard/src/features/personal-workspace/goal-explore-results.css
new file mode 100644
index 0000000000..5218d721e6
--- /dev/null
+++ b/apps/presentation/dashboard/src/features/personal-workspace/goal-explore-results.css
@@ -0,0 +1,33 @@
+.goal-research-workbench { width: min(1280px, 100%); margin: 0 auto; }
+.research-saved-evidence { margin: 24px 0; border-top: 1px solid var(--pw-line); }
+.research-saved-evidence > summary { padding: 16px 0; min-height: 44px; cursor: pointer; }
+.goal-explore-results { width: 100%; padding: 24px 0; border: 0; border-radius: 0; }
+.explore-reader-heading { display: flex; align-items: center; gap: 8px; }
+.goal-explore-results .goal-team-results-layout { grid-template-columns: minmax(200px, 280px) minmax(0, 1fr); }
+.goal-explore-results .goal-team-result-list { max-height: 680px; overflow-y: auto; padding: 2px; align-self: start; }
+.goal-explore-results .goal-team-result-list button { flex-shrink: 0; }
+.goal-explore-results .goal-team-result-list strong { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
+.goal-explore-results .goal-team-result-list small { font-family: inherit; }
+.goal-explore-results .goal-team-result-reader { overflow-wrap: anywhere; }
+.goal-explore-results .goal-team-result-reader > h4 { font-size: var(--text-lead); line-height: var(--leading-lead); font-weight: 600; margin: 0 0 12px; }
+.explore-reader-status { color: var(--pw-muted); font-size: var(--text-caption); }
+.goal-explore-results .goal-team-result-reader p, .goal-explore-results .goal-team-result-reader li { font-size: var(--text-lead); line-height: var(--leading-prose); color: var(--pw-text); }
+.explore-finding-prose { max-width: 72ch; }
+.explore-finding-prose section { margin: 24px 0; }
+.explore-finding-prose h5, .explore-finding-prose h6 { font: inherit; font-weight: 600; margin: 0 0 8px; }
+.explore-finding-prose p, .explore-finding-raw { white-space: pre-wrap; }
+.explore-finding-prose ul, .explore-reader-details ul { padding-inline-start: 20px; list-style: disc; }
+.explore-reader-details { border-top: 1px solid var(--pw-line); margin-top: 16px; }
+.goal-explore-results .explore-reader-details > summary { font-size: var(--text-body); }
+.explore-reader-details dl { display: grid; gap: 8px; }
+.explore-reader-details dt { color: var(--pw-muted); font-size: var(--text-caption); }
+.explore-reader-details dd { margin-bottom: 12px; }
+.goal-explore-results .explore-linked-tasks { padding: 0; list-style: none; }
+.explore-linked-tasks > li { border-bottom: 1px solid var(--pw-line); }
+.explore-linked-tasks summary > span { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
+.explore-linked-tasks summary small { display: block; margin-top: 8px; color: var(--pw-muted); }
+@media (max-width: 640px) {
+
+ .goal-explore-results .goal-team-results-layout { grid-template-columns: minmax(0, 1fr); }
+ .goal-explore-results .goal-team-result-list { max-height: none; }
+}
diff --git a/apps/presentation/dashboard/src/features/personal-workspace/goal-explore-results.tsx b/apps/presentation/dashboard/src/features/personal-workspace/goal-explore-results.tsx
index 93bed53094..e55a22465f 100644
--- a/apps/presentation/dashboard/src/features/personal-workspace/goal-explore-results.tsx
+++ b/apps/presentation/dashboard/src/features/personal-workspace/goal-explore-results.tsx
@@ -1,63 +1,88 @@
import {useEffect, useRef, useState} from "react";
import {RefreshCw} from "lucide-react";
import {fetchExploreResults, type ExploreResultPage} from "../../data/chat";
+import {parseExploreFindingSections} from "../../../../../../loopx/control_plane/presentation/explore_timeline.js";
import {ExploreMeasurements} from "./explore-measurements";
+import {ContextHelp} from "./context-help";
+import "./goal-explore-results.css";
-/** Read-only saved evidence, including history when Explore has been disabled. */
-export function GoalExploreResults({goalId, zh, hideEmpty = false}: {goalId: string; zh: boolean; hideEmpty?: boolean}) {
+/** Saved evidence remains readable after Explore is disabled. No execution here. */
+export function GoalExploreResults({goalId, zh, active = true, readSourceUrl}: {goalId: string; zh: boolean; active?: boolean; readSourceUrl?: string}) {
const [page, setPage] = useState(null);
const [selectedId, setSelectedId] = useState("");
- const [busy, setBusy] = useState(false);
+ const [cursor, setCursor] = useState();
+ const [revision, setRevision] = useState(0);
+ const [busy, setBusy] = useState(true);
const [error, setError] = useState(false);
- const generation = useRef(0);
const reader = useRef(null);
+ const loadedQuery = useRef("");
useEffect(() => {
- void load();
- return () => {generation.current++;};
- }, [goalId]);
- async function load(cursor?: string) {
- const current = ++generation.current;
- setBusy(true); setError(false); setPage(null); setSelectedId("");
- try {
- const next = await fetchExploreResults(goalId, cursor);
- if (current !== generation.current) return;
- setPage(next); setSelectedId(next.items[0]?.finding_id ?? "");
- } catch {
- if (current === generation.current) setError(true);
- } finally {if (current === generation.current) setBusy(false);}
- }
+ if (!active) return;
+ const queryKey = JSON.stringify([goalId, readSourceUrl, cursor, revision]);
+ if (loadedQuery.current === queryKey) return;
+ let current = true;
+ setBusy(true); setError(false); setPage(null);
+ void fetchExploreResults(goalId, cursor, readSourceUrl).then(next => {
+ if (!current) return;
+ loadedQuery.current = queryKey;
+ setPage(next); setSelectedId(id => next.items.some(item => item.finding_id === id) ? id : next.items[0]?.finding_id ?? "");
+ }, () => {if (current) setError(true);}).finally(() => {if (current) setBusy(false);});
+ return () => {current = false;};
+ }, [goalId, cursor, revision, active, readSourceUrl]);
const selected = page?.items.find(item => item.finding_id === selectedId);
- if (hideEmpty && page && !busy && !error && !page.items.length && page.total === 0 && !page.next_cursor) return null;
+ const sections = selected ? parseExploreFindingSections(selected.summary) : null;
+ const statuses: Record = zh
+ ? {tentative: "待检查", confirmed: "来源记为确认", refuted: "已反驳", open: "未完成", done: "已完成", blocked: "受阻", deferred: "已延后"}
+ : {tentative: "Tentative", confirmed: "Recorded confirmation", refuted: "Refuted", open: "Open", done: "Done", blocked: "Blocked", deferred: "Deferred"};
+ const outcomes: Record = zh
+ ? {replan: "转向", continue: "继续", stop: "停止", wait: "等待", no_change: "无变化", ask_human: "请人决定"}
+ : {replan: "Replan", continue: "Continue", stop: "Stop", wait: "Wait", no_change: "No change", ask_human: "Ask human"};
+ const time = (value: string) => Number.isFinite(Date.parse(value))
+ ? new Intl.DateTimeFormat(zh ? "zh-CN" : "en", {month: "short", day: "numeric", hour: "2-digit", minute: "2-digit"}).format(new Date(value)) : value;
return
-
+ {zh ? "已保存的结论" : "Saved findings"}
+
+ {zh ? "来源记录的观察与结论,保存不代表已经验证或被后续决策采用。关联任务是原记录的显式关联。" : "Source-recorded observations and conclusions. Saving does not establish validation or adoption. Linked tasks are explicit source relationships."}
+
+
{busy ? {zh ? "正在读取证据…" : "Reading evidence…"}
: null}
{error ? {zh ? "无法核验此页证据;旧内容已清除。请刷新重试。" : "Cannot verify this evidence page; previous content was cleared. Refresh to retry."}
: null}
{page && !page.items.length ? {zh ? "尚无已保存的结论。" : "No saved findings yet."}
: null}
{page && page.items.length > 0 ?
- {selected ?
+ {selected ?
{selected.question || selected.finding}
- {selected.scope ?
{zh ? "问题范围" : "Question scope"}: {selected.scope}
: null}
+
{statuses[selected.status] ?? selected.status}
{selected.measurements ?
: null}
- {selected.summary}
- {zh ? "记录状态" : "Recorded status"}: {selected.status}
- {zh ? "记录者" : "Recorded by"}: {selected.agent_id || "—"}
- {zh ? "来源引用" : "Source references"}: {selected.evidence_refs.length ? selected.evidence_refs.join(" · ") : "—"}
- {zh ? "关联任务" : "Linked tasks"}
- {zh ? "来自当前及已完成任务的显式关联,不代表后续决策已采用此结论。" : "Explicit links from current and completed tasks; they do not establish adoption by later decisions."}
- {selected.linked_todos.length ? {selected.linked_todos.map(todo => -
-
{todo.text}
{todo.status} · {todo.claimed_by || "—"}
- )}
: {zh ? "当前及已完成任务中未找到关联。" : "No links found in current or completed tasks."}
}
- {zh ? "记录标识" : "Record identifiers"}
- {selected.finding_id} · {selected.node_id}
{selected.linked_todos.map(todo => todo.todo_id).join(" · ")}
+ {sections ?
+
{zh ? "观察" : "Observation"}
{sections.observation}
+ {sections.interpretation && !sections.decision ?
{zh ? "解读" : "Interpretation"}
{sections.interpretation}
: null}
+ {sections.decision ?
{zh ? "决策" : "Decision"} · {outcomes[sections.decision.outcome] ?? sections.decision.outcome}
+ {(["changed", "stopped", "retained"] as const).map(key => sections.decision![key].length ?
+
{({changed: zh ? "变化" : "Changed", stopped: zh ? "停止" : "Stopped", retained: zh ? "保留" : "Retained"})[key]}
+
{sections.decision![key].map((text, index) => - {text}
)}
+
: null)} : null}
+
: {selected.summary}
}
+ {zh ? "来源与范围" : "Sources and scope"}
+ - {zh ? "记录者" : "Recorded by"}
- {selected.agent_id || "—"}
+ {selected.scope ? <>- {zh ? "问题范围" : "Question scope"}
- {selected.scope}
> : null}
+ {sections ? <>- {zh ? "适用范围" : "Applicability"}
- {sections.applicability}
- {zh ? "输入版本" : "Input revision"}
- {sections.inputRevision}
> : null}
+ - {zh ? "来源引用" : "Source references"}
- {selected.evidence_refs.length ?
{selected.evidence_refs.map((ref, index) => {ref} )}
: "—"}
+ - {zh ? "记录标识" : "Record identifiers"}
{selected.finding_id} · {selected.node_id}
+ {sections ? {zh ? "完整原文" : "Full original text"}
{selected.summary}
: null}
+
+ {zh ? "关联任务" : "Linked tasks"} · {selected.linked_todos.length}
+ {selected.linked_todos.length ? {selected.linked_todos.map(todo => -
+
{todo.text}{statuses[todo.status] ?? todo.status} · {todo.claimed_by || "—"}
+ {todo.text}
{todo.todo_id}
+ )}
: {zh ? "当前及已完成任务中未找到关联。" : "No links found in current or completed tasks."}
}
: null}
: null}
diff --git a/apps/presentation/dashboard/src/features/personal-workspace/goal-overview.tsx b/apps/presentation/dashboard/src/features/personal-workspace/goal-overview.tsx
index e7050c9a01..3c7ef29ca0 100644
--- a/apps/presentation/dashboard/src/features/personal-workspace/goal-overview.tsx
+++ b/apps/presentation/dashboard/src/features/personal-workspace/goal-overview.tsx
@@ -8,7 +8,6 @@ import { relativeTime } from "./goal-activity-view";
import { goalAcceptanceContractCopy } from "./delivery-review-copy";
import type {GoalWorkspaceDigest} from "../../../../../../loopx/control_plane/presentation/goal_workspace_digest.js";
import { GoalAcceptanceObservationCard } from "./goal-acceptance-observation-card";
-import { GoalResearchHistory } from "./goal-research-history";
import { GoalAcceptanceContractSection } from "./goal-acceptance-contract";
import type { WorkspaceActionPreview, WorkspaceActionPreviewRequest, WorkspaceDrawerSelection, WorkspaceGoal, WorkspaceGoalTab, WorkspaceModel, WorkspaceTimelineItem } from "./personal-workspace-model";
import { useWorkspaceI18n } from "./i18n";
@@ -29,15 +28,14 @@ function relativeLookLabel(iso: string, locale: string): string {
return new Intl.DateTimeFormat(locale === "zh-CN" ? "zh-CN" : "en", { month: "short", day: "numeric" }).format(then);
}
-export function GoalOverview({ active, goal, items, userTodos, readOnly, onOpenDetails, onSelect, onView, selectedTodoId, board, results, proposals, onPreview, historyRefreshRevision, readSourceUrl, review, digest, railOpen, onCloseRail, onOpenAgentConversation, sinceYouLooked, onMarkSeen }: {
- readSourceUrl?: string;
+export function GoalOverview({ active, goal, items, userTodos, readOnly, onOpenDetails, onSelect, onView, selectedTodoId, board, proposals, onPreview, historyRefreshRevision, review, digest, railOpen, onCloseRail, onOpenAgentConversation, sinceYouLooked, onMarkSeen }: {
onOpenAgentConversation?: (agentId: string) => void;
/** Material changes since the owner's last acknowledged look; null when none. */
sinceYouLooked?: SinceYouLookedDelta | null; onMarkSeen?: () => void;
railOpen: boolean; onCloseRail: () => void;
review: DeliveryReviewRead; digest: GoalWorkspaceDigest;
active: boolean; goal: WorkspaceGoal; items: WorkspaceTimelineItem[]; userTodos: WorkspaceModel["userTodos"];
- selectedTodoId: string | null; board: ReactNode; results: ReactNode; historyRefreshRevision: number;
+ selectedTodoId: string | null; board: ReactNode; historyRefreshRevision: number;
proposals: Record
; onPreview: (request: WorkspaceActionPreviewRequest) => Promise;
readOnly: boolean; onOpenDetails: () => void; onSelect: (selection: WorkspaceDrawerSelection | null) => void; onView: (view: WorkspaceGoalTab) => void;
}) {
@@ -110,9 +108,8 @@ export function GoalOverview({ active, goal, items, userTodos, readOnly, onOpenD
{moreAttention ? : null}
: null}
- {!readOnly || readSourceUrl ? : null}
{copy.remote}
: }/>
{readOnly ? <>{copy.remote}
>
:
diff --git a/apps/presentation/dashboard/src/features/personal-workspace/goal-research-history.css b/apps/presentation/dashboard/src/features/personal-workspace/goal-research-history.css
index e01cd3432b..e459584e0f 100644
--- a/apps/presentation/dashboard/src/features/personal-workspace/goal-research-history.css
+++ b/apps/presentation/dashboard/src/features/personal-workspace/goal-research-history.css
@@ -1,4 +1,4 @@
-.research-history { margin-block: 0; border-block: 1px solid var(--pw-line); padding-block: 24px; min-width: 0; }
+.research-history { margin-block: 0; padding-block: 8px 24px; min-width: 0; }
.research-history-heading { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; }
.research-history-heading h2 { font-size: var(--text-heading); font-weight: 600; margin: 0 0 4px; }
.research-history-heading > div:first-child { min-width: 0; }
@@ -16,7 +16,7 @@
.research-history-edges path { fill: none; stroke: var(--pw-muted); stroke-width: 1.2; }
.research-history-edges path.is-grouping { stroke: var(--pw-faint); stroke-dasharray: 3 4; }
.research-history-edges path.is-refutation { stroke: var(--pw-red); }
-.goal-overview .research-history-node { position: absolute; width: 206px; height: 112px; padding: 12px; display: flex; flex-direction: column; align-items: stretch; justify-content: flex-start; gap: 6px; color: var(--pw-text); background: var(--pw-bg); border: 1px solid var(--pw-line); border-radius: 8px; text-align: left; font: inherit; cursor: pointer; }
+.research-history .research-history-node { position: absolute; width: 206px; height: 112px; padding: 12px; display: flex; flex-direction: column; align-items: stretch; justify-content: flex-start; gap: 6px; color: var(--pw-text); background: var(--pw-bg); border: 1px solid var(--pw-line); border-radius: 8px; text-align: left; font: inherit; cursor: pointer; }
.research-history-node:hover { border-color: var(--pw-muted); }
.research-history-node.is-selected { border-color: var(--pw-text); outline: 1px solid var(--pw-text); }
.research-history-node.is-blocked { border-left: 3px solid var(--pw-caution); }
@@ -59,10 +59,9 @@
.research-timeline-guide { display: flex; align-items: center; gap: 4px; color: var(--pw-muted); font-size: var(--text-caption); }
.research-timeline-guide > .research-link { margin-inline-start: auto; }
.research-phases { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; }
-.goal-overview .research-phase { display: flex; flex-direction: column; align-items: stretch; gap: 8px; min-width: 0; padding: 14px; border: 1px solid var(--pw-line); border-radius: 10px; background: var(--pw-bg); color: var(--pw-text); font: inherit; text-align: left; cursor: pointer; }
-.goal-overview .research-phase:hover { border-color: var(--pw-line-strong); }
-.goal-overview .research-phase.is-late { background: var(--pw-blue-soft); border-color: var(--pw-blue); }
-.goal-overview .research-phase.is-active { outline: 2px solid var(--pw-text); outline-offset: -1px; }
+.research-history .research-phase { display: flex; flex-direction: column; align-items: stretch; gap: 8px; min-width: 0; padding: 14px; border: 1px solid var(--pw-line); border-radius: 10px; background: var(--pw-bg); color: var(--pw-text); font: inherit; text-align: left; cursor: pointer; }
+.research-history .research-phase:hover { border-color: var(--pw-line-strong); }
+.research-history .research-phase.is-active { outline: 2px solid var(--pw-text); outline-offset: -1px; }
.research-phase-head { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; font-size: var(--text-body); }
.research-phase-head time { color: var(--pw-muted); font-size: var(--text-caption); white-space: nowrap; }
.research-phase-counts { display: flex; align-items: baseline; gap: 4px; font-size: var(--text-caption); color: var(--pw-muted); }
@@ -74,8 +73,7 @@
.research-phase-legend > span { display: inline-flex; align-items: center; gap: 4px; }
.research-phase-legend i { display: inline-block; width: 8px; height: 8px; border-radius: 2px; background: var(--research-tone); }
.research-phase-decisions { font-size: var(--text-caption); color: var(--pw-text); }
-.research-phase-line { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; font-size: var(--text-caption); color: var(--pw-muted); line-height: var(--leading-caption); }
-.research-phase-line em, .research-trail-list em, .research-finding-sections dt { font-style: normal; font-weight: 600; color: var(--pw-text); margin-right: 6px; }
+.research-trail-list em, .research-finding-sections dt { font-style: normal; font-weight: 600; color: var(--pw-text); margin-right: 6px; }
.is-confirmed { --research-tone: var(--pw-green); }
.is-refuted { --research-tone: var(--pw-red); }
.is-tentative { --research-tone: var(--pw-amber); }
@@ -109,25 +107,25 @@
.research-route-track { position: relative; height: 44px; }
.research-route-track svg { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; }
.research-route-line { stroke: var(--pw-line-strong); stroke-width: 2; vector-effect: non-scaling-stroke; }
-.goal-overview .research-timeline-marker { position: absolute; top: 10px; width: 44px; height: 44px; min-width: 44px; min-height: 44px; margin-left: -22px; padding: 0; border-radius: 6px; border: 0; background: transparent; cursor: pointer; }
-.goal-overview .research-timeline-marker::after { content: ""; position: absolute; top: 50%; left: 50%; width: 14px; height: 14px; border: 2px solid var(--research-tone); border-radius: 50%; background: var(--research-tone); transform: translate(-50%, -50%); }
-.goal-overview .research-timeline-marker.is-open::after { background: var(--pw-bg); }
-.goal-overview .research-timeline-marker.is-small { opacity: .85; }
-.goal-overview .research-timeline-marker.is-small::after { width: 9px; height: 9px; border-width: 1px; }
-.goal-overview .research-timeline-marker.is-cluster::after { width: 24px; height: 24px; border: 2px solid var(--pw-text); background: var(--pw-bg); }
+.research-history .research-timeline-marker { position: absolute; top: 10px; width: 44px; height: 44px; min-width: 44px; min-height: 44px; margin-left: -22px; padding: 0; border-radius: 6px; border: 0; background: transparent; cursor: pointer; }
+.research-history .research-timeline-marker::after { content: ""; position: absolute; top: 50%; left: 50%; width: 14px; height: 14px; border: 2px solid var(--research-tone); border-radius: 50%; background: var(--research-tone); transform: translate(-50%, -50%); }
+.research-history .research-timeline-marker.is-open::after { background: var(--pw-bg); }
+.research-history .research-timeline-marker.is-small { opacity: .85; }
+.research-history .research-timeline-marker.is-small::after { width: 9px; height: 9px; border-width: 1px; }
+.research-history .research-timeline-marker.is-cluster::after { width: 24px; height: 24px; border: 2px solid var(--pw-text); background: var(--pw-bg); }
.research-timeline-marker.is-cluster > span { position: absolute; inset: 0; display: grid; place-items: center; z-index: 1; color: var(--pw-text); font-size: var(--text-caption); }
-.goal-overview .research-timeline-marker.is-selected::after { box-shadow: 0 0 0 3px var(--pw-bg), 0 0 0 5px var(--pw-text); }
-.goal-overview .research-timeline-marker.is-selected { opacity: 1; z-index: 1; }
-.goal-overview .research-timeline-marker.is-dimmed, .research-decision-ticks .is-dimmed { opacity: .2; }
-.goal-overview .research-timeline-marker:hover::after { transform: translate(-50%, -50%) scale(1.25); }
+.research-history .research-timeline-marker.is-selected::after { box-shadow: 0 0 0 3px var(--pw-bg), 0 0 0 5px var(--pw-text); }
+.research-history .research-timeline-marker.is-selected { opacity: 1; z-index: 1; }
+.research-history .research-timeline-marker.is-dimmed, .research-decision-ticks .is-dimmed { opacity: .2; }
+.research-history .research-timeline-marker:hover::after { transform: translate(-50%, -50%) scale(1.25); }
.research-swarm { position: relative; height: 64px; border-bottom: 1px solid var(--pw-line); }
.research-trail { border-top: 1px solid var(--pw-line); padding-top: 12px; }
.research-trail h3 { display: flex; align-items: baseline; gap: 10px; margin: 0 0 10px; font-size: var(--text-body); font-weight: 600; }
.research-trail h3 small { color: var(--pw-muted); font-size: var(--text-caption); font-weight: 400; }
.research-trail-list { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; margin: 0; padding: 0; list-style: none; }
-.goal-overview .research-trail-list button { display: flex; flex-direction: column; align-items: stretch; gap: 4px; width: 100%; height: 100%; padding: 12px; border: 1px solid var(--pw-line); border-radius: 10px; background: var(--pw-bg); color: var(--pw-text); font: inherit; font-size: var(--text-caption); line-height: var(--leading-caption); text-align: left; cursor: pointer; }
-.goal-overview .research-trail-list button:hover { border-color: var(--pw-line-strong); }
-.goal-overview .research-trail-list button.is-selected { border-color: var(--pw-text); outline: 1px solid var(--pw-text); }
+.research-history .research-trail-list button { display: flex; flex-direction: column; align-items: stretch; gap: 4px; width: 100%; height: 100%; padding: 12px; border: 1px solid var(--pw-line); border-radius: 10px; background: var(--pw-bg); color: var(--pw-text); font: inherit; font-size: var(--text-caption); line-height: var(--leading-caption); text-align: left; cursor: pointer; }
+.research-history .research-trail-list button:hover { border-color: var(--pw-line-strong); }
+.research-history .research-trail-list button.is-selected { border-color: var(--pw-text); outline: 1px solid var(--pw-text); }
.research-trail-list strong { font-size: var(--text-body); line-height: var(--leading-body); font-weight: 600; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.research-trail-list span:not(.research-trail-meta) { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.research-trail-list .is-muted, .research-finding-sections .is-muted { color: var(--pw-muted); }
@@ -137,18 +135,18 @@
.research-outcome.is-stop, .research-outcome.is-ask_human { background: var(--pw-amber-bg); color: var(--pw-text); }
.research-link { padding: 0; border: 0; background: none; color: var(--pw-blue-ink); font: inherit; text-decoration: underline; cursor: pointer; }
.research-trail > .research-link { margin-top: 8px; font-size: var(--text-caption); }
-.research-finding-sections { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: 4px 10px; margin: 8px 0; font-size: var(--text-caption); line-height: var(--leading-caption); }
+.research-finding-sections { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: 8px 16px; margin: 16px 0; font-size: var(--text-lead); line-height: var(--leading-prose); }
.research-finding-sections dd { margin: 0; min-width: 0; }
.research-finding-sections ul { margin: 0; padding-left: 16px; }
.research-unlinked-days { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 8px; }
-.goal-overview .research-unlinked-days button { display: flex; justify-content: space-between; align-items: center; gap: 8px; padding: 10px 12px; border: 1px solid var(--pw-line); border-radius: 6px; background: var(--pw-bg); color: var(--pw-text); text-align: left; font: inherit; font-size: var(--text-caption); }
+.research-history .research-unlinked-days button { display: flex; justify-content: space-between; align-items: center; gap: 8px; padding: 10px 12px; border: 1px solid var(--pw-line); border-radius: 6px; background: var(--pw-bg); color: var(--pw-text); text-align: left; font: inherit; font-size: var(--text-caption); }
.research-unlinked-days time { color: var(--pw-muted); }
.research-unlinked-days strong { font-weight: 500; }
-.goal-overview .research-unlinked-days .is-selected { outline: 2px solid var(--pw-text); }
-.goal-overview .research-unlinked-days .is-dimmed { opacity: .4; }
+.research-history .research-unlinked-days .is-selected { outline: 2px solid var(--pw-text); }
+.research-history .research-unlinked-days .is-dimmed { opacity: .4; }
.research-decision-ticks .is-stop, .research-decision-ticks .is-wait, .research-decision-ticks .is-ask_human,
.research-tick-key.is-stop, .research-tick-key.is-wait, .research-tick-key.is-ask_human { background: var(--pw-amber); }
.research-decision-ticks .is-no_change, .research-tick-key.is-no_change { background: var(--pw-muted); }
@media (max-width: 640px) { .research-unlinked-days { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
-@media (max-width: 900px) { .research-phases, .research-trail-list { grid-template-columns: 1fr; } .research-timeline-grid { grid-template-columns: minmax(0, 1fr); } .research-row-label { flex-direction: row; flex-wrap: wrap; gap: 8px; } .research-row-plot { margin-bottom: 8px; } }
-@media (prefers-reduced-motion: reduce) { .goal-overview .research-timeline-marker:hover::after { transform: translate(-50%, -50%); } }
+@media (max-width: 900px) { .research-phases { grid-template-columns: repeat(3, minmax(260px, 1fr)); overflow-x: auto; padding: 2px; scroll-snap-type: x proximity; } .research-phase { scroll-snap-align: start; } .research-trail-list { grid-template-columns: 1fr; } .research-timeline-grid { grid-template-columns: minmax(0, 1fr); } .research-row-label { flex-direction: row; flex-wrap: wrap; gap: 8px; } .research-row-plot { margin-bottom: 8px; } }
+@media (prefers-reduced-motion: reduce) { .research-history .research-timeline-marker:hover::after { transform: translate(-50%, -50%); } }
diff --git a/apps/presentation/dashboard/src/features/personal-workspace/goal-research-history.tsx b/apps/presentation/dashboard/src/features/personal-workspace/goal-research-history.tsx
index d24b548d5c..dc83ee32d4 100644
--- a/apps/presentation/dashboard/src/features/personal-workspace/goal-research-history.tsx
+++ b/apps/presentation/dashboard/src/features/personal-workspace/goal-research-history.tsx
@@ -26,6 +26,16 @@ const selectedFromUrl = (goal: string): Selection => {
return {nodes: day === null ? [...new Set(nodes.filter(Boolean))] : [], day};
};
+const viewFromUrl = (goal: string) => {
+ const query = new URLSearchParams(window.location.search);
+ const sameGoal = !query.has("research_goal") || query.get("research_goal") === goal;
+ const phase = query.get("research_phase");
+ return {
+ lens: sameGoal && query.get("research_lens") === "graph" ? "graph" as const : "timeline" as const,
+ phase: sameGoal && phase !== null && /^[0-2]$/.test(phase) ? Number(phase) : null,
+ };
+};
+
/** An effect-free history consumer. Its source is Explore, not raw sessions or
* a judge. All recorded updates survive; unsupported analysis stays unavailable. */
export function GoalResearchHistory({goalId, active, zh, refreshRevision = 0, readSourceUrl}: {goalId: string; active: boolean; zh: boolean; refreshRevision?: number; readSourceUrl?: string}) {
@@ -38,9 +48,8 @@ export function GoalResearchHistory({goalId, active, zh, refreshRevision = 0, re
const [selection, setSelection] = useState(() => selectedFromUrl(goalId));
const [recordsOpen, setRecordsOpen] = useState(false);
const [recordLimit, setRecordLimit] = useState(50);
- const [lens, setLens] = useState(() => new URLSearchParams(window.location.search).get("research_lens") === "graph" ? "graph" : "timeline");
- const [phase, setPhase] = useState(null);
- useEffect(() => {setPhase(null);}, [goalId]);
+ const [lens, setLens] = useState(() => viewFromUrl(goalId).lens);
+ const [phase, setPhase] = useState(() => viewFromUrl(goalId).phase);
const presentationId = useId().replace(/:/g, "");
const detail = useRef(null);
const graph = useRef(null);
@@ -103,7 +112,10 @@ export function GoalResearchHistory({goalId, active, zh, refreshRevision = 0, re
const updateRecords = (events: readonly ExploreHistoryEvent[]) => {events.map(event => - {stateLabel(event.status)} · {event.title}{event.summary ? ` — ${event.summary}` : ""}{event.measurements ? : null}{event.parent_id ?
{copy.group}: {event.parent_id} : null}{event.evidence_refs.map(ref => {ref})}{event.run_id ? {event.run_id} : null} )}
;
useEffect(() => {
- const pop = () => setSelection(selectedFromUrl(goalId));
+ const pop = () => {
+ setSelection(selectedFromUrl(goalId));
+ const view = viewFromUrl(goalId); setLens(view.lens); setPhase(view.phase);
+ };
window.addEventListener("popstate", pop);
return () => window.removeEventListener("popstate", pop);
}, [goalId]);
@@ -157,7 +169,16 @@ export function GoalResearchHistory({goalId, active, zh, refreshRevision = 0, re
const grouped = useMemo(() => groupUnlinkedHistoryQuestions(value?.nodes ?? [], value?.edges ?? []), [value]);
const layout = useMemo(() => layoutExploreHistory(grouped.nodes, value?.edges ?? []), [grouped, value]);
const timeline = useMemo(() => value ? buildExploreTimeline(value.nodes, value.edges, value.events) : null, [value]);
- const chooseLens = (next: Lens) => { setLens(next); const url = new URL(window.location.href); if (next === "graph") url.searchParams.set("research_lens", "graph"); else url.searchParams.delete("research_lens"); window.history.replaceState(window.history.state, "", url); };
+ const chooseView = (next: Lens, nextPhase: number | null) => {
+ setLens(next); setPhase(nextPhase);
+ const url = new URL(window.location.href);
+ url.searchParams.set("research_goal", goalId);
+ url.searchParams.set("research_lens", next);
+ if (nextPhase === null) url.searchParams.delete("research_phase");
+ else url.searchParams.set("research_phase", String(nextPhase));
+ if (next !== lens || nextPhase !== phase) window.history.pushState(null, "", url);
+ };
+ const chooseLens = (next: Lens) => chooseView(next, phase);
const fullTime = (at: number) => new Intl.DateTimeFormat(zh ? "zh-CN" : "en", {month: "numeric", day: "numeric", hour: "2-digit", minute: "2-digit", hour12: false}).format(new Date(at));
const dayTime = (at: number) => new Intl.DateTimeFormat(zh ? "zh-CN" : "en", {month: "numeric", day: "numeric"}).format(new Date(at));
const positions = new Map(layout.map(item => [item.node.id, item]));
@@ -177,7 +198,7 @@ export function GoalResearchHistory({goalId, active, zh, refreshRevision = 0, re
else url.searchParams.set("research_node", next.nodes.length === 1 ? next.nodes[0]! : JSON.stringify(next.nodes));
url.searchParams.set("research_goal", goalId);
}
- else url.searchParams.delete("research_goal");
+ else url.searchParams.set("research_goal", goalId);
if (selection.day !== next.day || selection.nodes.length !== next.nodes.length || selection.nodes.some((id, index) => id !== next.nodes[index])) window.history.pushState(null, "", url);
if (focus && (next.nodes.length || next.day !== null)) window.requestAnimationFrame(() => {
const panel = detail.current;
@@ -185,7 +206,6 @@ export function GoalResearchHistory({goalId, active, zh, refreshRevision = 0, re
});
};
const choose = (id: string, focus = true) => select({nodes: id ? [id] : [], day: null}, focus);
- if (!busy && !error && !read.events.length && !analysis.pack && !analysis.snapshot?.catalog.packs.length) return null;
return