Keep popup instructions readable until continued

Show concise progress during first room create/join; reserve persistent recovery instructions for saved/uncertain attempts. Retain completed-action text until a local Continue button, without gating rules or AI. Preserve unread text across backgrounding, bounded summaries, mute and private-safe feedback. Fix the container QA helper path-import collision.

Verified: typecheck, lint, 5200 offline tests, 52 HTTP tests and production build. Delayed/lost-response browser checks prove progress, refresh recovery and exact retry without duplicates. Notice stayed for over 47 seconds while the QA game progressed; Continue advances only presentation. Mobile 16px/44px controls verified. Existing games preserved. Container/deployed acceptance follows publication.
This commit is contained in:
Quintine committed 2026-09-22 09:51:55 +10:00
1 parent c2d392e155
commit f29d05620c
10 files changed
+238 -115

No files matched your search

+31 -1
View File
@@ -11,6 +11,36 @@ export type QueuedActionNotice = AutomaticActionEvent & { count: number };
export type ActionNoticeCursor = { seenSeq: number; pending: QueuedActionNotice[] };
export type ActionNoticePresentation = ActionNoticeCursor & { active: QueuedActionNotice | null };
type ActionNoticeUpdate =
| { type: 'events'; events: readonly AutomaticActionEvent[]; enabled: boolean; visible: boolean }
| { type: 'continue' }
| { type: 'hide' }
| { type: 'clear' };
/** Existing history establishes a silent baseline, including after refresh. */
export function initialActionNotices(events: readonly AutomaticActionEvent[]): ActionNoticePresentation {
return { seenSeq: events.reduce((last, event) => Math.max(last, event.seq), -1), pending: [], active: null };
}
/** Local presentation only: reading or dismissing a notice never advances the game. */
export function updateActionNotices(state: ActionNoticePresentation, update: ActionNoticeUpdate): ActionNoticePresentation {
if (update.type === 'hide') return { ...state, pending: [] };
if (update.type === 'clear') return { ...state, active: null, pending: [] };
if (update.type === 'continue') return { ...state, active: state.pending[0] ?? null, pending: state.pending.slice(1) };
const active = update.enabled ? state.active : null;
const cursor = advanceActionNotices(state, update.events, {
active: !!active,
show: update.enabled && update.visible,
});
// New updates never replace text that the player has not dismissed.
return active
? { ...cursor, active }
: { ...cursor, active: cursor.pending[0] ?? null, pending: cursor.pending.slice(1) };
}
/** Consume every observed sequence, even when presentation is disabled. */
export function advanceActionNotices(
cursor: ActionNoticeCursor,
@@ -30,7 +60,7 @@ export function advanceActionNotices(
};
}
/** Keep at most three 1.5-second notices, including the one already on screen. */
/** Keep at most three notices, including the one awaiting local dismissal. */
export function appendActionNotices(
pending: readonly QueuedActionNotice[],
incoming: readonly AutomaticActionEvent[],