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 @@ -264,7 +264,7 @@ export function GoalResearchHistory({goalId, active, zh, refreshRevision = 0, re
if (focusPending.current && detail.current) {
focusPending.current = false;
detail.current.focus({preventScroll: true});
detail.current.scrollIntoView({block: "nearest"});
detail.current.scrollIntoView({block: "start"});
}
}, [active, selected, selectedMembers]);
const clusterY = layout.length ? Math.max(...layout.map(item => item.y + 124)) + 24 : 24;
Expand All @@ -288,7 +288,7 @@ export function GoalResearchHistory({goalId, active, zh, refreshRevision = 0, re
else url.searchParams.set("research_goal", goalId);
if (selection.finding !== next.finding || selection.day !== next.day || selection.nodes.length !== next.nodes.length || selection.nodes.some((id, index) => id !== next.nodes[index])) window.history.pushState(null, "", url);
};
const choose = (id: string, focus = true) => {readerParent.current = null; select({nodes: id ? [id] : [], day: null}, focus);};
const choose = (id: string, focus = true, finding?: string) => {readerParent.current = null; select({nodes: id ? [id] : [], day: null, finding}, focus);};
const closeReader = () => {
const parent = readerParent.current;
readerParent.current = null;
Expand Down Expand Up @@ -331,7 +331,7 @@ export function GoalResearchHistory({goalId, active, zh, refreshRevision = 0, re
</ContextHelp>
</div> : null;
const historyView = !value ? null : lens === "timeline" && timeline ? <div className="research-history-canvas is-timeline" ref={graph} tabIndex={-1}>
<GoalResearchTimeline timeline={timeline} zh={zh} selectedIds={selection.nodes} selectedDay={selection.day} dayGroups={grouped.groups} onGroupSelect={day => select({nodes: [], day})} phase={phase} onPhase={next => chooseView(lens, next)} onSelect={id => choose(id)} onSelectMany={nodes => select({nodes, day: null})} timeLabel={fullTime} dayLabel={dayTime}/>
<GoalResearchTimeline timeline={timeline} zh={zh} selectedIds={selection.nodes} selectedDay={selection.day} dayGroups={grouped.groups} onGroupSelect={day => select({nodes: [], day})} phase={phase} onPhase={next => chooseView(lens, next)} onSelect={(id, finding) => choose(id, true, finding)} onSelectMany={nodes => select({nodes, day: null})} timeLabel={fullTime} dayLabel={dayTime}/>
</div> : <div className="research-history-canvas" ref={graph} tabIndex={0} aria-label={zh ? "完整已载入研究关系图;可滚动" : "Full loaded research graph; scrollable"}>
<div className="research-history-stage" style={{width, height}}>
<svg width={width} height={height} aria-hidden="true" className="research-history-edges">
Expand Down Expand Up @@ -505,10 +505,10 @@ export function GoalResearchHistory({goalId, active, zh, refreshRevision = 0, re
</li>)}</ol>
</div>
</div> : null}
{readingSelection ? <details className="research-history-context" key={selected?.id ?? JSON.stringify(selection.nodes) + selection.day}>
<summary>{zh ? "研究全景" : "Research overview"}</summary>
<ResearchProgressSummary analysis={analysis}/>{historyView}
</details> : historyView}
<details className={`research-history-context${readingSelection ? "" : " is-overview"}`} open={!readingSelection || undefined} key={JSON.stringify([goalId, readSourceUrl])}>
<summary hidden={!readingSelection}>{zh ? "研究全景" : "Research overview"}</summary>
{readingSelection ? <ResearchProgressSummary analysis={analysis}/> : null}{historyView}
</details>
</div> : null}
{(selection.nodes.some(id => !value?.nodes.some(node => node.id === id)) || (selection.day !== null && !selectedGroup)) && read.complete && !busy && !error && !history.error ? <p role="status">{copy.lost}</p> : null}
{value?.events.length && !value.nodes.length ? <ContextHelp label={zh ? "节点结构未记录" : "Node structure not recorded"} icon={CircleHelp}><p className="research-history-note">{zh ? "尚未记录节点结构;可展开下面的已读记录。" : "Node structure has not been recorded; loaded records remain available below."}</p></ContextHelp> : null}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ type Copy = Record<string, string>;
type Props = {
timeline: ExploreTimeline; zh: boolean; selectedIds: readonly string[]; phase: number | null;
selectedDay: string | null; dayGroups: ReturnType<typeof groupUnlinkedHistoryQuestions>["groups"]; onGroupSelect: (day: string) => void;
onSelect: (id: string) => void; onSelectMany: (ids: string[]) => void; onPhase: (index: number | null) => void;
onSelect: (id: string, finding?: string) => void; onSelectMany: (ids: string[]) => void; onPhase: (index: number | null) => void;
timeLabel: (value: number) => string; dayLabel: (value: number) => string;
};

Expand Down Expand Up @@ -198,13 +198,13 @@ export function GoalResearchTimeline({timeline, zh, selectedIds, selectedDay, da
</div>;
}

function TrailList({items, copy, label, timeLabel, selectedIds, onSelect}: {items: TimelineFinding[]; copy: Copy; label: (key: string) => string; timeLabel: (value: number) => string; selectedIds: readonly string[]; onSelect: (id: string) => void}) {
function TrailList({items, copy, label, timeLabel, selectedIds, onSelect}: {items: TimelineFinding[]; copy: Copy; label: (key: string) => string; timeLabel: (value: number) => string; selectedIds: readonly string[]; onSelect: (id: string, finding?: string) => void}) {
const [limit, setLimit] = useState(3);
return <>
<ol className="research-trail-list">{items.slice(0, limit).map(item => {
const decision = item.sections!.decision!;
return <li key={item.event.event_id}>
<button type="button" aria-pressed={selectedIds.includes(item.event.node_id)} className={selectedIds.includes(item.event.node_id) ? "is-selected" : ""} onClick={() => onSelect(item.event.node_id)}>
<button type="button" aria-pressed={selectedIds.includes(item.event.node_id)} className={selectedIds.includes(item.event.node_id) ? "is-selected" : ""} onClick={() => onSelect(item.event.node_id, item.event.result_id)}>
<span className="research-trail-meta"><b className={`research-outcome is-${decision.outcome}`}>{label(decision.outcome)}</b><time>{item.at !== null ? timeLabel(item.at) : ""}</time><span>{label(item.event.status)}</span></span>
<strong>{item.event.title}</strong>
<span><em>{copy.changed}</em>{decision.changed.join(";") || "—"}</span>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1153,7 +1153,7 @@ const en = {
"source.runtimeUnavailable": "Cannot read {source}. Your selected source is unchanged.",
"source.failureDetails": "Read details",
"source.readOnlyNoticeDescription": "You can view Goals, Tasks, evidence, and run status. Writes, guidance, and Agent sessions remain on the source host.",
"source.readOnlyNoticeTitle": "Remote read-only projection",
"source.readOnlyNoticeTitle": "Read-only source",
"source.readOnlyWriteError": "Remote SSH tunnel sources are read-only projections and cannot perform control-plane writes.",
"source.refreshHosts": "Reload SSH Hosts",
"source.remove": "Remove source {source}",
Expand Down Expand Up @@ -2802,7 +2802,7 @@ const zhCN: Record<WorkspaceMessageKey, string> = {
"source.runtimeUnavailable": "暂时读不到 {source},已保留所选来源。",
"source.failureDetails": "读取详情",
"source.readOnlyNoticeDescription": "你可以查看 Goal、Task、证据与运行状态;写入、纠偏和 Agent 会话仍留在来源主机。",
"source.readOnlyNoticeTitle": "远端只读投影",
"source.readOnlyNoticeTitle": "只读来源",
"source.readOnlyWriteError": "远端 SSH 隧道来源是只读投影,不能执行控制面写入。",
"source.refreshHosts": "重新读取 SSH Host",
"source.remove": "移除来源 {source}",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2979,7 +2979,7 @@ export function PersonalWorkspacePage({

{loopxMessageReceipt ? <p className="personal-composer-status" role="status">{loopxMessageReceipt}</p> : null}
{readOnly ? (
sourceGoal?.sourceAccess ? null : <div className="personal-read-only-notice"><strong>{t("source.readOnlyNoticeTitle")}</strong><span>{t("source.readOnlyNoticeDescription")}</span></div>
sourceGoal?.sourceAccess ? null : <div className="personal-read-only-notice"><strong>{t("source.readOnlyNoticeTitle")}</strong><ContextHelp label={t("source.readOnlyNoticeTitle")}><p>{t("source.readOnlyNoticeDescription")}</p></ContextHelp></div>
) : <>
{!selectedGoal && !managerChatOpen && managerConversationReceiptVisible && managerMessages.length ? (
<ManagerConversationTray onReviewGoalDraft={reviewGoalDraft} onSuggestReply={suggestReply}
Expand Down

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

5 changes: 4 additions & 1 deletion apps/presentation/dashboard/src/views/dashboard-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3448,8 +3448,11 @@ export function DashboardPage() {
await navigate({
replace: bindingInitialIdentity,
search: (current) => {
// Normalizing an alias of the same physical reader preserves its
// bookmarks; a different source still clears source-local selection.
const runtimeSourceChanged = Boolean(nextPayload.local_dashboard_api?.read_only_sources?.length
&& "statusUrl" in current && current.statusUrl && current.statusUrl !== url);
&& "statusUrl" in current && current.statusUrl && current.statusUrl !== url
&& (!prepared || !bound || statusSourceForUrl(prepared, current.statusUrl, window.location.href)?.statusUrl !== bound.statusUrl));
return {
...current,
statusUrl: url,
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.
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.
Original file line number Diff line number Diff line change
Expand Up @@ -95,12 +95,15 @@ export const researchDisplayLanguageScenario = {
const context = page.context();
// Model an appended analysis version becoming visible in the same
// real owner's catalog after this page has already read its snapshot.
let newVersionVisible = false;
let newVersionVisible = false, rejectedSubmission = false;
await page.route(url => url.pathname === "/api/chat/research-analysis", async route => {
if (route.request().method() !== "GET") return route.continue();
const response = await route.fetch();
const value = await response.json();
if (!newVersionVisible) value.catalog.packs = value.catalog.packs.filter(pack => pack.title !== "部分译文");
// Fault-inject advertised analysis availability for the refusal
// case only. The real fixture HTTP owner still rejects every POST.
if (rejectedSubmission) value.execution_enabled = true;
await route.fulfill({response, json: value});
});
const url = `${owner.base}/chat/?goalId=${owner.goal_id}&view=research&statusUrl=%2Fstatus.json`;
Expand Down Expand Up @@ -156,7 +159,9 @@ export const researchDisplayLanguageScenario = {
const entry = history.locator(".research-route-caption button").first();
const returnKey = await entry.getAttribute("data-research-return");
await entry.focus(); await page.keyboard.press("Enter"); await reader.waitFor();
assert.match(await reader.locator("h3").first().innerText(), /V18 是否在固定预算/);
const question = reader.locator(".research-reader-question");
assert(await question.evaluate(element => element.open), "The route's complete question opens with its finding");
assert.match(await question.innerText(), /V18 是否在固定预算/);
assert.match(await reader.innerText(), /实测变化为 -0.25ms/);
assert.match(await reader.innerText(), /第4次尝试有2项检查未通过(Trial4failed2checks);仅限本地(local-only)的证据仍属初步/);
assert.match(await reader.innerText(), /已反驳/);
Expand All @@ -178,7 +183,7 @@ export const researchDisplayLanguageScenario = {
await page.locator(".personal-channel-scroll").evaluate(element => element.scrollTop = 0);
await page.screenshot({path: resolve(outputDir, `research-display-language-reader-${width}.png`), animations: "disabled"});
await original.click();
assert.equal(await reader.locator("h3").first().innerText(), capture.events[0].title);
assert.equal(await question.locator(":scope > summary").innerText(), capture.events[0].title);
await page.screenshot({path: resolve(outputDir, `research-display-language-original-reader-${width}.png`), animations: "disabled"});
await history.getByRole("button", {name: "查看译文", exact: true}).click();
await reader.focus(); await page.keyboard.press("Escape");
Expand Down Expand Up @@ -251,6 +256,8 @@ export const researchDisplayLanguageScenario = {
if (width === 1440) {
const before = await fetch(`${owner.base}/fixture/readback`).then(response => response.json());
assert.equal(before.mutation_attempts, 0, "Reading, language and original toggles never execute");
rejectedSubmission = true;
await history.getByRole("button", {name: "刷新记录", exact: true}).click();
await history.locator(".research-analysis-entry-actions > button").click();
await history.getByRole("button", {name: "重新分析并补全译文", exact: true}).click();
await history.getByRole("alert").filter({hasText: "分析请求未完成,已保存结果保留。"}).waitFor();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export const researchFindingReaderScenario = {
const article = reader.locator(".research-recorded-finding");
const picker = reader.getByRole("combobox", {name: "选择问题结论", exact: true});
await picker.waitFor();
await page.getByText("开启研究模式", {exact: true}).waitFor();
assert.equal(await page.getByText("开启研究模式", {exact: true}).count(), 0, "The read-only capture does not expose a configuration action");
assert.equal(await picker.locator("option").count(), 20, "Every finding remains directly selectable");
assert.equal(await article.count(), 1, "A dense question presents one full document, not twenty stacked documents");
assert.equal(await picker.inputValue(), "finding-19");
Expand Down Expand Up @@ -102,6 +102,46 @@ export const researchFindingReaderScenario = {
assert.equal(await reader.count(), 0, "Returning restores the original full history");
assert.equal(new URL(page.url()).searchParams.has("research_finding"), false);

// A decision is an exact historical finding, even when twenty decisions
// belong to one question. Opening it must not substitute the latest one.
for (const width of [1440, 390]) {
await page.setViewportSize({width, height: 900});
const decisions = history.locator(".research-trail-list button");
const older = decisions.nth(1);
assert.match(await older.innerText(), /比较 19:/);
await older.focus();
await page.keyboard.press("Enter");
await picker.waitFor();
assert.equal(await picker.inputValue(), "finding-18", "An older decision opens its own finding, not the question's latest one");
assert.match(await article.innerText(), /观测 19:/);
const heading = await article.getByRole("heading").boundingBox();
const viewport = await page.locator(".personal-channel-scroll").boundingBox();
assert(heading && viewport && heading.y >= viewport.y && heading.y + heading.height <= 900,
`Opening a long finding starts with its heading visible at ${width}px`);
await page.screenshot({path: resolve(outputDir, `research-decision-reader-${width}.png`), animations: "disabled"});
await page.reload({waitUntil: "domcontentloaded"});
await picker.waitFor();
assert.equal(await picker.inputValue(), "finding-18", "Reload retains the decision's exact finding");
await reader.press("Escape");

await history.getByRole("button", {name: /显示更多决策/}).click();
const expandedCount = await decisions.count();
const earlier = decisions.nth(7);
assert.match(await earlier.innerText(), /比较 13:/);
await earlier.tap();
await picker.waitFor();
assert.equal(await picker.inputValue(), "finding-12");
assert.match(await article.innerText(), /观测 13:/);
await reader.press("Escape");
await reader.waitFor({state: "detached"});
assert.equal(await decisions.count(), expandedCount, "Returning keeps the expanded decision collection");
assert(await earlier.evaluate(element => element === document.activeElement), "Escape returns keyboard focus to the exact decision");
await page.goBack();
await picker.waitFor();
assert.equal(await picker.inputValue(), "finding-12", "Back restores the chosen historical decision");
await reader.press("Escape");
}

await page.goto(url.replace("research_node=question", "research_node=blocked"));
await reader.waitFor();
assert.match(await reader.innerText(), /无法读取评测来源,当前结果未知/);
Expand Down
Loading
Loading