From 854df03363fc83b2f315bbee1177c52297c1e0c0 Mon Sep 17 00:00:00 2001 From: oxoxDev <164490987+oxoxDev@users.noreply.github.com> Date: Wed, 24 Jun 2026 22:39:37 +0530 Subject: [PATCH] fix(agentworld): reliably boot Tiny Place world renderer + error/retry (#4038) (#4060) --- app/src/agentworld/iso/GameWorld.ts | 92 +++++++++++++-- app/src/agentworld/iso/index.ts | 2 +- .../agentworld/pages/WorldSection.test.tsx | 105 ++++++++++++++++++ app/src/agentworld/pages/WorldSection.tsx | 62 ++++++++++- app/src/lib/i18n/ar.ts | 2 + app/src/lib/i18n/bn.ts | 2 + app/src/lib/i18n/de.ts | 2 + app/src/lib/i18n/en.ts | 2 + app/src/lib/i18n/es.ts | 2 + app/src/lib/i18n/fr.ts | 2 + app/src/lib/i18n/hi.ts | 2 + app/src/lib/i18n/id.ts | 2 + app/src/lib/i18n/it.ts | 2 + app/src/lib/i18n/ko.ts | 2 + app/src/lib/i18n/pl.ts | 2 + app/src/lib/i18n/pt.ts | 2 + app/src/lib/i18n/ru.ts | 2 + app/src/lib/i18n/zh-CN.ts | 2 + 18 files changed, 272 insertions(+), 17 deletions(-) create mode 100644 app/src/agentworld/pages/WorldSection.test.tsx diff --git a/app/src/agentworld/iso/GameWorld.ts b/app/src/agentworld/iso/GameWorld.ts index d819d24d1..ce6f9d0e7 100644 --- a/app/src/agentworld/iso/GameWorld.ts +++ b/app/src/agentworld/iso/GameWorld.ts @@ -8,6 +8,7 @@ * container that is scaled to fill its parent, so the world stays crisp pixel * art at any size. */ +import debugFactory from 'debug'; import { Application, BitmapFont, @@ -35,6 +36,26 @@ import { ROOM_REGISTRY, type RoomEntry } from './rooms'; import { TextureFactory } from './textures'; import { type AgentState, type ChatMessage, TileCode, type WalkNode } from './types'; +const debug = debugFactory('agentworld:gameworld'); + +// The CEF/Chromium renderer used by the desktop shell does not reliably expose a +// WebGPU adapter — `Application.init({ preference: 'webgpu' })` can wait forever +// on adapter acquisition and never resolve, leaving the world stuck on "Booting +// renderer...". WebGL is the dependable path in Chromium/CEF, so we request it +// directly instead of letting Pixi probe (and hang on) WebGPU first. +const RENDERER_PREFERENCE = 'webgl' as const; +// Backstop in case even WebGL context creation wedges: race init against this so +// the caller can surface an error + retry rather than spinning indefinitely. +const INIT_TIMEOUT_MS = 10_000; + +/** Thrown when renderer init rejects or exceeds {@link INIT_TIMEOUT_MS}. */ +export class RendererInitError extends Error { + public constructor(message: string, options?: { cause?: unknown }) { + super(message, options); + this.name = 'RendererInitError'; + } +} + const NAMEPLATE_FONT = 'iso-body'; const BUBBLE_FONT = 'iso-bubble'; const FONT_CHARACTERS: Array | string> = [ @@ -217,22 +238,36 @@ export class GameWorld { public async init(parent: HTMLElement): Promise { if (this.app) { + debug('init skipped: renderer already initialized'); return; } + debug('init start preference=%s timeoutMs=%d', RENDERER_PREFERENCE, INIT_TIMEOUT_MS); TextureSource.defaultOptions.scaleMode = 'nearest'; + // Build the Application locally first; only commit it to `this.app` once init + // fully succeeds, so a timeout/failure leaves GameWorld in its pristine + // pre-init state and `init()` can be retried cleanly. const app = new Application(); - await app.init({ - width: NATIVE_RESOLUTION, - height: NATIVE_RESOLUTION, - antialias: true, - preference: 'webgpu', - powerPreference: 'high-performance', - resolution: globalThis.devicePixelRatio || 1, - autoDensity: true, - backgroundAlpha: 1, - background: 0x0c0f16, - }); + try { + await this.runInitWithTimeout(app); + debug('renderer init resolved preference=%s', RENDERER_PREFERENCE); + } catch (error: unknown) { + // Tear down any partially-created GPU/canvas resources so a retry starts + // from a clean slate (and we don't leak a dangling renderer). + try { + app.destroy?.(true, { children: true, texture: true }); + } catch (destroyError: unknown) { + debug('init cleanup: destroy after failure threw: %s', String(destroyError)); + } + this.app = null; + this.factory = null; + const message = error instanceof Error ? error.message : String(error); + debug('renderer init failed: %s', message); + throw error instanceof RendererInitError + ? error + : new RendererInitError(`renderer init failed: ${message}`, { cause: error }); + } + this.app = app; this.factory = new TextureFactory(app.renderer); @@ -258,6 +293,41 @@ export class GameWorld { app.ticker.add(this.tick); } + /** + * Race `Application.init` against {@link INIT_TIMEOUT_MS}. Resolves when the + * renderer is ready; rejects with a {@link RendererInitError} if init rejects + * or the timeout fires first (the CEF WebGPU-adapter hang, primarily). + */ + private async runInitWithTimeout(app: Application): Promise { + let timer: ReturnType | undefined; + const timeout = new Promise((_resolve, reject) => { + timer = setTimeout(() => { + debug('renderer init timed out after %dms', INIT_TIMEOUT_MS); + reject(new RendererInitError(`renderer init timed out after ${INIT_TIMEOUT_MS}ms`)); + }, INIT_TIMEOUT_MS); + }); + + const initialize = app.init({ + width: NATIVE_RESOLUTION, + height: NATIVE_RESOLUTION, + antialias: true, + preference: RENDERER_PREFERENCE, + powerPreference: 'high-performance', + resolution: globalThis.devicePixelRatio || 1, + autoDensity: true, + backgroundAlpha: 1, + background: 0x0c0f16, + }); + + try { + await Promise.race([initialize, timeout]); + } finally { + if (timer !== undefined) { + clearTimeout(timer); + } + } + } + private installFonts(): void { if (fontsInstalled) { return; diff --git a/app/src/agentworld/iso/index.ts b/app/src/agentworld/iso/index.ts index b1909071f..57e8e2d7d 100644 --- a/app/src/agentworld/iso/index.ts +++ b/app/src/agentworld/iso/index.ts @@ -1,6 +1,6 @@ /** Public surface of the isometric agent world engine. */ -export { GameWorld } from './GameWorld'; +export { GameWorld, RendererInitError } from './GameWorld'; export type { AgentSummary } from './GameWorld'; export { BaseRoom } from './BaseRoom'; export { diff --git a/app/src/agentworld/pages/WorldSection.test.tsx b/app/src/agentworld/pages/WorldSection.test.tsx new file mode 100644 index 000000000..63d236be5 --- /dev/null +++ b/app/src/agentworld/pages/WorldSection.test.tsx @@ -0,0 +1,105 @@ +/** + * Tests for WorldSection — the "Tiny Place" World tab renderer boot UX. + * + * Regression coverage for #4038: the PixiJS renderer init promise could reject + * or hang forever (CEF WebGPU adapter never settling), leaving a permanent + * silent "Booting renderer..." overlay with no error and no retry. These tests + * assert that a rejected init surfaces an error overlay + Retry button, and that + * Retry re-runs init (recovering to the ready state on a subsequent success). + * + * The iso engine is mocked at module level — no real WebGL/WebGPU is touched. + */ +import { render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { beforeEach, describe, expect, test, vi } from 'vitest'; + +import WorldSection from './WorldSection'; + +// Controllable GameWorld test double. `initImpl` is swapped per-test so we can +// drive resolve / reject / never-settle behavior. +let initImpl: (parent: HTMLElement) => Promise; +const initSpy = vi.fn((parent: HTMLElement) => initImpl(parent)); +const setChangeListener = vi.fn(); +const setRoom = vi.fn(); +const spawnAgents = vi.fn(); +const setAutonomous = vi.fn(); +const destroy = vi.fn(); + +vi.mock('../iso', () => { + class FakeGameWorld { + public currentRoomKey = 'outside'; + public init = (parent: HTMLElement): Promise => initSpy(parent); + public setChangeListener = setChangeListener; + public setRoom = setRoom; + public spawnAgents = spawnAgents; + public setAutonomous = setAutonomous; + public destroy = destroy; + } + return { + GameWorld: FakeGameWorld, + ROOM_REGISTRY: [ + { key: 'outside', name: 'World', description: 'A large open plaza.' }, + { key: 'poker', name: 'Poker', description: 'Eight seats around a felt table.' }, + ], + RendererInitError: class RendererInitError extends Error {}, + }; +}); + +beforeEach(() => { + vi.clearAllMocks(); + initImpl = () => Promise.resolve(); +}); + +describe('WorldSection renderer boot', () => { + test('shows booting overlay while init is in flight', () => { + initImpl = () => new Promise(() => {}); // never settles + render(); + expect(screen.getByText(/booting renderer/i)).toBeInTheDocument(); + expect(screen.queryByRole('button', { name: /retry/i })).not.toBeInTheDocument(); + }); + + test('hides booting overlay and wires the world once init resolves', async () => { + initImpl = () => Promise.resolve(); + render(); + await waitFor(() => { + expect(screen.queryByText(/booting renderer/i)).not.toBeInTheDocument(); + }); + expect(setRoom).toHaveBeenCalledWith('outside'); + expect(spawnAgents).toHaveBeenCalled(); + expect(setAutonomous).toHaveBeenCalledWith(true); + expect(screen.queryByRole('button', { name: /retry/i })).not.toBeInTheDocument(); + }); + + test('shows error overlay + Retry when init rejects', async () => { + initImpl = () => Promise.reject(new Error('webgpu adapter hung')); + render(); + await waitFor(() => { + expect(screen.getByText(/could not start the world renderer/i)).toBeInTheDocument(); + }); + expect(screen.getByRole('button', { name: /retry/i })).toBeInTheDocument(); + // The dead-end booting overlay must NOT also be showing. + expect(screen.queryByText(/booting renderer/i)).not.toBeInTheDocument(); + }); + + test('Retry re-invokes init and recovers to ready on success', async () => { + const user = userEvent.setup(); + // First attempt fails, second succeeds. + initImpl = () => Promise.reject(new Error('init failed')); + render(); + await waitFor(() => { + expect(screen.getByRole('button', { name: /retry/i })).toBeInTheDocument(); + }); + expect(initSpy).toHaveBeenCalledTimes(1); + + initImpl = () => Promise.resolve(); + await user.click(screen.getByRole('button', { name: /retry/i })); + + await waitFor(() => { + expect(initSpy).toHaveBeenCalledTimes(2); + }); + await waitFor(() => { + expect(screen.queryByText(/could not start the world renderer/i)).not.toBeInTheDocument(); + }); + expect(setRoom).toHaveBeenCalledWith('outside'); + }); +}); diff --git a/app/src/agentworld/pages/WorldSection.tsx b/app/src/agentworld/pages/WorldSection.tsx index 90436999b..cf0db2a04 100644 --- a/app/src/agentworld/pages/WorldSection.tsx +++ b/app/src/agentworld/pages/WorldSection.tsx @@ -9,6 +9,11 @@ const debug = debugFactory('agentworld:world'); const WORLD_ROOM_KEY = 'outside'; const WORLD_POPULATION = 100; const ROOM_POPULATION = 8; +// Defense-in-depth backstop: even though GameWorld.init() already races its own +// timeout, guard against the (rare) case where the init promise never settles at +// all — flip to an error state so the user always gets a Retry instead of an +// indefinite "Booting renderer..." overlay. +const BOOT_TIMEOUT_MS = 15_000; const populationFor = (key: string): number => key === WORLD_ROOM_KEY ? WORLD_POPULATION : ROOM_POPULATION; @@ -25,7 +30,10 @@ export default function WorldSection() { const containerRef = useRef(null); const worldRef = useRef(null); const [ready, setReady] = useState(false); + const [error, setError] = useState(null); const [roomKey, setRoomKey] = useState(WORLD_ROOM_KEY); + // Bumped by the Retry button to re-run the init effect from scratch. + const [retryNonce, setRetryNonce] = useState(0); useEffect(() => { const container = containerRef.current; @@ -34,14 +42,35 @@ export default function WorldSection() { return; } - debug('mounting pixi world'); + debug('mounting pixi world attempt=%d', retryNonce); + // Reset visible state for this (possibly retried) boot attempt. + setReady(false); + setError(null); const world = new GameWorld(); worldRef.current = world; let disposed = false; + // Component-level backstop in case init() never settles (belt-and-braces + // over GameWorld's own internal timeout). + let bootTimer: ReturnType | undefined = setTimeout(() => { + if (disposed) { + return; + } + debug('renderer boot backstop fired after %dms', BOOT_TIMEOUT_MS); + setError(t('agentWorld.world.initError', 'Could not start the world renderer.')); + }, BOOT_TIMEOUT_MS); + + const clearBootTimer = (): void => { + if (bootTimer !== undefined) { + clearTimeout(bootTimer); + bootTimer = undefined; + } + }; + void world .init(container) .then(() => { + clearBootTimer(); if (disposed) { debug('renderer initialized after unmount; destroying stale world'); world.destroy(); @@ -56,18 +85,31 @@ export default function WorldSection() { setReady(true); debug('renderer ready room=%s population=%d', WORLD_ROOM_KEY, WORLD_POPULATION); }) - .catch((error: unknown) => { - debug('renderer init failed: %s', String(error)); + .catch((initError: unknown) => { + clearBootTimer(); + debug('renderer init failed: %s', String(initError)); + if (disposed) { + return; + } + setError(t('agentWorld.world.initError', 'Could not start the world renderer.')); }); return () => { debug('unmounting pixi world'); disposed = true; + clearBootTimer(); world.setChangeListener(null); world.destroy(); worldRef.current = null; }; - }, []); + // `t` is stable enough for messaging; retryNonce re-runs the whole boot. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [retryNonce]); + + const handleRetry = (): void => { + debug('retry requested'); + setRetryNonce(nonce => nonce + 1); + }; const handleRoom = (key: string): void => { const world = worldRef.current; @@ -88,7 +130,17 @@ export default function WorldSection() { return (
- {ready ? null : ( + {error ? ( +
+

{error}

+ +
+ ) : ready ? null : (
{t('agentWorld.world.booting', 'Booting renderer...')}
diff --git a/app/src/lib/i18n/ar.ts b/app/src/lib/i18n/ar.ts index 35e399fbd..b2de368b5 100644 --- a/app/src/lib/i18n/ar.ts +++ b/app/src/lib/i18n/ar.ts @@ -83,6 +83,8 @@ const messages: TranslationMap = { 'Tiny.Place شبكة اجتماعية لوكلاء الذكاء الاصطناعي. استخدم OpenHuman للتفاعل والعثور على الوظائف ونشرها والتداول والنمو معًا.', 'agentWorld.world': 'العالم', 'agentWorld.world.booting': 'جارٍ تشغيل العارض...', + 'agentWorld.world.initError': 'تعذّر تشغيل عارض العالم.', + 'agentWorld.world.retry': 'إعادة المحاولة', 'agentWorld.world.title': 'Tiny Place', 'agentWorld.world.description': 'انضم إلى tiny.place ليتمكن وكيلك من التنسيق مع الوكلاء الآخرين: العثور على الوظائف ونشرها، والتداول، وتبادل الرسائل، والتعاون في المكافآت.', diff --git a/app/src/lib/i18n/bn.ts b/app/src/lib/i18n/bn.ts index ec1281471..2b634e726 100644 --- a/app/src/lib/i18n/bn.ts +++ b/app/src/lib/i18n/bn.ts @@ -84,6 +84,8 @@ const messages: TranslationMap = { 'Tiny.Place হলো এআই এজেন্টদের জন্য একটি সোশ্যাল নেটওয়ার্ক। যোগাযোগ করতে, কাজ খুঁজতে ও পোস্ট করতে, লেনদেন করতে এবং একসাথে বেড়ে উঠতে OpenHuman ব্যবহার করুন।', 'agentWorld.world': 'বিশ্ব', 'agentWorld.world.booting': 'রেন্ডারার চালু হচ্ছে...', + 'agentWorld.world.initError': 'ওয়ার্ল্ড রেন্ডারার চালু করা যায়নি।', + 'agentWorld.world.retry': 'আবার চেষ্টা করুন', 'agentWorld.world.title': 'Tiny Place', 'agentWorld.world.description': 'tiny.place-এ যোগ দিন যাতে আপনার এজেন্ট অন্য এজেন্টদের সাথে সমন্বয় করতে পারে — কাজ খুঁজে পাওয়া ও পোস্ট করা, লেনদেন, বার্তা পাঠানো এবং বাউন্টিতে একসাথে কাজ করা।', diff --git a/app/src/lib/i18n/de.ts b/app/src/lib/i18n/de.ts index a39991f92..e8cfb2014 100644 --- a/app/src/lib/i18n/de.ts +++ b/app/src/lib/i18n/de.ts @@ -85,6 +85,8 @@ const messages: TranslationMap = { 'Tiny.Place ist ein soziales Netzwerk für KI-Agenten. Nutze OpenHuman, um zu interagieren, Jobs zu finden und zu veröffentlichen, zu handeln und gemeinsam zu wachsen.', 'agentWorld.world': 'Welt', 'agentWorld.world.booting': 'Renderer wird gestartet...', + 'agentWorld.world.initError': 'Der Welt-Renderer konnte nicht gestartet werden.', + 'agentWorld.world.retry': 'Wiederholen', 'agentWorld.world.title': 'Tiny Place', 'agentWorld.world.description': 'Tritt tiny.place bei, damit dein Agent sich mit anderen Agenten abstimmen kann: Jobs finden und ausschreiben, handeln, Nachrichten senden und bei Bounties zusammenarbeiten.', diff --git a/app/src/lib/i18n/en.ts b/app/src/lib/i18n/en.ts index ff1df7c7a..e5366cffd 100644 --- a/app/src/lib/i18n/en.ts +++ b/app/src/lib/i18n/en.ts @@ -30,6 +30,8 @@ const en: TranslationMap = { 'Tiny.Place is a social network for AI agents. Use OpenHuman to interact, find and post jobs, trade, and grow together.', 'agentWorld.world': 'World', 'agentWorld.world.booting': 'Booting renderer...', + 'agentWorld.world.initError': 'Could not start the world renderer.', + 'agentWorld.world.retry': 'Retry', 'agentWorld.world.title': 'Tiny Place', '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.', diff --git a/app/src/lib/i18n/es.ts b/app/src/lib/i18n/es.ts index 41d3d90cd..622923b59 100644 --- a/app/src/lib/i18n/es.ts +++ b/app/src/lib/i18n/es.ts @@ -84,6 +84,8 @@ const messages: TranslationMap = { 'Tiny.Place es una red social para agentes de IA. Usa OpenHuman para interactuar, encontrar y publicar trabajos, comerciar y crecer juntos.', 'agentWorld.world': 'Mundo', 'agentWorld.world.booting': 'Iniciando renderizador...', + 'agentWorld.world.initError': 'No se pudo iniciar el renderizador del mundo.', + 'agentWorld.world.retry': 'Reintentar', 'agentWorld.world.title': 'Tiny Place', 'agentWorld.world.description': 'Únete a tiny.place para que tu agente coordine con otros agentes: encontrar y publicar trabajos, comerciar, enviar mensajes y colaborar en recompensas.', diff --git a/app/src/lib/i18n/fr.ts b/app/src/lib/i18n/fr.ts index c4ccbdd0c..49378b0e0 100644 --- a/app/src/lib/i18n/fr.ts +++ b/app/src/lib/i18n/fr.ts @@ -84,6 +84,8 @@ const messages: TranslationMap = { 'Tiny.Place est un réseau social pour les agents IA. Utilisez OpenHuman pour interagir, trouver et publier des missions, échanger et grandir ensemble.', 'agentWorld.world': 'Monde', 'agentWorld.world.booting': 'Démarrage du moteur de rendu...', + 'agentWorld.world.initError': 'Impossible de démarrer le moteur de rendu du monde.', + 'agentWorld.world.retry': 'Réessayer', 'agentWorld.world.title': 'Tiny Place', 'agentWorld.world.description': 'Rejoignez tiny.place pour que votre agent collabore avec d’autres agents : trouver et publier des missions, échanger, discuter et coopérer sur des primes.', diff --git a/app/src/lib/i18n/hi.ts b/app/src/lib/i18n/hi.ts index be87f6ab9..2943ebb20 100644 --- a/app/src/lib/i18n/hi.ts +++ b/app/src/lib/i18n/hi.ts @@ -84,6 +84,8 @@ const messages: TranslationMap = { 'Tiny.Place एआई एजेंट्स के लिए एक सोशल नेटवर्क है। बातचीत करने, काम खोजने और पोस्ट करने, व्यापार करने और साथ मिलकर आगे बढ़ने के लिए OpenHuman का उपयोग करें।', 'agentWorld.world': 'दुनिया', 'agentWorld.world.booting': 'रेंडरर शुरू हो रहा है...', + 'agentWorld.world.initError': 'वर्ल्ड रेंडरर शुरू नहीं किया जा सका।', + 'agentWorld.world.retry': 'पुनः प्रयास करें', 'agentWorld.world.title': 'Tiny Place', 'agentWorld.world.description': 'tiny.place से जुड़ें ताकि आपका एजेंट दूसरे एजेंट्स के साथ तालमेल कर सके — काम ढूँढना और पोस्ट करना, व्यापार करना, संदेश भेजना और बाउंटी पर मिलकर काम करना।', diff --git a/app/src/lib/i18n/id.ts b/app/src/lib/i18n/id.ts index 49d4b859c..ec89fdea0 100644 --- a/app/src/lib/i18n/id.ts +++ b/app/src/lib/i18n/id.ts @@ -83,6 +83,8 @@ const messages: TranslationMap = { 'Tiny.Place adalah jejaring sosial untuk agen AI. Gunakan OpenHuman untuk berinteraksi, menemukan dan memasang pekerjaan, berdagang, serta tumbuh bersama.', 'agentWorld.world': 'Dunia', 'agentWorld.world.booting': 'Memulai perender...', + 'agentWorld.world.initError': 'Tidak dapat memulai perender dunia.', + 'agentWorld.world.retry': 'Coba lagi', 'agentWorld.world.title': 'Tiny Place', 'agentWorld.world.description': 'Bergabunglah dengan tiny.place agar agen Anda dapat berkoordinasi dengan agen lain: menemukan dan memposting pekerjaan, berdagang, berkirim pesan, dan bekerja sama dalam bounty.', diff --git a/app/src/lib/i18n/it.ts b/app/src/lib/i18n/it.ts index 6f8635f91..d0f37049b 100644 --- a/app/src/lib/i18n/it.ts +++ b/app/src/lib/i18n/it.ts @@ -84,6 +84,8 @@ const messages: TranslationMap = { 'Tiny.Place è un social network per agenti IA. Usa OpenHuman per interagire, trovare e pubblicare lavori, scambiare e crescere insieme.', 'agentWorld.world': 'Mondo', 'agentWorld.world.booting': 'Avvio del renderer...', + 'agentWorld.world.initError': 'Impossibile avviare il renderer del mondo.', + 'agentWorld.world.retry': 'Riprova', 'agentWorld.world.title': 'Tiny Place', 'agentWorld.world.description': 'Unisciti a tiny.place per far coordinare il tuo agente con altri agenti: trovare e pubblicare lavori, scambiare, messaggiare e collaborare alle taglie.', diff --git a/app/src/lib/i18n/ko.ts b/app/src/lib/i18n/ko.ts index 0268d14de..275f69c21 100644 --- a/app/src/lib/i18n/ko.ts +++ b/app/src/lib/i18n/ko.ts @@ -84,6 +84,8 @@ const messages: TranslationMap = { 'Tiny.Place는 AI 에이전트를 위한 소셜 네트워크입니다. OpenHuman을 사용해 소통하고, 일자리를 찾고 올리며, 거래하고 함께 성장하세요.', 'agentWorld.world': '월드', 'agentWorld.world.booting': '렌더러를 시작하는 중...', + 'agentWorld.world.initError': '월드 렌더러를 시작할 수 없습니다.', + 'agentWorld.world.retry': '다시 시도', 'agentWorld.world.title': 'Tiny Place', 'agentWorld.world.description': 'tiny.place에 참여하면 에이전트가 다른 에이전트와 협업할 수 있습니다: 작업을 찾고 게시하고, 거래하고, 메시지를 주고받고, 바운티에 함께 참여하세요.', diff --git a/app/src/lib/i18n/pl.ts b/app/src/lib/i18n/pl.ts index 170da6f90..d271ee349 100644 --- a/app/src/lib/i18n/pl.ts +++ b/app/src/lib/i18n/pl.ts @@ -84,6 +84,8 @@ const messages: TranslationMap = { 'Tiny.Place to sieć społecznościowa dla agentów AI. Używaj OpenHuman, aby wchodzić w interakcje, znajdować i publikować zlecenia, handlować i wspólnie się rozwijać.', 'agentWorld.world': 'Świat', 'agentWorld.world.booting': 'Uruchamianie renderera...', + 'agentWorld.world.initError': 'Nie udało się uruchomić renderera świata.', + 'agentWorld.world.retry': 'Ponów', 'agentWorld.world.title': 'Tiny Place', 'agentWorld.world.description': 'Dołącz do tiny.place, aby Twój agent współpracował z innymi agentami: znajdował i publikował zlecenia, handlował, wysyłał wiadomości i działał przy nagrodach.', diff --git a/app/src/lib/i18n/pt.ts b/app/src/lib/i18n/pt.ts index f46ae3c14..667c7ff1e 100644 --- a/app/src/lib/i18n/pt.ts +++ b/app/src/lib/i18n/pt.ts @@ -84,6 +84,8 @@ const messages: TranslationMap = { 'Tiny.Place é uma rede social para agentes de IA. Use o OpenHuman para interagir, encontrar e publicar trabalhos, negociar e crescer juntos.', 'agentWorld.world': 'Mundo', 'agentWorld.world.booting': 'Iniciando renderizador...', + 'agentWorld.world.initError': 'Não foi possível iniciar o renderizador do mundo.', + 'agentWorld.world.retry': 'Tentar novamente', 'agentWorld.world.title': 'Tiny Place', 'agentWorld.world.description': 'Junte-se ao tiny.place para que seu agente coordene com outros agentes: encontrar e publicar trabalhos, negociar, enviar mensagens e colaborar em recompensas.', diff --git a/app/src/lib/i18n/ru.ts b/app/src/lib/i18n/ru.ts index 944be574e..09a4a010d 100644 --- a/app/src/lib/i18n/ru.ts +++ b/app/src/lib/i18n/ru.ts @@ -84,6 +84,8 @@ const messages: TranslationMap = { 'Tiny.Place — это социальная сеть для ИИ-агентов. Используйте OpenHuman, чтобы взаимодействовать, находить и публиковать задания, торговать и расти вместе.', 'agentWorld.world': 'Мир', 'agentWorld.world.booting': 'Запуск рендерера...', + 'agentWorld.world.initError': 'Не удалось запустить рендерер мира.', + 'agentWorld.world.retry': 'Повторить', 'agentWorld.world.title': 'Tiny Place', 'agentWorld.world.description': 'Присоединяйтесь к tiny.place, чтобы ваш агент взаимодействовал с другими агентами: находил и публиковал задания, торговал, обменивался сообщениями и работал над наградами.', diff --git a/app/src/lib/i18n/zh-CN.ts b/app/src/lib/i18n/zh-CN.ts index 0f5264164..152dd7c85 100644 --- a/app/src/lib/i18n/zh-CN.ts +++ b/app/src/lib/i18n/zh-CN.ts @@ -83,6 +83,8 @@ const messages: TranslationMap = { 'Tiny.Place 是面向 AI 智能体的社交网络。使用 OpenHuman 来互动、查找和发布工作、交易并共同成长。', 'agentWorld.world': '世界', 'agentWorld.world.booting': '正在启动渲染器...', + 'agentWorld.world.initError': '无法启动世界渲染器。', + 'agentWorld.world.retry': '重试', 'agentWorld.world.title': 'Tiny Place', 'agentWorld.world.description': '加入 tiny.place,让你的代理与其他代理协作:查找和发布任务、交易、收发消息以及共同完成悬赏。',