feat(editor): F4.6a simulation mode and a computer's desktop

Java's mode buttons start and stop the room, now in a worker of the
editor's page (LocalSimHost), with Java's speed slider. While the
simulation runs the design is locked as in Java: no palette, no sheet,
nothing moves, no undo. Cables that carry a frame light up in Java's
green, and a tap on a computer opens its desktop: Java's title, the
software installation first, then the installed programs in the order
of Java's list for the language, a taskbar with the network icon and
the network as the simulation runs it. The room sends that over a new
topic, node:<id>. Desktops stay over a return to the design and show
again at the next start. The programs' windows follow from F4.6b.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-03 13:41:41 +00:00
co-authored by Claude Opus 5.5
parent 6e97e3565d
commit dfd5ec03ce
44 changed files with 1190 additions and 41 deletions
+19 -3
View File
@@ -15,9 +15,10 @@ export type AppKind = (typeof APP_KINDS)[number];
/**
* What a client subscribes to: which cables carry frames (`activity`), a
* card's capture (`trace:<portId>`), a program's window
* (`app:<nodeId>/<app>`).
* (`app:<nodeId>/<app>`), a computer's network settings as it runs with them
* (`node:<nodeId>`: its desktop's title and network info).
*/
export type Topic = 'activity' | `trace:${string}` | `app:${string}/${AppKind}`;
export type Topic = 'activity' | `trace:${string}` | `app:${string}/${AppKind}` | `node:${string}`;
export const CLIENT_MESSAGE_KINDS = ['sub', 'unsub', 'start', 'stop', 'speed', 'input'] as const;
export type ClientMessageKind = (typeof CLIENT_MESSAGE_KINDS)[number];
@@ -41,7 +42,7 @@ export type ClientMessage =
readonly reqId: string;
};
export const HOST_MESSAGE_KINDS = ['state', 'activity', 'trace', 'app', 'ack', 'error'] as const;
export const HOST_MESSAGE_KINDS = ['state', 'activity', 'trace', 'app', 'node', 'ack', 'error'] as const;
export type HostMessageKind = (typeof HOST_MESSAGE_KINDS)[number];
export type SimMode = 'design' | 'simulation';
@@ -66,9 +67,23 @@ export type HostMessage =
| { readonly v: 1; readonly kind: 'trace'; readonly portId: string; readonly frames: readonly TraceFrame[]; readonly reset: boolean }
/** A program's new window lines from line `from` on; on subscribing, all (from 0). */
| { readonly v: 1; readonly kind: 'app'; readonly node: string; readonly app: AppKind; readonly from: number; readonly lines: readonly unknown[] }
/**
* A computer's first card as the simulation runs it (DHCP may change it):
* on subscribing, when it changes, and none while the simulation never ran.
*/
| { readonly v: 1; readonly kind: 'node'; readonly node: string; readonly settings: NodeSettings }
| { readonly v: 1; readonly kind: 'ack'; readonly reqId: string }
| { readonly v: 1; readonly kind: 'error'; readonly code: ErrorCode; readonly reqId?: string; readonly detail?: string };
/** What Java's desktop shows of a computer's network (NetworkInfoPanel, the window's title). */
export interface NodeSettings {
readonly ipAddress: string;
readonly subnetMask: string;
readonly defaultGateway: string;
readonly dnsServer: string;
readonly macAddress: string;
}
export type Parsed<T> = { readonly message: T } | { readonly error: { readonly code: ErrorCode; readonly detail: string } };
const isRecord = (value: unknown): value is Record<string, unknown> => typeof value === 'object' && value !== null && !Array.isArray(value);
@@ -79,6 +94,7 @@ export function isTopic(value: unknown): value is Topic {
if (value === 'activity') return true;
if (typeof value !== 'string') return false;
if (value.startsWith('trace:')) return value.length > 'trace:'.length;
if (value.startsWith('node:')) return /^node:[^/]+$/.test(value);
const app = /^app:([^/]+)\/(.+)$/.exec(value);
return app !== null && (APP_KINDS as readonly string[]).includes(app[2]!);
}