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
11 changes: 11 additions & 0 deletions apps/presentation/dashboard/src/data/status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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; }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -463,7 +467,8 @@ export function GoalResearchHistory({goalId, originalGoalId = goalId, preferHist
control?.querySelector<HTMLElement>("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);
Expand All @@ -490,35 +495,46 @@ export function GoalResearchHistory({goalId, originalGoalId = goalId, preferHist
</ContextHelp></div>
</div>
<div className={`research-history-controls${readingSelection ? " is-reading" : ""}`}>
<button type="button" className="research-history-search" aria-label={zh ? "搜索研究记录" : "Search research records"} disabled={!value?.events.length} onClick={() => {
{!emptySourceHistory ? <button type="button" className="research-history-search" aria-label={zh ? "搜索研究记录" : "Search research records"} disabled={!value?.events.length} onClick={() => {
setRecordsOpen(true);
window.requestAnimationFrame(() => {
records.current?.querySelector<HTMLInputElement>("input")?.focus({preventScroll: true});
records.current?.scrollIntoView({block: "start"});
});
}}><Search size={15} aria-hidden/></button>
{!readingSelection ? <div className="research-lens" role="group" aria-label={zh ? "视图" : "View"}>{(["timeline", "graph"] as const).map(item => <button key={item} type="button" aria-pressed={lens === item} onClick={() => chooseLens(item)}>{item === "timeline" ? (zh ? "时间线" : "Timeline") : (zh ? "关系图" : "Graph")}</button>)}</div> : null}
}}><Search size={15} aria-hidden/></button> : null}
{!readingSelection && !emptySourceHistory ? <div className="research-lens" role="group" aria-label={zh ? "视图" : "View"}>{(["timeline", "graph"] as const).map(item => <button key={item} type="button" aria-pressed={lens === item} onClick={() => chooseLens(item)}>{item === "timeline" ? (zh ? "时间线" : "Timeline") : (zh ? "关系图" : "Graph")}</button>)}</div> : null}
<div className="research-analysis-scoped-action">{!progressiveOverview ? <ResearchAnalysisButton analysis={analysis}/> : null}
{readingSelection && analysis.pack ? <span className="research-analysis-scope">{zh ? "分析范围" : "Analysis scope"} · {analysis.pack.title}</span> : null}
</div>
<button type="button" aria-label={copy.refresh} disabled={busy} onClick={refresh}><RefreshCw size={15} aria-hidden/></button>
</div></header>
<div className="research-history-meta">
{!emptySourceHistory ? <div className="research-history-meta">
<div className="research-history-coverage" role="status">{emptyWithAnalysis ? <>{emptyHistoryMessage} · {read.events.length} / {read.total}<ContextHelp icon={FlaskConical} label={zh ? "研究历程来源" : "Research history source"}>
<p>{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."}</p>
</ContextHelp></> : <>{copy.time}{value?.startAt ? <> · <time>{timeLabel(value.startAt)}</time> — <time>{timeLabel(value.endAt)}</time></> : 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}`}`}</>}</div>
{!readingSelection ? displayControl : null}
</div>
</div> : null}
{readFailure ? <div className="research-history-read-error" role="alert">
<AlertCircle size={18} aria-hidden/>
<div><strong>{read.events.length ? (zh ? "刷新失败,当前记录已保留" : "Refresh failed; current records are preserved") : copy.failure}</strong>
<ContextHelp label={zh ? "研究记录读取详情" : "History read details"}><p>{readFailure}</p></ContextHelp>
</div>
<button type="button" disabled={busy} onClick={refresh}><RefreshCw size={14} aria-hidden/>{zh ? "重试" : "Retry"}</button>
</div> : null}
{emptyHistory && !emptyWithAnalysis ? <div className="research-history-empty">
{emptySourceHistory ? <div className="research-history-empty research-history-source-empty">
<div>
<p role="status">{zh ? "这份来源没有可读的 Explore 记录。" : "This source has no readable Explore records."}</p>
{sourceStrategy ? <div className="research-source-strategy">
<strong>{zh ? "最近一条策略记录" : "Last recorded strategy"}</strong>
<time dateTime={sourceStrategy.recordedAt}>{timeLabel(sourceStrategy.recordedAt)}</time>
<p>{sourceStrategy.summary}</p>
{sourceStrategy.observation && sourceStrategy.observation !== sourceStrategy.summary ? <p>{sourceStrategy.observation}</p> : null}
</div> : null}
</div>
{onOpenSourceTasks ? <button type="button" onClick={onOpenSourceTasks}>{zh ? "查看原实验任务" : "View original tasks"}</button> : null}
</div> : emptyHistory && !emptyWithAnalysis ? <div className="research-history-empty">
<div role="status"><strong>{emptyHistoryMessage}</strong><ContextHelp icon={FlaskConical} label={zh ? "研究历程来源" : "Research history source"}><p>{zh ? "开启研究模式后,后续研究问题、尝试与证据会在这里形成可回读的历程。" : "Enable research mode to build a readable history of questions, attempts and evidence from later work."}</p></ContextHelp></div>
<button type="button" onClick={focusResearchMode}>{zh ? "查看研究模式" : "Review research mode"}</button>
</div> : null}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {goalHierarchy} from "./goal-hierarchy";
import type {WorkspaceGoal} from "./personal-workspace-model";
import './research-case-study.css';

type ResearchGoal = Pick<WorkspaceGoal, 'goalId' | 'parentGoalId' | 'title' | 'sourceAccess'>;
type ResearchGoal = Pick<WorkspaceGoal, 'goalId' | 'parentGoalId' | 'title' | 'sourceAccess' | 'sourceStrategy'>;

/** Parentage organizes explicit source views; it never combines evidence or execution scope. */
export function ResearchGoalNavigation({goalId, goals, zh, onSelect}: {
Expand All @@ -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 <div className="goal-research-workbench">
{props.goals && props.onSelectGoal ? <ResearchGoalNavigation goalId={props.goalId} goals={props.goals} zh={props.zh} onSelect={props.onSelectGoal}/> : null}
<GoalResearchHistory {...props} originalGoalId={source?.original_goal_id}
preferHistory={Boolean(source)}/>
preferHistory={Boolean(source)} sourceStrategy={selected?.sourceStrategy}
onOpenSourceTasks={source ? props.onOpenTasks : undefined}/>
<details className="research-saved-evidence" open={evidenceOpen} onToggle={event => setEvidenceOpen(event.currentTarget.open)}>
<summary>{props.zh ? "阅读已保存的结论" : "Read saved findings"}</summary>
{evidenceOpen ? <GoalExploreResults originalGoalId={source?.original_goal_id} goalId={props.goalId} zh={props.zh} active={props.active} readSourceUrl={props.readSourceUrl}/> : null}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2972,7 +2972,7 @@ export function PersonalWorkspacePage({
historyRefreshRevision={historyRefreshRevision} proposals={proposals} onPreview={createPreview} />,
tasks: taskBoard(!settingsOpen && selectedGoalTab === "tasks"),
research: !readOnly || researchReadSourceUrl
? <GoalResearchWorkbench goalId={selectedGoal.goalId} goals={workspaceGoals} onSelectGoal={id => selectGoal(id, "research")} active={!settingsOpen && selectedGoalTab === "research"} zh={locale === "zh-CN"} refreshRevision={historyRefreshRevision} readSourceUrl={researchReadSourceUrl} />
? <GoalResearchWorkbench goalId={selectedGoal.goalId} goals={workspaceGoals} onSelectGoal={id => selectGoal(id, "research")} onOpenTasks={() => setSelectedGoalTab("tasks")} active={!settingsOpen && selectedGoalTab === "research"} zh={locale === "zh-CN"} refreshRevision={historyRefreshRevision} readSourceUrl={researchReadSourceUrl} />
: <ContextHelp label={locale === "zh-CN" ? "来源无研究读取入口" : "Research source unavailable"} icon={CircleHelp}><p role="status">{locale === "zh-CN" ? "此来源尚未提供研究记录的读取入口。" : "This source does not provide a research history endpoint."}</p></ContextHelp>,
files: goalResults(selectedGoalTab === "files" && !settingsOpen),
chat: agentWorkActive ? <GoalAgentWorkPage key={`${selectionScope}:${selectedWorkAgentId}`} goal={selectedGoal} agentId={selectedWorkAgentId!} readOnly={readOnly}
Expand Down
6 changes: 6 additions & 0 deletions apps/presentation/dashboard/src/views/dashboard-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1103,6 +1103,11 @@ function buildPersonalHomeModel(
personalAgentSentence(payload, row, state, t),
].map((value) => 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,
Expand Down Expand Up @@ -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,
Expand Down
4 changes: 4 additions & 0 deletions docs/development/frontend-delivery.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading