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
22 changes: 22 additions & 0 deletions docs/JOURNAL.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,28 @@ or files, and the "so-what" for future readers.

---

## 2026-07-18 — Accepting a corpus proposal jumped the scroll to the bottom

**Tags:** bug

Every "Accept" on a synthesis proposal fired `onChanged → onReloadCase →
render()`, a FULL portal re-render. `render()` rebuilds `#xr-view` and the
synthesis block repopulates asynchronously, so the scroll reset (landing
at the bottom) on every single accept — batch-accepting a run of proposals
was miserable. The re-render was also redundant: the accept handler already
updates the row in place (`✓`) and persists through the model firewalls
(EvidenceLinker / ClaimModel) independently of any render.

Fix: drop the per-accept re-render. `renderProposals` now gives in-place
feedback only — the row marks `✓`, the open count ticks down, and a hint
notes "N accepted — Refresh to fold them into the case view." The graph/
dossier reflect the accepted claims/links on the next Refresh (the batch
workflow), never mid-triage. Dismiss already used `row.remove()` (no
re-render) and was unaffected; it now also decrements the count.
`onChanged` is removed from the `renderProposals` contract.
(Membership add/remove still re-renders via its own `onReloadCase` — that
path is unchanged.)

## 2026-07-18 — The claim-join narrowed the brief: representative digest + a map/reduce version split

**Tags:** design
Expand Down
7 changes: 5 additions & 2 deletions src/portal/synthesis-block.js
Original file line number Diff line number Diff line change
Expand Up @@ -333,8 +333,11 @@ export function renderSynthesisBlock(host, { data, dossier, callbacks = {} }) {
onTriage: async (key, status) => {
record.triage = { ...(record.triage || {}), [key]: status };
await saveCaseBrief(record);
},
onChanged: () => callbacks.onReloadCase && callbacks.onReloadCase()
}
// No onChanged: accepting updates the row in place and
// persists through the model firewalls; a full re-render
// per Accept reset the scroll (jumped to the bottom).
// The case view folds accepted items in on the next Refresh.
});
};

Expand Down
41 changes: 35 additions & 6 deletions src/portal/synthesis-review.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,14 @@ async function accept(p, { model, memberByHash }) {
* Render the proposals list into `host`. `acceptable`/`rejected` come
* from filterProposals; `claimsById` labels existing refs;
* `memberByHash` (article_hash → {url, caseId}) resolves claim
* proposals; `model` stamps suggested_by; `onChanged` re-renders the
* case view after an accept.
* proposals; `model` stamps suggested_by.
*
* Accepting a proposal applies it immediately through the model
* firewalls and marks the row in place (✓). It deliberately does NOT
* re-render the whole case view: a per-accept full re-render reset the
* scroll position and threw the reader to the bottom, which made
* accepting a run of proposals painful. The graph/dossier fold the
* accepted claims/links in on the next Refresh.
*
* 27 S.3: `triage` is the brief record's per-proposal status map
* (proposalKey → 'accepted' | 'dismissed'); `onTriage(key, status)`
Expand All @@ -85,7 +91,7 @@ async function accept(p, { model, memberByHash }) {
* dismissal), and a zero-proposal run says so instead of rendering
* nothing.
*/
export function renderProposals(host, { acceptable, rejected, claimsById, memberByHash, model, onChanged, triage = {}, onTriage }) {
export function renderProposals(host, { acceptable, rejected, claimsById, memberByHash, model, triage = {}, onTriage }) {
host.replaceChildren();
const setTriage = async (key, status) => {
if (typeof onTriage === 'function') {
Expand All @@ -96,15 +102,36 @@ export function renderProposals(host, { acceptable, rejected, claimsById, member

const { open, accepted, dismissed } = partitionProposals(acceptable, triage);

host.appendChild(el('h4', 'xr-case__heading',
`Proposals — review and accept (${open.length})`));
const heading = el('h4', 'xr-case__heading',
`Proposals — review and accept (${open.length})`);
host.appendChild(heading);
if (open.length === 0 && accepted.length === 0 && dismissed.length === 0
&& (!rejected || rejected.length === 0)) {
host.appendChild(el('div', 'xr-inspector__mono',
'0 proposals — the synthesis found no reviewable actions this run.'));
return;
}

// Live, in-place feedback so a run of Accepts never re-renders (and
// never jumps the scroll): the open count ticks down and a hint
// appears. The accepted claim/link is already persisted; the case
// view folds it in on the next Refresh.
let openRemaining = open.length;
let acceptedCount = 0;
const updateCount = () => {
heading.textContent = `Proposals — review and accept (${openRemaining})`;
};
const refreshHint = el('div', 'xr-synth__status');
refreshHint.hidden = true;
host.appendChild(refreshHint);
const noteAccepted = ({ wasOpen }) => {
if (wasOpen) { openRemaining = Math.max(0, openRemaining - 1); updateCount(); }
acceptedCount += 1;
refreshHint.hidden = false;
refreshHint.textContent =
`${acceptedCount} accepted — Refresh (top right) to fold them into the case view.`;
};

const proposalRow = (p, key, { undismiss = false } = {}) => {
const row = el('div', 'xr-synth__prop');
row.appendChild(el('span', 'xr-synth__prop-desc', describe(p, claimsById)));
Expand All @@ -117,7 +144,7 @@ export function renderProposals(host, { acceptable, rejected, claimsById, member
await accept(p, { model, memberByHash });
await setTriage(key, 'accepted');
row.replaceChildren(el('span', 'xr-synth__prop-desc', '✓ ' + describe(p, claimsById)));
if (typeof onChanged === 'function') onChanged();
noteAccepted({ wasOpen: !undismiss });
} catch (err) {
Utils.error('Proposal accept failed', err);
acceptBtn.disabled = false;
Expand All @@ -130,6 +157,8 @@ export function renderProposals(host, { acceptable, rejected, claimsById, member
rejectBtn.type = 'button';
rejectBtn.addEventListener('click', async () => {
await setTriage(key, 'dismissed');
openRemaining = Math.max(0, openRemaining - 1);
updateCount();
row.remove();
});
row.appendChild(rejectBtn);
Expand Down
Loading