diff --git a/apps/presentation/dashboard/src/data/status.ts b/apps/presentation/dashboard/src/data/status.ts index 8ab9a00136..b79f89b55d 100644 --- a/apps/presentation/dashboard/src/data/status.ts +++ b/apps/presentation/dashboard/src/data/status.ts @@ -626,6 +626,17 @@ export const runGoalSchema = z.object({ raw_index_records: z.number().optional().default(0), unique_runs: z.number().optional().default(0), latest_runs: z.array(runRecordSchema).optional().default([]), + semantic_history: z.object({ + agents: z.array(z.object({ + latest_agent_vision_run: z.object({ + generated_at: z.string(), + agent_vision: z.object({ + vision_patch: z.object({vision_summary: z.string().nullish()}).nullish(), + path_delta: z.object({observed_reality: z.string().nullish()}).nullish().catch(null), + }), + }).nullish().catch(null), + })), + }).nullish().catch(null), }); export const runHistorySchema = z.object({ 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 3c02cb528b..5b3c5896bb 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 @@ -88,6 +88,9 @@ .research-history-read-error p, .research-history-empty p { margin: 4px 0 0; color: var(--pw-muted); font-size: var(--text-body); line-height: var(--leading-prose); } .research-history-read-error > button, .research-history-empty > button { display: inline-flex; align-items: center; justify-content: center; gap: 6px; flex-shrink: 0; min-height: 44px; padding: 8px 12px; border: 1px solid var(--pw-line); border-radius: 6px; background: var(--pw-bg); color: var(--pw-text); font: inherit; font-size: var(--text-body); cursor: pointer; } .research-history-read-error > button:hover, .research-history-empty > button:hover { background: var(--pw-hover); } +.research-source-strategy { margin-block-start: 12px; padding-block-start: 12px; border-block-start: 1px solid var(--pw-line); overflow-wrap: anywhere; } +.research-source-strategy time { margin-inline-start: 8px; color: var(--pw-muted); font-size: var(--text-caption); } +.research-source-strategy strong + time + p { color: var(--pw-text); } .research-history-error { color: var(--pw-red); font-size: var(--text-body); overflow-wrap: anywhere; } .research-history button:focus-visible, .research-history-canvas:focus-visible, .research-history-detail:focus-visible { outline: 2px solid var(--pw-blue); outline-offset: -2px; } @media (max-width: 640px) { @@ -98,6 +101,8 @@ .research-history-detail { padding: 20px; } .research-history-read-error, .research-history-empty { flex-wrap: wrap; } .research-history-read-error > button, .research-history-empty > button { margin-inline-start: 0; margin-top: 8px; } + .research-history-source-empty { flex-direction: column; } + .research-history-source-empty > button { margin-top: 0; } .research-history-meta { flex-direction: column; align-items: flex-start; gap: 6px; } .research-display-language { width: 100%; justify-content: space-between; } } 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 d5e6f710bb..2c0b430c48 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 @@ -10,6 +10,7 @@ import {GoalResearchTimeline} from "./goal-research-timeline"; import {requestJson} from "../../data/chat"; import {sourceReadApiUrl} from "../../data/local-status-query"; import {ContextHelp} from "./context-help"; +import type {WorkspaceGoal} from "./personal-workspace-model"; import {groupUnlinkedHistoryQuestions, layoutExploreHistory, parseExploreHistoryPage, projectExploreHistory, type ExploreHistoryEvent} from "../../../../../../loopx/control_plane/presentation/explore_history.js"; import "./goal-research-history.css"; import {ExploreMeasurements, formatExploreMeasurementValue} from "./explore-measurements"; @@ -52,7 +53,10 @@ const viewFromUrl = (goal: string) => { /** 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, originalGoalId = goalId, preferHistory = false, active, zh, refreshRevision = 0, readSourceUrl}: {goalId: string; originalGoalId?: string; preferHistory?: boolean; active: boolean; zh: boolean; refreshRevision?: number; readSourceUrl?: string}) { +export function GoalResearchHistory({goalId, originalGoalId = goalId, preferHistory = false, active, zh, refreshRevision = 0, readSourceUrl, sourceStrategy, onOpenSourceTasks}: { + goalId: string; originalGoalId?: string; preferHistory?: boolean; active: boolean; zh: boolean; refreshRevision?: number; readSourceUrl?: string; + sourceStrategy?: WorkspaceGoal["sourceStrategy"]; onOpenSourceTasks?: () => void; +}) { const analysis = useResearchAnalysis(goalId, active, zh, readSourceUrl); const readCaseMode = () => { const query = new URLSearchParams(window.location.search); @@ -463,7 +467,8 @@ export function GoalResearchHistory({goalId, originalGoalId = goalId, preferHist control?.querySelector("button:not(:disabled)")?.focus({preventScroll: true}); }; const readFailure = error || history.error; - const emptyHistory = !busy && !readFailure && read.total === 0; + const emptyHistory = !busy && !readFailure && read.complete && read.total === 0; + const emptySourceHistory = preferHistory && emptyHistory; const emptyWithAnalysis = emptyHistory && Boolean(analysis.pack?.result || analysis.pack?.native_outcomes); const emptyHistoryMessage = zh ? "这个 Goal 尚无研究历程" : "No research history for this Goal yet"; const hasCases = (analysis.snapshot?.catalog.packs ?? []).some(p => p.case_study_summary); @@ -490,27 +495,27 @@ export function GoalResearchHistory({goalId, originalGoalId = goalId, preferHist
- - {!readingSelection ?
{(["timeline", "graph"] as const).map(item => )}
: null} + }}> : null} + {!readingSelection && !emptySourceHistory ?
{(["timeline", "graph"] as const).map(item => )}
: null}
{!progressiveOverview ? : null} {readingSelection && analysis.pack ? {zh ? "分析范围" : "Analysis scope"} · {analysis.pack.title} : null}
-
+ {!emptySourceHistory ?
{emptyWithAnalysis ? <>{emptyHistoryMessage} · {read.events.length} / {read.total}

{zh ? "已保存的分析、原生测量与 Explore 历程分别读取。这个 Goal 没有已记录的 Explore 历程;已保存的结果仍可在下方阅读。" : "Saved analysis, native measurements and Explore history are read separately. This Goal has no recorded Explore history; its saved results remain readable below."}

: <>{copy.time}{value?.startAt ? <> · — : null} · {busy ? `${copy.reading} ${loaded}${read.total ? ` / ${read.total}` : ""}` : read.total === null ? (zh ? "来源尚未读到" : "Source has not been read") : `${read.events.length} / ${read.total} ${read.total === 1 ? copy.record : copy.records}${read.complete ? "" : ` · ${copy.partial}`}`}}
{!readingSelection ? displayControl : null} -
+
: null} {readFailure ?
{read.events.length ? (zh ? "刷新失败,当前记录已保留" : "Refresh failed; current records are preserved") : copy.failure} @@ -518,7 +523,18 @@ export function GoalResearchHistory({goalId, originalGoalId = goalId, preferHist
: null} - {emptyHistory && !emptyWithAnalysis ?
+ {emptySourceHistory ?
+
+

{zh ? "这份来源没有可读的 Explore 记录。" : "This source has no readable Explore records."}

+ {sourceStrategy ?
+ {zh ? "最近一条策略记录" : "Last recorded strategy"} + +

{sourceStrategy.summary}

+ {sourceStrategy.observation && sourceStrategy.observation !== sourceStrategy.summary ?

{sourceStrategy.observation}

: null} +
: null} +
+ {onOpenSourceTasks ? : null} +
: emptyHistory && !emptyWithAnalysis ?
{emptyHistoryMessage}

{zh ? "开启研究模式后,后续研究问题、尝试与证据会在这里形成可回读的历程。" : "Enable research mode to build a readable history of questions, attempts and evidence from later work."}

: null} diff --git a/apps/presentation/dashboard/src/features/personal-workspace/goal-research-workbench.tsx b/apps/presentation/dashboard/src/features/personal-workspace/goal-research-workbench.tsx index fccbfc6938..8952259aaf 100644 --- a/apps/presentation/dashboard/src/features/personal-workspace/goal-research-workbench.tsx +++ b/apps/presentation/dashboard/src/features/personal-workspace/goal-research-workbench.tsx @@ -5,7 +5,7 @@ import {goalHierarchy} from "./goal-hierarchy"; import type {WorkspaceGoal} from "./personal-workspace-model"; import './research-case-study.css'; -type ResearchGoal = Pick; +type ResearchGoal = Pick; /** Parentage organizes explicit source views; it never combines evidence or execution scope. */ export function ResearchGoalNavigation({goalId, goals, zh, onSelect}: { @@ -27,13 +27,15 @@ export function ResearchGoalNavigation({goalId, goals, zh, onSelect}: { } /** One read surface for the selected original Goal; opening it never starts research. */ -export function GoalResearchWorkbench(props: {goalId: string; active: boolean; zh: boolean; refreshRevision: number; readSourceUrl?: string; goals?: ResearchGoal[]; onSelectGoal?: (goalId: string) => void}) { - const source = props.goals?.find(goal => goal.goalId === props.goalId)?.sourceAccess; +export function GoalResearchWorkbench(props: {goalId: string; active: boolean; zh: boolean; refreshRevision: number; readSourceUrl?: string; goals?: ResearchGoal[]; onSelectGoal?: (goalId: string) => void; onOpenTasks?: () => void}) { + const selected = props.goals?.find(goal => goal.goalId === props.goalId); + const source = selected?.sourceAccess; const [evidenceOpen, setEvidenceOpen] = useState(false); return
{props.goals && props.onSelectGoal ? : null} + preferHistory={Boolean(source)} sourceStrategy={selected?.sourceStrategy} + onOpenSourceTasks={source ? props.onOpenTasks : undefined}/>
setEvidenceOpen(event.currentTarget.open)}> {props.zh ? "阅读已保存的结论" : "Read saved findings"} {evidenceOpen ? : null} diff --git a/apps/presentation/dashboard/src/features/personal-workspace/personal-workspace-model.ts b/apps/presentation/dashboard/src/features/personal-workspace/personal-workspace-model.ts index 56bd176f1f..744f28456a 100644 --- a/apps/presentation/dashboard/src/features/personal-workspace/personal-workspace-model.ts +++ b/apps/presentation/dashboard/src/features/personal-workspace/personal-workspace-model.ts @@ -166,6 +166,7 @@ export type WorkspaceGoalSubagentConfiguration = { export type WorkspaceGoal = { sourceAccess?: {namespace: string; label: string; readOnly: true; original_goal_id?: string}; + sourceStrategy?: {recordedAt: string; summary: string; observation?: string}; /** Registry organization in this status source; not Agent ownership or execution dependency. */ parentGoalId?: string | null; /** Bounded, safe text from this Goal's declared source document. */ diff --git a/apps/presentation/dashboard/src/features/personal-workspace/personal-workspace-page.tsx b/apps/presentation/dashboard/src/features/personal-workspace/personal-workspace-page.tsx index 7e4538e0bc..556752b149 100644 --- a/apps/presentation/dashboard/src/features/personal-workspace/personal-workspace-page.tsx +++ b/apps/presentation/dashboard/src/features/personal-workspace/personal-workspace-page.tsx @@ -2972,7 +2972,7 @@ export function PersonalWorkspacePage({ historyRefreshRevision={historyRefreshRevision} proposals={proposals} onPreview={createPreview} />, tasks: taskBoard(!settingsOpen && selectedGoalTab === "tasks"), research: !readOnly || researchReadSourceUrl - ? selectGoal(id, "research")} active={!settingsOpen && selectedGoalTab === "research"} zh={locale === "zh-CN"} refreshRevision={historyRefreshRevision} readSourceUrl={researchReadSourceUrl} /> + ? selectGoal(id, "research")} onOpenTasks={() => setSelectedGoalTab("tasks")} active={!settingsOpen && selectedGoalTab === "research"} zh={locale === "zh-CN"} refreshRevision={historyRefreshRevision} readSourceUrl={researchReadSourceUrl} /> :

{locale === "zh-CN" ? "此来源尚未提供研究记录的读取入口。" : "This source does not provide a research history endpoint."}

, files: goalResults(selectedGoalTab === "files" && !settingsOpen), chat: agentWorkActive ? projectionSentence(value, t)) .find((value) => value !== "" && value !== "暂无") ?? t("projection.nextUpdatePending"); + const sourceStrategies = goal.source_access ? goal.semantic_history?.agents.flatMap(({latest_agent_vision_run: run}) => { + const summary = run?.agent_vision.vision_patch?.vision_summary?.trim(); + if (!run || !summary || !Number.isFinite(Date.parse(run.generated_at))) return []; + return [{recordedAt: run.generated_at, summary, observation: run.agent_vision.path_delta?.observed_reality?.trim()}]; + }) : undefined; return [{ activationState: goal.activation_state, objective: goal.goal_objective ?? null, @@ -1140,6 +1145,7 @@ function buildPersonalHomeModel( goalId: goal.id, parentGoalId: goal.parent_goal_id, sourceAccess: goal.source_access ? {...goal.source_access, readOnly: true} : undefined, + sourceStrategy: sourceStrategies?.sort((left, right) => Date.parse(right.recordedAt) - Date.parse(left.recordedAt))[0], latestActivity: row.latestRun?.generated_at ?? "", needsYou, needsYouActionKind: needsYouTodo?.actionKind ?? null, diff --git a/docs/development/frontend-delivery.md b/docs/development/frontend-delivery.md index 9851d819fc..6a8d863c7c 100644 --- a/docs/development/frontend-delivery.md +++ b/docs/development/frontend-delivery.md @@ -135,6 +135,10 @@ identity on aliased views. A workspace id cannot replace a local Goal or another source; the same physical source cannot be linked twice. Removing an aliased link uses its workspace id and current physical namespace. Linked-source research opens native history by default; an authored case reading remains optional. +When a complete linked history read has no Explore records, show that limit +alongside any retained strategy and a route to the original Tasks. A failed +read still offers recovery, and a linked reader never offers to enable research +mode on the original Goal. Each linked Goal carries `source_access` in the workspace projection. Its original Goal id, tasks, explicit graph edges and native measurements remain