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

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ export function GoalResearchHistory({goalId, active, zh, refreshRevision = 0, re
const [loaded, setLoaded] = useState(0);
const [revision, setRevision] = useState(0);
const [selection, setSelection] = useState(() => selectedFromUrl(goalId));
// Pinning the displayed finding must not collapse a newly opened question.
const [questionExpanded, setQuestionExpanded] = useState(() => !selectedFromUrl(goalId).finding);
const [recordsOpen, setRecordsOpen] = useState(false);
const records = useRef<HTMLDetailsElement>(null);
const [lens, setLens] = useState<Lens>(() => viewFromUrl(goalId).lens);
Expand Down Expand Up @@ -139,14 +141,14 @@ export function GoalResearchHistory({goalId, active, zh, refreshRevision = 0, re
</details> : null}</>;
};
const findingMeta = (finding: ExploreHistoryEvent) => <small>{timeLabel(finding.recorded_at)} · {stateLabel(finding.status)}{finding.confidence !== null ? ` · ${zh ? "来源自报置信度" : "Source-reported confidence"} ${finding.confidence}` : ""}</small>;
const findingElement = (finding: ExploreHistoryEvent, title: string) => {
const findingElement = (finding: ExploreHistoryEvent) => {
const sections = findingSections(finding);
const content = <>{findingBody(finding)}{sections || finding.evidence_refs.length ? <details className="research-finding-sources"><summary>{zh ? "来源与适用范围" : "Sources and applicability"}{finding.evidence_refs.length ? ` · ${finding.evidence_refs.length}` : ""}</summary>
{sections ? <dl className="research-finding-sections"><dt>{sectionCopy.applicability}</dt><dd>{sections.applicability}</dd><dt>{sectionCopy.inputRevision}</dt><dd><code>{sections.inputRevision}</code></dd></dl> : null}
{finding.evidence_refs.map(ref => <code key={ref}>{ref}</code>)}
<details><summary>{zh ? "完整原文" : "Full original text"}</summary><h4>{originals.get(finding.event_id)?.title ?? finding.title}</h4><p>{originals.get(finding.event_id)?.summary ?? finding.summary}</p></details></details> : null}</>;
return <article className="research-recorded-finding" key={finding.event_id}>
{finding.title !== title ? <h4>{finding.title}</h4> : null}{findingMeta(finding)}{content}
<h3>{finding.title}</h3>{findingMeta(finding)}{content}
</article>;
};
const updateRecords = (events: readonly ExploreHistoryEvent[]) => <ol className="research-history-records">{events.map(event => <li key={event.event_id}><time>{timeLabel(event.recorded_at)}</time><div><p>{stateLabel(event.status)} · {event.title}</p>{event.summary ? findingBody(event) : null}</div>{event.parent_id ? <code>{copy.group}: {event.parent_id}</code> : null}{event.evidence_refs.map(ref => <code key={ref}>{ref}</code>)}{event.run_id ? <code>{event.run_id}</code> : null}</li>)}</ol>;
Expand All @@ -156,7 +158,9 @@ export function GoalResearchHistory({goalId, active, zh, refreshRevision = 0, re
readerParent.current = null;
focusReturn.current = null;
focusPending.current = true;
setSelection(selectedFromUrl(goalId));
const next = selectedFromUrl(goalId);
setSelection(next);
setQuestionExpanded(!next.finding);
const view = viewFromUrl(goalId); setLens(view.lens); setPhase(view.phase);
};
window.addEventListener("popstate", pop);
Expand Down Expand Up @@ -272,6 +276,7 @@ export function GoalResearchHistory({goalId, active, zh, refreshRevision = 0, re
focusPending.current = true;
}
setSelection(next);
setQuestionExpanded(!next.finding);
const url = new URL(window.location.href);
url.searchParams.delete("research_node"); url.searchParams.delete("research_group"); url.searchParams.delete("research_finding");
if (next.nodes.length || next.day !== null) {
Expand Down Expand Up @@ -440,8 +445,14 @@ export function GoalResearchHistory({goalId, active, zh, refreshRevision = 0, re
{selected ? <div className="research-history-detail" ref={detail} role="region" aria-label={selected.latest.title} tabIndex={-1} onKeyDown={event => {if (event.key === "Escape") {event.stopPropagation(); closeReader();}}}>
<div className="research-reader-content">
<div className="research-reader-location"><button type="button" className="research-reader-back" onClick={closeReader}><ArrowLeft size={14} aria-hidden/>{readerParent.current ? (zh ? "返回问题列表" : "Back to questions") : copy.all}</button>
<p className="research-detail-kind"><GitBranch size={14} aria-hidden/>{kindLabel(selected.latest.node_kind)} · {stateLabel(selected.latest.status)}</p></div>
<h3>{selected.latest.title}</h3>{displayControl}
<div className="research-reader-states">
<p className="research-detail-kind"><GitBranch size={14} aria-hidden/>{kindLabel(selected.latest.node_kind)} · {stateLabel(selected.latest.status)}</p>
{selected.findings.length > 1 && findingExceptions ? <p className="research-finding-exceptions">{findingExceptions}</p> : null}
</div></div>
{selected.findings.length ? selectedFinding?.title !== selected.latest.title ? <details className="research-reader-question" key={selected.id} open={questionExpanded} onToggle={event => setQuestionExpanded(event.currentTarget.open)}>
<summary><span>{selected.latest.title}</span></summary>
{selected.latest.summary ? <p>{selected.latest.summary}</p> : null}
</details> : null : <h3>{selected.latest.title}</h3>}{displayControl}
{selected.latest.blocked_reason ? <p className="research-history-error">{selected.latest.blocked_reason}</p> : null}
{selected.findings.length ? <>
{selected.findings.length > 1 ? <nav className="research-finding-navigation" aria-label={zh ? "问题结论" : "Question findings"}>
Expand All @@ -464,16 +475,15 @@ export function GoalResearchHistory({goalId, active, zh, refreshRevision = 0, re
})}
</div>
</nav> : null}
{selected.findings.length > 1 && findingExceptions ? <p className="research-finding-exceptions">{findingExceptions}</p> : null}
{selectedFinding ? findingElement(selectedFinding, selected.latest.title) : <p role="status">{zh ? "此记录中找不到所选结论;可从列表选择其他结论。" : "The selected finding is absent from this snapshot. Other findings remain available."}</p>}
{selectedFinding ? findingElement(selectedFinding) : <p role="status">{zh ? "此记录中找不到所选结论;可从列表选择其他结论。" : "The selected finding is absent from this snapshot. Other findings remain available."}</p>}
</> : <>{selected.latest.summary ? <p>{selected.latest.summary}</p> : null}<ContextHelp label={zh ? "暂无附属结论" : "No attached findings"} icon={CircleHelp}><p className="research-history-note">{zh ? "此节点没有附属结论记录。" : "No finding is attached to this node."}</p></ContextHelp></>}
<details className="research-node-provenance" key={selected.id}><summary>{zh ? "来源与历史" : "Sources and history"}</summary>
{selectedContextCitations.length ? <><h4>{zh ? "译文依据" : "Translation context"}</h4>
<div className="analysis-citations">{selectedContextCitations.map(citation => <button type="button" key={JSON.stringify(citation)}
onClick={event => {void analysis.readCitation(citation, event.currentTarget);}}>
{citation.source_id} · {zh ? "行" : "lines"} {citation.first_line}–{citation.last_line}
</button>)}</div></> : null}
{selected.findings.length && selected.latest.summary ? <p>{selected.latest.summary}</p> : null}
{selected.findings.length && selectedFinding?.title === selected.latest.title && selected.latest.summary ? <p>{selected.latest.summary}</p> : null}
{measurementSummary(selected.id)}
<h4>{copy.versions}</h4>{selected.latest.evidence_refs.length ? selected.latest.evidence_refs.map(ref => <code key={ref}>{ref}</code>) : <p>{zh ? "产物版本与来源引用未记录。" : "Artifact version and source references were not recorded."}</p>}
{selectedEdges.length ? <><h4>{copy.links}</h4><ul>{selectedEdges.map(edge => <li key={edge.event_id}>{positions.get(edge.from_node)?.node.latest.title ?? (zh ? "来源节点未记录" : "Source node not recorded")} → {relationLabel(edge.edge_type)} → {positions.get(edge.to_node)?.node.latest.title ?? (zh ? "目标节点未记录" : "Target node not recorded")}{edge.summary ? ` — ${edge.summary}` : ""}</li>)}</ul></> : null}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,31 @@ catalog over the same real history backend. They verify that the current pack's
name stays beside the action while history metrics are collapsed, and changes
with the existing scope selector. They do not qualify analysis authorization or
model execution; the regression rejects any non-read analysis request.

## Parent context and narrow reading

Packaged Chat UI, production read-only Explore HTTP owner, synthetic long
questions and three mixed-state findings. No real research material appears
in these captures. The analysis fixture is synthetic; this is layout and
navigation evidence, not model-quality or installed-product qualification.

Before: `920f18809d906b840325c4f40013e9224e4a3a2e`. After: this PR's reader
implementation. Desktop: 1200 × 888; narrow: 390 × 844. Both views use the same
fixture and selected finding, with the reader scrolled to its beginning.

| View | Before | After |
| --- | --- | --- |
| Desktop | ![Desktop before](context-before-desktop.png) | ![Desktop after](context-after-desktop.png) |
| Narrow | ![Narrow before](context-before-phone.png) | ![Narrow after](context-after-phone.png) |

Direct finding selection starts with its complete result and keeps the parent
question optional. Opening a question from the route still expands its complete
title immediately. The same full parent context remains available from a finding:

![Expanded parent context](context-after-context-phone.png)

The existing `research-route-reading` scenario verifies both entry intents,
readable finding text, complete titles, mixed states, keyboard return and
reload/Back in Chinese and English at 1440, 768 and 390 pixels. The broader
`research-history` scenario also covers unavailable sources, recovery, empty
history and saved-analysis readback. Neither reader dispatches a model.
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
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
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion examples/personal-workspace-browser/research-history.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -754,7 +754,7 @@ Path(sys.argv[1]).write_text(json.dumps({'goal_id':goal,'display_name':'Syntheti
assert(await detail.getByText('0.2 / 30', {exact:true}).isVisible());
await page.screenshot({path:resolve(outputDir, `research-execution-${viewport.width}.png`), animations:'disabled'});
assert.deepEqual(selectedNodes(),['hit-6']);
await detail.getByRole('heading',{name:'Synthetic hit target 6',exact:true}).waitFor();
await detail.getByText('Synthetic hit target 6',{exact:true}).waitFor();
await detail.press('Escape');
assert.deepEqual(selectedNodes(),ids,'Escape returns to the original group');
assert(await detail.locator('[data-research-node="hit-6"] button').evaluate(e=>e===document.activeElement));
Expand Down
26 changes: 23 additions & 3 deletions examples/personal-workspace-browser/research-route-reading.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ for route in range(3):
events.append(build_explore_finding_event(goal_id=goal,node_id=node,finding_id=f'finding-{node}',title='固定合成记录',
summary='完整观测:局部质量下降,延迟改善;完整成本和独立验证仍未知。',status='refuted' if route==2 else 'tentative',
recorded_at=f'2026-01-0{route+1}T{1 if endpoint=="start" else 13:02}:00:00Z'))
if route==0 and endpoint=='end':
for index,status in enumerate(('confirmed','refuted')):
events.append(build_explore_finding_event(goal_id=goal,node_id=node,finding_id=f'finding-{node}-{index}',
title='相同输入下的候选取舍与后续验证',summary='本地比较保留正结果和回退;独立收益及完整成本仍未知。',
status=status,recorded_at=f'2026-01-01T13:00:0{index+1}Z'))
Path(sys.argv[1]).write_text(json.dumps({'goal_id':goal,'display_name':'合成研究 · 路线阅读','events':events},ensure_ascii=False))
`, path], {cwd: repoRoot});
}
Expand Down Expand Up @@ -109,8 +114,23 @@ export const researchRouteReadingScenario = {
const fullTitle = capture.events.find(event => event.result_id === `route-0-${endpoint ? "end" : "start"}` && event.event_kind === "node").title;
await page.keyboard.press("Enter");
await reader.waitFor();
assert.equal(await reader.locator("h3").first().innerText(), fullTitle, "One activation reveals the complete original title");
assert(await reader.locator("h3").evaluate(element => element.scrollHeight <= element.clientHeight + 1), "Selected text is not clipped");
const question = reader.getByText(fullTitle, {exact: true});
assert.equal(await question.innerText(), fullTitle, "One route activation reveals the complete original question");
assert(await question.evaluate(element => element.scrollHeight <= element.clientHeight + 1 && element.scrollWidth <= element.clientWidth + 1), "Requested question text is not clipped");
if (endpoint) {
await reader.locator("select").selectOption("finding-route-0-end");
const body = reader.getByText("完整观测:局部质量下降,延迟改善;完整成本和独立验证仍未知。", {exact: true}).first();
const box = await body.boundingBox();
const reading = await history.evaluate(element => {const bounds = element.closest(".personal-channel-scroll")?.getBoundingClientRect(); return {top: Math.max(0, bounds?.top ?? 0), bottom: Math.min(innerHeight, bounds?.bottom ?? innerHeight)};});
const lineHeight = await body.evaluate(element => parseFloat(getComputedStyle(element).lineHeight));
assert(box.y >= reading.top - 1 && box.y + Math.min(box.height, lineHeight * 2) <= reading.bottom + 1, "The selected finding begins with readable content inside the reading viewport");
assert(await body.evaluate(element => parseFloat(getComputedStyle(element).fontSize) >= 16), "Density retains readable finding text");
assert.match(await reader.locator(".research-finding-exceptions").innerText(), /1/);
const context = reader.getByText(fullTitle, {exact: true});
await context.click();
assert(await context.evaluate(element => element.scrollHeight <= element.clientHeight + 1 && element.scrollWidth <= element.clientWidth + 1), "Full parent context remains one optional activation away");
await context.click();
}
if (endpoint && language === "zh-CN" && width !== 768) await page.screenshot({path: resolve(outputDir, `research-route-reading-detail-${width}.png`), animations: "disabled"});
await page.keyboard.press("Escape");
assert.equal(await reader.count(), 0);
Expand Down Expand Up @@ -142,7 +162,7 @@ export const researchRouteReadingScenario = {
await reader.getByRole("button", {name: "Whole graph", exact: true}).click();
await page.goBack();
await reader.waitFor();
assert.match(await reader.locator("h3").first().innerText(), /末端研究/);
assert.match(await reader.locator(".research-reader-question").innerText(), /末端研究/);
assert.equal(analysisWrites, 0, "Reading, language changes and every action state never dispatch a model");
const readback = await (await page.request.get(`${owner.base}/fixture/readback`)).json();
assert(readback.source_unchanged && readback.mutation_attempts === 0, "No source or execution state changes");
Expand Down
Loading