diff --git a/app/src/agentworld/iso/GameWorld.ts b/app/src/agentworld/iso/GameWorld.ts index 5c5f6f773..6adf28b0f 100644 --- a/app/src/agentworld/iso/GameWorld.ts +++ b/app/src/agentworld/iso/GameWorld.ts @@ -2,9 +2,10 @@ * The top-level world controller. * * `GameWorld` owns the PixiJS application (WebGPU-preferred), the active room, - * every agent, and the render loop. It exposes the authoritative entry point - * {@link GameWorld.updateAgentState} for external/AI control, plus click-to-move - * for human debugging. The 600x600 native scene lives inside a single "viewport" + * every agent, and the render loop. It exposes the reconciliation seam + * {@link GameWorld.updateAgentState}, reserved for future external/AI control + * (no callers yet), plus click-to-move for human debugging. The 600x600 native + * scene lives inside a single "viewport" * container that is scaled to fill its parent, so the world stays crisp pixel * art at any size. */ @@ -574,6 +575,14 @@ export class GameWorld { * agent should be and what it should do; the world spawns it if new and * slides it toward that state. Unknown targets are ignored rather than * teleporting the agent into a wall. + * + * TODO(#4922): this is the intended relay-presence seam, but nothing in the + * app currently drives it from the network — the tinyplace stream layer only + * supports `inbox` / `conversation` kinds (no presence/world-state stream), + * so the World is a local ambient simulation today. Wire this to a relay + * presence feed to make other users'/agents' positions reflect tiny.place in + * real time. Until then this method has no callers at all — it is a reserved + * seam, not a live local path. */ public updateAgentState(agentId: string, state: AgentState): void { const room = this.room; @@ -706,7 +715,14 @@ export class GameWorld { this.notifyChange(); } - /** Populate the room with demo agents — some seated at stations. */ + /** + * Populate the room with demo agents — some seated at stations. + * + * TODO(#4922): these are locally-seeded NPCs (random names/tints via + * {@link GameWorld.pickName} / {@link GameWorld.pickTint}), not real + * tiny.place participants. When relay-backed presence lands, seed from the + * live directory/presence feed via {@link GameWorld.updateAgentState} instead. + */ public spawnAgents(count: number): void { const room = this.room; if (!room) { diff --git a/app/src/agentworld/pages/WorldSection.test.tsx b/app/src/agentworld/pages/WorldSection.test.tsx index 63d236be5..2886ce94a 100644 --- a/app/src/agentworld/pages/WorldSection.test.tsx +++ b/app/src/agentworld/pages/WorldSection.test.tsx @@ -81,6 +81,22 @@ describe('WorldSection renderer boot', () => { expect(screen.queryByText(/booting renderer/i)).not.toBeInTheDocument(); }); + test('renders an accessible "offline preview" pill so users know agents are a local sim (#4922)', () => { + initImpl = () => new Promise(() => {}); // renderer state is irrelevant here + render(); + // The badge is a focusable button (operable under the card's + // pointer-events-none), not a decorative span. + const badge = screen.getByRole('button', { name: /offline preview/i }); + expect(badge).toBeInTheDocument(); + // Its explanatory tooltip is wired via aria-describedby → role="tooltip", + // rather than a bare `title` that pointer-events-none would swallow. + const tooltipId = badge.getAttribute('aria-describedby'); + expect(tooltipId).toBeTruthy(); + const tooltip = document.getElementById(tooltipId as string); + expect(tooltip).toHaveAttribute('role', 'tooltip'); + expect(tooltip).toHaveTextContent(/local simulation/i); + }); + test('Retry re-invokes init and recovers to ready on success', async () => { const user = userEvent.setup(); // First attempt fails, second succeeds. diff --git a/app/src/agentworld/pages/WorldSection.tsx b/app/src/agentworld/pages/WorldSection.tsx index c5a8e555a..811bdc1ef 100644 --- a/app/src/agentworld/pages/WorldSection.tsx +++ b/app/src/agentworld/pages/WorldSection.tsx @@ -153,9 +153,38 @@ export default function WorldSection() {

{t( 'agentWorld.world.description', - 'Join tiny.place so your agent can coordinate with other agents — find and post jobs, trade, message, and team up on bounties.' + 'Join tiny.place so your agent can coordinate with other agents on the network — find and post jobs, trade, message, and team up on bounties.' )}

+ {/* The World is a local ambient simulation today: NPCs are seeded + client-side and there is no relay-backed presence / world-state sync + (see GameWorld.updateAgentState + GameWorld.spawnAgents, #4922). This + pill sets expectations until live presence lands. + + The parent card is `pointer-events-none`, so the badge opts back in + with `pointer-events-auto` to stay hoverable/focusable. It uses the + project's self-contained wrapping tooltip pattern (see + SuperContextToggle) rather than the shared , which is + single-line nowrap and can't fit this sentence, and rather than a + bare `title` (unreachable under `pointer-events-none`). */} + + + + {t( + 'agentWorld.world.offlineBadgeTitle', + 'Agents shown here are a local simulation. Live presence and world sync are coming soon.' + )} + +