feat(editor): F4.6b the command line on a computer's desktop

A new oracle, TerminalWindowRun, types into Java's own command line
window: the window writes each line after its prompt, takes a blank
line, cls, reset and exit itself, keeps the lines for the arrow keys,
and keeps its text, history and working directory over a stop and a
start.

The room now holds a command line window per computer for everyone
(TerminalWindow), kept over stops; the protocol carries its lines and
the one way to read them (terminalScreen); the core goes on in the
working directory of the run before (continueIn). The desktop opens
programs in Java's frame, and the command line works with keyboard and
finger: grey on black, the prompt hidden while a command runs, history,
chips for the commands, Ctrl+C. The room's failure is now tested in the
browser.

Fixed: the room handed the engine the wall clock with fractions of a
millisecond, and a ping hung after its first timeout. The engine and
the room count whole milliseconds, as Java's clock does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-03 14:22:49 +00:00
co-authored by Claude Opus 5.5
parent dfd5ec03ce
commit ac48e969f4
23 changed files with 1255 additions and 47 deletions
+49 -9
View File
@@ -107,8 +107,11 @@ describe('a simulation room', () => {
expect(each.from).toBe(each.from === 0 ? 0 : lines.length);
lines = [...lines.slice(0, each.from), ...each.lines];
}
expect(lines).toEqual(engine.terminal(notebook.id)!.log.all);
expect(lines.length).toBeGreaterThan(4);
// The window: its start screen, the line entered after its prompt, then what the program wrote, as the engine has it.
const program = engine.terminal(notebook.id)!.log.all.map(({ timeMilliseconds: _time, ...entry }) => entry);
expect(lines.slice(0, 3)).toEqual([{ kind: 'text', text: engine.terminal(notebook.id)!.startScreen }, { kind: 'prompt', prompt: '/> ' }, { kind: 'input', prompt: '/> ', line: 'ping 141.99.5.10', command: true }]);
expect(lines.slice(3).filter((line) => (line as { kind: string }).kind !== 'prompt')).toEqual(program);
expect(program.length).toBeGreaterThan(4);
});
it('sends the cables that carry frames when they change, and only to who asked', () => {
@@ -160,7 +163,7 @@ describe('a simulation room', () => {
expect(reset.frames.length).toBeGreaterThan(8);
});
it('empties a window when its program starts anew', () => {
it("keeps a command line's window over a stop and a start, as Java does", () => {
const { got, send, node, run } = setUp();
const notebook = node('Notebook 1.10');
send({ kind: 'sub', topics: [`app:${notebook.id}/terminal`] });
@@ -168,11 +171,17 @@ describe('a simulation room', () => {
send({ kind: 'start' });
send({ kind: 'input', node: notebook.id, app: 'terminal', action: 'enter', args: { line: 'ipconfig' }, reqId: 'r1' });
run(400);
expect(ofKind(got, 'app').at(-1)!.lines.length).toBeGreaterThan(0);
const before = ofKind(got, 'app').reduce((count, each) => each.from + each.lines.length, 0);
expect(before).toBeGreaterThan(3);
const sentBefore = ofKind(got, 'app').length;
send({ kind: 'stop' });
expect(ofKind(got, 'app').at(-1)).toMatchObject({ from: 0, lines: [] });
send({ kind: 'start' });
expect(ofKind(got, 'app').at(-1)).toMatchObject({ from: 0 });
run(400);
// Nothing taken away: the window goes on after its lines.
expect(ofKind(got, 'app').slice(sentBefore).every((each) => each.from >= before)).toBe(true);
send({ kind: 'input', node: notebook.id, app: 'terminal', action: 'enter', args: { line: 'pwd' }, reqId: 'r2' });
run(400);
expect(ofKind(got, 'app').at(-1)!.from).toBeGreaterThanOrEqual(before);
});
it('answers an action that cannot be done with its reason', () => {
@@ -212,12 +221,14 @@ describe('a simulation room', () => {
expect(roundTrip(90)).toBeGreaterThan(roundTrip(100));
});
it('catches up at most two seconds after the clock jumped', () => {
const { got, send, jump } = setUp();
it('catches up at most two seconds after the clock jumped, and goes on from there', () => {
const { got, send, jump, run } = setUp();
send({ kind: 'start' });
jump(60_000);
// The missed minute is not made up over the next ticks either.
run(TICK_MILLISECONDS);
send({ kind: 'stop' });
expect(ofKind(got, 'state').at(-1)!.t).toBe(MAXIMUM_CATCH_UP_MILLISECONDS);
expect(ofKind(got, 'state').at(-1)!.t).toBe(MAXIMUM_CATCH_UP_MILLISECONDS + TICK_MILLISECONDS);
});
it('starts with the document as it is then', () => {
@@ -329,5 +340,34 @@ describe('a simulation room', () => {
send({ kind: 'sub', topics: [`node:${server.id}`] });
expect(ofKind(got.slice(before), 'node')).toHaveLength(1);
});
it("hands the engine whole milliseconds of a wall clock with fractions, losing none, and a ping goes through", () => {
// performance.now() has fractions; the core counts whole milliseconds as Java does (a fraction once made the ping hang).
const doc = exampleDoc();
let now = 0.718;
const room = new SimulationRoom(doc, { now: () => now, seed: () => SEED });
const got: HostMessage[] = [];
const client = room.connect((message) => got.push(message));
const notebook = readProject(doc).graph.nodes.find((each) => each.displayName === 'Notebook 1.10')!;
room.receive(client, { v: 1, kind: 'sub', topics: [`app:${notebook.id}/terminal`] });
room.receive(client, { v: 1, kind: 'start' });
for (let tick = 0; tick < 15; tick++) {
now += 200.4;
room.tick();
}
room.receive(client, { v: 1, kind: 'input', node: notebook.id, app: 'terminal', action: 'enter', args: { line: 'ping 141.99.5.10' }, reqId: 'r' });
for (let tick = 0; tick < 60; tick++) {
now += 200.4;
room.tick();
}
room.receive(client, { v: 1, kind: 'speed', value: 90 });
const t = ofKind(got, 'state').at(-1)!.t;
expect(Number.isInteger(t)).toBe(true);
// Less than a millisecond behind the wall clock since the start, never ahead of it.
expect(t).toBeLessThanOrEqual(now - 0.718);
expect(t).toBeGreaterThan(now - 0.718 - 1);
const text = ofKind(got, 'app').flatMap((each) => each.lines as { kind: string; text?: string }[]).map((line) => line.text ?? '').join('');
expect(text).toContain('4 Paket(e) gesendet, 4 Paket(e) empfangen');
});
});