mirror of
https://github.com/razzant/ouroboros.git
synced 2026-10-03 12:18:39 +00:00
154 lines
7.4 KiB
JavaScript
154 lines
7.4 KiB
JavaScript
import { taskPresentation } from './log_events.js';
|
|
|
|
// Pure desired-chip projection. Terminal truth wins; while unfinished, an
|
|
// owner stop/finalization hold stays sticky across ordinary progress frames.
|
|
export function desiredLiveCardPhase(record = {}, terminalPhase = 'done') {
|
|
if (record.finished) {
|
|
const presentation = taskPresentation(terminalPhase || 'done');
|
|
return {
|
|
phase: presentation.phase,
|
|
text: presentation.headline,
|
|
className: `chat-live-phase ${presentation.phase}`,
|
|
};
|
|
}
|
|
if (record.cancelPendingPolicy) {
|
|
return {
|
|
phase: 'working',
|
|
text: record.cancelPendingPolicy === 'finalize' ? 'Finalizing…' : 'Cancelling…',
|
|
className: 'chat-live-phase working cancelling',
|
|
};
|
|
}
|
|
if (record.finalizingHold) {
|
|
// #1110: when the outcome is already observed, it OWNS the chip and the
|
|
// hold states itself beside it. A card whose task had failed used to read
|
|
// only "Finalizing…", so the failure had to be smuggled into the title.
|
|
const observed = String(record.observedOutcome || '');
|
|
if (observed) {
|
|
const presentation = taskPresentation(observed);
|
|
return {
|
|
phase: presentation.phase,
|
|
text: presentation.headline,
|
|
className: `chat-live-phase ${presentation.phase}`,
|
|
secondary: 'Finalizing…',
|
|
};
|
|
}
|
|
return {
|
|
phase: 'working',
|
|
text: 'Finalizing…',
|
|
className: 'chat-live-phase working finalizing',
|
|
};
|
|
}
|
|
if (record.modelWaiting) return {
|
|
phase: 'working', text: 'Waiting for access', className: 'chat-live-phase working waiting',
|
|
};
|
|
return { phase: 'working', text: 'Working', className: 'chat-live-phase working' };
|
|
}
|
|
|
|
// A replayed final may preserve only an already-terminal phase. Ordinary DOM
|
|
// progress is presentation state, not terminal outcome truth.
|
|
export function replayTerminalPhase(record) {
|
|
return (record?.finished ? record?.phaseEl?.dataset?.phase : '') || 'done';
|
|
}
|
|
|
|
// Preserve the authoritative unfinished phase fact across an optimistic owner
|
|
// stop. DOM text alone is insufficient: a failed request must also restore an
|
|
// open post-task finalization hold so later progress cannot repaint Working.
|
|
export function captureLiveCardPhaseState(record = {}) {
|
|
return {
|
|
phase: String(record?.phaseEl?.dataset?.phase || 'working'),
|
|
finalizingHold: Boolean(record?.finalizingHold),
|
|
observedOutcome: String(record?.observedOutcome || ''),
|
|
};
|
|
}
|
|
|
|
export function restoreLiveCardPhaseState(record, snapshot) {
|
|
if (!record || !snapshot || record.finished) return null;
|
|
record.cancelPendingPolicy = '';
|
|
record.finalizingHold = Boolean(snapshot.finalizingHold);
|
|
record.observedOutcome = String(snapshot.observedOutcome || '');
|
|
return desiredLiveCardPhase(record, snapshot.phase || 'working');
|
|
}
|
|
|
|
// One writer for the stable factual task/subagent phase chip. Technical
|
|
// nonterminal diagnostics stay in the card timeline/details.
|
|
export function setLiveCardPhase(record, phase = 'working', text = '', className = '', secondary = '') {
|
|
if (!record?.phaseEl) return false;
|
|
const activePhase = String(phase || 'working');
|
|
const activeText = String(text || taskPresentation(activePhase).headline);
|
|
const activeClassName = className || `chat-live-phase ${activePhase}`;
|
|
const secondaryText = String(secondary || '');
|
|
const activeLabel = `${record.isSubagent ? 'Subagent' : 'Task'} status: ${activeText}`
|
|
+ (secondaryText ? `, ${secondaryText}` : '');
|
|
const secondaryChanged = setLiveCardPhaseSecondary(record, secondaryText);
|
|
const phaseEl = record.phaseEl;
|
|
const changed = phaseEl.dataset.phase !== activePhase
|
|
|| phaseEl.className !== activeClassName
|
|
|| phaseEl.textContent !== activeText;
|
|
if (phaseEl.dataset.phase !== activePhase) phaseEl.dataset.phase = activePhase;
|
|
if (phaseEl.className !== activeClassName) phaseEl.className = activeClassName;
|
|
// Do not make a polite live region re-announce identical routine progress.
|
|
if (phaseEl.textContent !== activeText) phaseEl.textContent = activeText;
|
|
if (phaseEl.getAttribute('role') !== 'status') phaseEl.setAttribute('role', 'status');
|
|
if (phaseEl.getAttribute('aria-live') !== 'polite') phaseEl.setAttribute('aria-live', 'polite');
|
|
if (phaseEl.getAttribute('aria-atomic') !== 'true') phaseEl.setAttribute('aria-atomic', 'true');
|
|
if (phaseEl.getAttribute('aria-label') !== activeLabel) phaseEl.setAttribute('aria-label', activeLabel);
|
|
return setLiveCardTypingVisible(record, !record.finished) || changed || secondaryChanged;
|
|
}
|
|
|
|
// The secondary chip is a SEPARATE fact beside the outcome, never a second
|
|
// status word: only the finalization hold writes it, and the primary chip's
|
|
// accessible name states both so the pair is read as one status.
|
|
export function setLiveCardPhaseSecondary(record, text = '') {
|
|
if (!record) return false;
|
|
if (record.phaseSecondaryEl === undefined) {
|
|
record.phaseSecondaryEl = record.root?.querySelector?.('[data-live-phase-secondary]') || null;
|
|
}
|
|
const el = record.phaseSecondaryEl;
|
|
if (!el) return false;
|
|
const next = String(text || '');
|
|
const hidden = !next || Boolean(record.phaseEl?.hidden);
|
|
if (el.textContent === next && el.hidden === hidden) return false;
|
|
el.textContent = next;
|
|
el.hidden = hidden;
|
|
return Boolean(el.isConnected);
|
|
}
|
|
|
|
// Phase and activity share this one animation writer. A subscription wait
|
|
// remains unfinished without pretending the paused role is doing computation.
|
|
export function setLiveCardTypingVisible(record, visible) {
|
|
if (!record?.inlineTypingEl) return false;
|
|
const display = visible && !record.modelWaiting && !record.reviewAnchor && !record.historicalUnavailable && !record.historicalUnconfirmed ? '' : 'none';
|
|
if (record.inlineTypingEl.style.display === display) return false;
|
|
record.inlineTypingEl.style.display = display;
|
|
return Boolean(record.inlineTypingEl.isConnected);
|
|
}
|
|
|
|
// Shared inert anatomy; the caller retains the reason (review or missing
|
|
// historical outcome), and no runtime lifecycle status is invented.
|
|
export function setInertCardPresentation(record, enabled) {
|
|
if (!record?.phaseEl) return;
|
|
record.phaseEl.hidden = enabled;
|
|
setLiveCardPhaseSecondary(record, enabled ? '' : desiredLiveCardPhase(record).secondary);
|
|
if (record.root?.dataset) record.root.dataset.inert = enabled ? '1' : '0';
|
|
setLiveCardTypingVisible(record, !enabled && !record.finished);
|
|
}
|
|
|
|
export function setHistoricalUnavailable(record, enabled) {
|
|
if (!record || (Boolean(record.historicalUnavailable) === enabled && !record.historicalUnconfirmed)) return false;
|
|
record.historicalUnavailable = enabled;
|
|
record.historicalUnconfirmed = false;
|
|
setInertCardPresentation(record, enabled || Boolean(record.reviewAnchor));
|
|
if (!enabled && !record.reviewAnchor) {
|
|
const desired = desiredLiveCardPhase(record);
|
|
setLiveCardPhase(record, desired.phase, desired.text, desired.className, desired.secondary);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
export function setHistoricalUnconfirmed(record) {
|
|
if (!record || record.finished || record.reviewAnchor || record.historicalUnavailable
|
|
|| record.historicalUnconfirmed) return false;
|
|
record.historicalUnconfirmed = true;
|
|
setInertCardPresentation(record, true);
|
|
return true;
|
|
}
|