/**
* Desktop event bus (#76) — the single event catalog.
*
* Everything noteworthy that happens inside the simulated desktop is emitted
* as a typed XPEvent. Hosts subscribe via the `onEvent` prop on
* (or the `useXPEvents` hook inside the tree); the scenario system (#84) reads
* the same stream as its trigger vocabulary.
*
* This union is the SINGLE SOURCE OF TRUTH. The naming grammar, the canonical
* domain list, payload-field conventions, and the additive-evolution policy are
* documented in `docs/EVENTS.md` (#130). The reference table in `USAGE.md` is
* GENERATED from the JSDoc on each member below by `scripts/gen-events-doc.mjs`
* and guarded against drift by `test/eventDocs.test.ts` — so:
*
* • give every new member a one-line `/** … *␟/` JSDoc (it becomes the table
* description), following the `domain:action` grammar and the payload rules
* in `docs/EVENTS.md`;
* • run `npm run docs:events` after adding or changing an event, or CI fails.
*/
export type XPEventBody =
/** An application window was opened. */
{
type: 'app:launch';
appId: string;
windowId: string;
title: string;
}
/** An application window was closed. */
| {
type: 'app:close';
appId: string;
windowId: string;
}
/** A window gained focus (was brought to the front). */
| {
type: 'window:focus';
windowId: string;
appId: string;
}
/** A window was minimized to the taskbar. */
| {
type: 'window:minimize';
windowId: string;
appId: string;
}
/** A window was maximized. */
| {
type: 'window:maximize';
windowId: string;
appId: string;
}
/** A window was restored from a minimized/maximized state. */
| {
type: 'window:restore';
windowId: string;
appId: string;
}
/** The Start menu was opened. */
| {
type: 'startmenu:open';
}
/** The Start menu was closed. */
| {
type: 'startmenu:close';
}
/** A right-click context menu was opened; `target` classifies what was clicked ('desktop' / 'file' / 'app' …) and `path` locates the node when one was the target. */
| {
type: 'contextmenu:open';
target?: string;
path?: string[];
}
/** A file or folder was opened (double-clicked / launched). */
| {
type: 'file:open';
path: string[];
name: string;
nodeType: string;
app?: string;
}
/** A file or folder was created. */
| {
type: 'file:create';
path: string[];
name: string;
nodeType: 'file' | 'folder';
}
/** A file's properties were edited; `content` is present when its text changed (the "player typed the passphrase" puzzle beat). */
| {
type: 'file:update';
path: string[];
name: string;
content?: string;
}
/** A file was deleted (moved to the Recycle Bin). */
| {
type: 'file:delete';
path: string[];
name: string;
}
/** A file or folder was renamed. */
| {
type: 'file:rename';
path: string[];
oldName: string;
newName: string;
}
/** A file was moved (cut+paste or drag) from `from` to `to`. */
| {
type: 'file:move';
from: string[];
to: string[];
name: string;
}
/** A file was copied from `from` to `to`. */
| {
type: 'file:copy';
from: string[];
to: string[];
name: string;
}
/** A file was restored from the Recycle Bin. */
| {
type: 'file:restore';
name: string;
}
/** A locked node was unlocked (correct password, or a host/scenario force-unlock). */
| {
type: 'file:unlock';
name: string;
}
/** A file's Properties dialog was opened — metadata (size, dates) inspected; a clue channel for scenarios (M1). */
| {
type: 'file:properties';
path: string[];
name: string;
}
/** A folder was deleted (files emit `file:delete`; folders emit this). */
| {
type: 'folder:delete';
path: string[];
name: string;
}
/** The Recycle Bin was emptied. */
| {
type: 'recyclebin:empty';
}
/** A printer queue was opened. */
| {
type: 'print:queue-open';
printerId: string;
}
/** A print job or its retained source metadata was inspected. */
| {
type: 'print:job-open';
jobId: string;
printerId: string;
}
/** A print job was added or changed. */
| {
type: 'print:job-update';
jobId: string;
printerId: string;
status: string;
}
/** A mutable print job was cancelled or removed. */
| {
type: 'print:job-cancel';
jobId: string;
printerId: string;
status: string;
}
/** A wrong password was entered for a locked node; `attempt` counts consecutive failures. */
| {
type: 'password:fail';
path: string[];
name: string;
attempt: number;
}
/** The user logged in successfully. */
| {
type: 'session:login';
}
/** A login attempt failed (wrong password). */
| {
type: 'session:login-fail';
}
/** The user logged out. */
| {
type: 'session:logout';
}
/** The desktop finished booting and is interactive. */
| {
type: 'session:boot-complete';
}
/** The machine was shut down, restarted, or logged out via the Start menu. */
| {
type: 'session:shutdown';
mode: 'shutdown' | 'restart' | 'logout';
}
/** A power transition was requested, before persistence and presentation begin. */
| {
type: 'session:shutdown-request';
mode: 'shutdown' | 'restart' | 'logout';
}
/** The configured power transition entered its blackout sequence. */
| {
type: 'session:blackout';
mode: 'shutdown' | 'restart' | 'logout';
}
/** The power transition completed and is ready for host navigation or reload. */
| {
type: 'session:shutdown-complete';
mode: 'shutdown' | 'restart' | 'logout';
}
/** A scenario flag's value changed (set/inc). Lets a trigger fire on progress itself, not only on a UI event. Emitted by the scenario runtime, not the core engine. */
| {
type: 'flag:change';
flag: string;
value: string | number | boolean | null;
}
/** A command was executed in the Command Prompt. */
| {
type: 'cmd:exec';
command: string;
}
/** A chat reply was requested from the host's ChatProvider. */
| {
type: 'chat:request';
buddyId: string;
}
/** A chat reply was received from the provider and rendered. */
| {
type: 'chat:reply';
buddyId: string;
text: string;
}
/** The provider failed or is absent; a scripted fallback line was used instead. */
| {
type: 'chat:fallback';
buddyId: string;
text: string;
}
/** Inbound LLM text was blocked by the ModerationProvider. */
| {
type: 'chat:moderated';
buddyId: string;
reason?: string;
}
/** Internet Explorer navigated to a URL; `generated` is true when the page came from a host content provider rather than a bundled/authored page. */
| {
type: 'ie:navigate';
url: string;
generated?: boolean;
}
/** The desktop wallpaper was changed (`wallpaper` is the id or URL). */
| {
type: 'wallpaper:change';
wallpaper: string;
}
/** The screensaver started. */
| {
type: 'screensaver:start';
}
/** The screensaver was dismissed. */
| {
type: 'screensaver:stop';
}
/** A tray notification balloon was shown. */
| {
type: 'notification:show';
id: string;
title: string;
body?: string;
}
/** A tray notification balloon was clicked. */
| {
type: 'notification:click';
id: string;
}
/** Fired on the top of each hour; hour is 0-23 (drives the hourly chime). */
| {
type: 'time:hour';
hour: number;
}
/** A persisted schedule fired (delay elapsed or its `at` deadline passed, incl. while the page was closed). */
| {
type: 'time:fire';
id: string;
}
/** The instance virtual wall-clock changed; `source` identifies a user edit or host API call. */
| {
type: 'time:change';
from: string;
to: string;
source: 'user' | 'host';
}
/** The user has been inactive for the idle threshold; `idleMs` is that threshold. */
| {
type: 'user:idle';
idleMs: number;
}
/** The user resumed activity after being idle. */
| {
type: 'user:active';
}
/** The player logged into QQ (the buddy-list panel opened). */
| {
type: 'qq:login';
}
/** The QQ client opened, or a specific buddy chat was opened (`buddyId`). */
| {
type: 'qq:open';
buddyId?: string;
}
/** A buddy came online. */
| {
type: 'qq:online';
buddyId: string;
nickname: string;
}
/** A QQ message was sent or received; `direction` is 'incoming' (from the buddy) or 'outgoing' (from the player). */
| {
type: 'qq:message';
buddyId: string;
direction: 'incoming' | 'outgoing';
text: string;
}
/** The player sent a reply to a buddy (the puzzle-relevant "player answered" beat). */
| {
type: 'qq:reply';
buddyId: string;
text: string;
}
/** A buddy went offline. */
| {
type: 'qq:offline';
buddyId: string;
}
/** A buddy's status or signature changed — a world reaction (e.g. a mood line the player is meant to notice). */
| {
type: 'qq:status';
buddyId: string;
status?: string;
signature?: string;
}
/** The player picked a scripted reply option (a branching choice, distinct from the free-text `qq:reply`). */
| {
type: 'qq:choice';
buddyId: string;
choiceId: string;
}
/** A read-only QQ archive or conversation was opened. */
| {
type: 'qq:archive-open';
archiveId: string;
conversationId?: string;
}
/** A QQ archive search was performed. */
| {
type: 'qq:archive-search';
archiveId: string;
query: string;
resultCount: number;
}
/** An individual archived QQ message was inspected. */
| {
type: 'qq:archive-message-open';
archiveId: string;
conversationId: string;
messageId: string;
}
/** An attachment in an archived QQ message was selected. */
| {
type: 'qq:archive-attachment-open';
archiveId: string;
conversationId: string;
messageId: string;
attachmentId: string;
}
/** A game started a new round; `appId` names the game and `difficulty` is present when it applies. */
| {
type: 'game:start';
appId: string;
difficulty?: string;
}
/** A game was won; `timeMs` is the completion time when the game tracks one. */
| {
type: 'game:win';
appId: string;
difficulty?: string;
timeMs?: number;
}
/** A game was lost. */
| {
type: 'game:lose';
appId: string;
difficulty?: string;
}
/** Media playback started or resumed; `path` is the source when known. */
| {
type: 'media:play';
path?: string;
title?: string;
trackId?: string;
playlistId?: string;
}
/** Media playback was paused. */
| {
type: 'media:pause';
path?: string;
trackId?: string;
playlistId?: string;
}
/** Media playback reached the end of the track. */
| {
type: 'media:ended';
path?: string;
trackId?: string;
playlistId?: string;
}
/** The playhead was moved; `position` is the new time in seconds. */
| {
type: 'media:seek';
path?: string;
position: number;
trackId?: string;
playlistId?: string;
}
/** The active track in a data-driven playlist changed. */
| {
type: 'media:track-change';
playlistId: string;
trackId: string;
index: number;
}
/** A playlist reached its deterministic end without repeating. */
| {
type: 'media:playlist-ended';
playlistId: string;
}
/** A query was run against an in-world search engine (a fake Baidu/AltaVista); hit is whether authored results matched. Emitted by the scenario runtime/app, not the core engine. */
| {
type: 'search:query';
query: string;
hit: boolean;
resultIds?: string[];
}
/** A term/clue entered the player's word bank (clicked a highlighted term, or granted by the scenario). */
| {
type: 'evidence:collect';
termId: string;
source?: string;
}
/** An item was pinned to the evidence board. */
| {
type: 'evidence:pin';
itemId: string;
}
/** Two pinned items were linked on the evidence board. */
| {
type: 'evidence:link';
sourceId: string;
targetId: string;
}
/** An item was removed from the evidence board. */
| {
type: 'evidence:unpin';
itemId: string;
}
/** The player submitted a deduction form (Mad-Libs slots / Obra-Dinn triples); `slots` maps slot id → chosen value. */
| {
type: 'deduction:submit';
formId: string;
slots?: Record;
}
/** A submitted deduction verified as correct; `groups` names the slot-groups that matched (supports verify-in-batches). */
| {
type: 'deduction:verified';
formId: string;
groups?: string[];
}
/** A submitted deduction was rejected; `groups` names the slot-groups that failed. */
| {
type: 'deduction:failed';
formId: string;
groups?: string[];
}
/** A structured evidence report was submitted with confidence and evidence citations. */
| {
type: 'deduction:report-submit';
reportId: string;
submission: import('./apps/EvidenceReport/logic').ReportSubmission;
}
/** One report claim was judged without exposing slot-by-slot solution details. */
| {
type: 'deduction:claim-result';
reportId: string;
claimId: string;
result: import('./apps/EvidenceReport/logic').ClaimResult;
}
/** A guided lesson started. */
| {
type: 'lesson:start';
lessonId: string;
}
/** A lesson step was completed (the learner performed the expected action). */
| {
type: 'lesson:step-complete';
lessonId: string;
stepId: string;
}
/** A hint was shown for the current step (hint-ladder escalation). */
| {
type: 'lesson:hint-shown';
lessonId: string;
stepId: string;
hintId?: string;
}
/** The learner took a wrong action on a step. */
| {
type: 'lesson:step-failed';
lessonId: string;
stepId: string;
}
/** A lesson finished; `score` is the assessed result when the lesson grades. */
| {
type: 'lesson:complete';
lessonId: string;
score?: number;
}
/** A software install/setup flow started. */
| {
type: 'install:start';
appId: string;
}
/** A software install completed. */
| {
type: 'install:complete';
appId: string;
}
/** A software install was cancelled before completing. */
| {
type: 'install:cancelled';
appId: string;
}
/** A semantic app control changed (checkbox toggled, option selected); `control` names it and `value` is the new value. Emitted by data-driven apps (defineApp), gated by `settingEquals`. */
| {
type: 'ui:action';
appId: string;
control: string;
value?: string | number | boolean;
}
/** A scoped-handle method was invoked successfully; `cap` names the capability, `method` the handle method, `args` a sanitized summary. */
| {
type: 'agent:call';
cap: string;
method: string;
args?: Record;
latencyMs?: number;
}
/** A scoped-handle call was denied (capability not in the allowlist); `cap` names the missing capability. */
| {
type: 'agent:denied';
cap: string;
method: string;
args?: Record;
}
/** A scoped-handle call required and received approval from the human gate before executing. */
| {
type: 'agent:approved';
cap: string;
method: string;
args?: Record;
}
/** The visitor followed a link out of the fiction to an external URL — the conversion signal campaigns measure. `newTab` is whether it opened in a new tab; `source` is the originating window id or file path, when known. */
| {
type: 'link:external';
url: string;
newTab: boolean;
source?: string;
};
/**
* The engine's event type. {@link XPEventBody} is the payload union; the optional
* `rehearsal` marker (#207) is stamped by the seek/rehearsal engine on events it
* replays into the journal. The `onEvent` host bridge drops rehearsal events, so
* fast-forwarding to a beat never fires external side effects or pollutes host
* analytics — while the engine's own `happened`/`count` predicates still see
* them (gating stays correct). Real gameplay never sets it.
*/
export type XPEvent = XPEventBody & {
rehearsal?: boolean;
};
export type XPEventType = XPEvent['type'];
export type XPEventListener = (event: XPEvent) => void;
/**
* Minimal synchronous pub/sub. Listener errors are isolated so a faulty host
* callback can never break the desktop.
*/
export declare class XPEventBus {
private listeners;
subscribe(listener: XPEventListener): () => void;
emit(event: XPEvent): void;
}
/**
* Create a fresh event bus. Advanced composers using the bare providers can
* make one bus, pass it to `EventBusProvider`, and observe it via `subscribe`
* — the same instance the desktop emits on (#122).
*/
export declare const createXPEventBus: () => XPEventBus;