feat: move feedback to sidebar footer, rename World card, fix Clear App Data scoping (#3945)

This commit is contained in:
Steven Enamakel
2026-06-22 18:01:23 -07:00
committed by GitHub
parent b6a6202d46
commit 95d517ca26
27 changed files with 302 additions and 142 deletions
+33 -13
View File
@@ -22,12 +22,15 @@ use crate::reset_reboot_schedule;
/// 3. Shut down the embedded core. `CoreProcessHandle::shutdown` cancels /// 3. Shut down the embedded core. `CoreProcessHandle::shutdown` cancels
/// the cancellation token and awaits the tokio task, which drops the /// the cancellation token and awaits the tokio task, which drops the
/// SQLite pool, log writer, etc. — releasing every Windows file handle. /// SQLite pool, log writer, etc. — releasing every Windows file handle.
/// 4. Remove the three paths (current data dir, default data dir, active /// 4. Remove the **active user's** slice from this process: the current data
/// workspace marker) from this process. Missing entries are non-fatal. /// dir plus the two shared root markers (`active_workspace.toml` and
/// `active_user.toml`). The shared root `~/.openhuman` is left in place so
/// other users' `users/<id>` subtrees survive. Missing entries are
/// non-fatal.
/// 5. Restart the embedded core via `ensure_running`. /// 5. Restart the embedded core via `ensure_running`.
/// ///
/// Returns `Ok(())` only when the core is back up and the directories are /// Returns `Ok(())` only when the core is back up and the active user's data
/// gone (or were already absent). Any step's `Err` short-circuits and /// is gone (or was already absent). Any step's `Err` short-circuits and
/// surfaces to the UI, which already renders the message as a toast. /// surfaces to the UI, which already renders the message as a toast.
#[tauri::command] #[tauri::command]
pub async fn reset_local_data( pub async fn reset_local_data(
@@ -43,10 +46,11 @@ pub async fn reset_local_data(
// that logic here. // that logic here.
let paths = fetch_data_paths().await?; let paths = fetch_data_paths().await?;
log::info!( log::info!(
"[core] reset_local_data: paths resolved current={} default={} marker={}", "[core] reset_local_data: paths resolved current={} default={} workspace_marker={} user_marker={}",
paths.current_openhuman_dir.display(), paths.current_openhuman_dir.display(),
paths.default_openhuman_dir.display(), paths.default_openhuman_dir.display(),
paths.active_workspace_marker_path.display() paths.active_workspace_marker_path.display(),
paths.active_user_marker_path.display()
); );
// ── 2. Acquire the restart lock ───────────────────────────────────── // ── 2. Acquire the restart lock ─────────────────────────────────────
@@ -85,20 +89,25 @@ pub async fn reset_local_data(
// `?` — we must still restart the embedded core in step 5 so the app // `?` — we must still restart the embedded core in step 5 so the app
// doesn't end up with the sidecar dead. The original delete error is // doesn't end up with the sidecar dead. The original delete error is
// surfaced after the restart attempt. // surfaced after the restart attempt.
//
// Scoping (issue: "Clear App Data" wiped every user, not just the active
// one): all user data lives under `~/.openhuman/users/<id>` and the shared
// root `~/.openhuman` holds every user's subtree. So we remove ONLY:
// * the two shared root-level marker files — `active_workspace.toml`
// (workspace pointer) and `active_user.toml` (sign-out), and
// * the active user's own directory (`current_openhuman_dir`).
// We must NOT `remove_dir_all` the shared root `default_openhuman_dir`:
// that destroyed sibling accounts' `users/<other>` data. Removing the
// active-user marker is what now signs the user out (previously this only
// happened as a side effect of nuking the root).
let delete_result: Result<(), String> = async { let delete_result: Result<(), String> = async {
remove_path_if_exists( remove_path_if_exists(
&paths.active_workspace_marker_path, &paths.active_workspace_marker_path,
"active workspace marker", "active workspace marker",
) )
.await?; .await?;
remove_path_if_exists(&paths.active_user_marker_path, "active user marker").await?;
remove_dir_if_exists(&paths.current_openhuman_dir, "current openhuman dir").await?; remove_dir_if_exists(&paths.current_openhuman_dir, "current openhuman dir").await?;
if paths.default_openhuman_dir != paths.current_openhuman_dir {
remove_dir_if_exists(&paths.default_openhuman_dir, "default openhuman dir").await?;
} else {
log::debug!(
"[core] reset_local_data: default dir == current dir; already removed above"
);
}
Ok(()) Ok(())
} }
.await; .await;
@@ -130,8 +139,14 @@ pub async fn reset_local_data(
/// Resolved data paths returned by `config_get_data_paths`. /// Resolved data paths returned by `config_get_data_paths`.
struct ResolvedDataPaths { struct ResolvedDataPaths {
current_openhuman_dir: std::path::PathBuf, current_openhuman_dir: std::path::PathBuf,
/// The shared root `~/.openhuman`. Retained for logging/diagnostics only —
/// the reset must NOT delete it, because it holds every user's
/// `users/<id>` subtree (deleting it wiped sibling accounts' data).
default_openhuman_dir: std::path::PathBuf, default_openhuman_dir: std::path::PathBuf,
active_workspace_marker_path: std::path::PathBuf, active_workspace_marker_path: std::path::PathBuf,
/// `~/.openhuman/active_user.toml` — the shared active-user marker. Removed
/// to sign the current user out so the next launch boots pre-login.
active_user_marker_path: std::path::PathBuf,
} }
fn is_windows_file_lock_raw_os_error(raw_os_error: Option<i32>) -> bool { fn is_windows_file_lock_raw_os_error(raw_os_error: Option<i32>) -> bool {
@@ -306,10 +321,15 @@ async fn fetch_data_paths() -> Result<ResolvedDataPaths, String> {
.get("active_workspace_marker_path") .get("active_workspace_marker_path")
.and_then(|v| v.as_str()) .and_then(|v| v.as_str())
.ok_or_else(|| "config_get_data_paths missing active_workspace_marker_path".to_string())?; .ok_or_else(|| "config_get_data_paths missing active_workspace_marker_path".to_string())?;
let user_marker = inner
.get("active_user_marker_path")
.and_then(|v| v.as_str())
.ok_or_else(|| "config_get_data_paths missing active_user_marker_path".to_string())?;
Ok(ResolvedDataPaths { Ok(ResolvedDataPaths {
current_openhuman_dir: std::path::PathBuf::from(current), current_openhuman_dir: std::path::PathBuf::from(current),
default_openhuman_dir: std::path::PathBuf::from(default), default_openhuman_dir: std::path::PathBuf::from(default),
active_workspace_marker_path: std::path::PathBuf::from(marker), active_workspace_marker_path: std::path::PathBuf::from(marker),
active_user_marker_path: std::path::PathBuf::from(user_marker),
}) })
} }
+2 -2
View File
@@ -96,12 +96,12 @@ export default function WorldSection() {
<div className="pointer-events-none absolute left-3 top-3 z-10 max-w-sm rounded-xl border border-white/15 bg-neutral-950/70 px-4 py-3 shadow-xl backdrop-blur-md"> <div className="pointer-events-none absolute left-3 top-3 z-10 max-w-sm rounded-xl border border-white/15 bg-neutral-950/70 px-4 py-3 shadow-xl backdrop-blur-md">
<h1 className="text-lg font-semibold text-white"> <h1 className="text-lg font-semibold text-white">
{t('agentWorld.world.title', 'Agent World')} {t('agentWorld.world.title', 'Tiny Place')}
</h1> </h1>
<p className="mt-1 text-xs leading-relaxed text-neutral-300"> <p className="mt-1 text-xs leading-relaxed text-neutral-300">
{t( {t(
'agentWorld.world.description', 'agentWorld.world.description',
'Register your agent in tiny.place to get it to start moving around.' 'Join tiny.place so your agent can coordinate with other agents — find and post jobs, trade, message, and team up on bounties.'
)} )}
</p> </p>
</div> </div>
+38 -1
View File
@@ -1,6 +1,10 @@
import { useLocation, useNavigate } from 'react-router-dom';
import { useT } from '../../../lib/i18n/I18nContext'; import { useT } from '../../../lib/i18n/I18nContext';
import { trackEvent } from '../../../services/analytics';
import { APP_VERSION } from '../../../utils/config'; import { APP_VERSION } from '../../../utils/config';
import ConnectionIndicator from '../../ConnectionIndicator'; import ConnectionIndicator from '../../ConnectionIndicator';
import { NavIcon } from './navIcons';
import SidebarAppRail from './SidebarAppRail'; import SidebarAppRail from './SidebarAppRail';
import SidebarHeader from './SidebarHeader'; import SidebarHeader from './SidebarHeader';
import SidebarNav from './SidebarNav'; import SidebarNav from './SidebarNav';
@@ -27,6 +31,22 @@ import { SidebarSlotOutlet } from './SidebarSlot';
*/ */
export default function AppSidebar() { export default function AppSidebar() {
const { t } = useT(); const { t } = useT();
const location = useLocation();
const navigate = useNavigate();
const feedbackActive = location.pathname === '/feedback';
const handleFeedbackClick = () => {
if (!feedbackActive) {
trackEvent('tab_bar_change', {
from_tab: 'unknown',
to_tab: 'feedback',
from_path: location.pathname,
to_path: '/feedback',
});
}
navigate('/feedback');
};
return ( return (
<div className="flex h-full min-h-0 flex-col bg-white dark:bg-neutral-900"> <div className="flex h-full min-h-0 flex-col bg-white dark:bg-neutral-900">
<div className="flex-shrink-0 border-b border-stone-200/70 dark:border-neutral-800/70"> <div className="flex-shrink-0 border-b border-stone-200/70 dark:border-neutral-800/70">
@@ -46,9 +66,26 @@ export default function AppSidebar() {
app rail above its thread list) can order them via Tailwind `order-*`. */} app rail above its thread list) can order them via Tailwind `order-*`. */}
<SidebarSlotOutlet className="flex h-full flex-col" /> <SidebarSlotOutlet className="flex h-full flex-col" />
</div> </div>
{/* Slim feedback row — pinned just above the status bar. Kept thin and
low-profile so it reads as a footer affordance, not a primary nav tab
(it used to live in SidebarNav). */}
<button
type="button"
data-walkthrough="tab-feedback"
onClick={handleFeedbackClick}
title={t('nav.feedback')}
aria-current={feedbackActive ? 'page' : undefined}
className={`group flex flex-shrink-0 items-center justify-center gap-2 border-t border-stone-200/70 px-3 py-1 text-[11px] transition-colors cursor-pointer dark:border-neutral-800/70 ${
feedbackActive
? 'bg-white text-stone-900 font-medium dark:bg-neutral-800 dark:text-neutral-100'
: 'text-stone-500 hover:bg-stone-200/70 hover:text-stone-700 dark:text-neutral-400 dark:hover:bg-neutral-800/60 dark:hover:text-neutral-200'
}`}>
<NavIcon id="feedback" className="h-3.5 w-3.5 flex-shrink-0" />
<span className="min-w-0 truncate">{t('nav.feedback')}</span>
</button>
{/* App-wide footer: connectivity status + build/version, pinned to the {/* App-wide footer: connectivity status + build/version, pinned to the
bottom of the sidebar. */} bottom of the sidebar. */}
<div className="flex flex-shrink-0 items-center justify-center gap-2 border-t border-stone-200 px-2 py-1.5 dark:border-neutral-800"> <div className="flex flex-shrink-0 items-center justify-center gap-2 border-t border-stone-200 px-2 py-0.5 dark:border-neutral-800">
<ConnectionIndicator /> <ConnectionIndicator />
&middot; &middot;
<span className="text-[10px] text-stone-400 dark:text-neutral-500"> <span className="text-[10px] text-stone-400 dark:text-neutral-500">
+6 -6
View File
@@ -3,8 +3,8 @@ import { describe, expect, it } from 'vitest';
import { AVATAR_MENU_ITEMS, NAV_TABS } from '../navConfig'; import { AVATAR_MENU_ITEMS, NAV_TABS } from '../navConfig';
describe('NAV_TABS', () => { describe('NAV_TABS', () => {
it('has exactly 6 entries', () => { it('has exactly 5 entries', () => {
expect(NAV_TABS).toHaveLength(6); expect(NAV_TABS).toHaveLength(5);
}); });
it('has the correct ids in order', () => { it('has the correct ids in order', () => {
@@ -14,7 +14,6 @@ describe('NAV_TABS', () => {
'brain', 'brain',
'agent-world', 'agent-world',
'connections', 'connections',
'feedback',
]); ]);
}); });
@@ -25,7 +24,6 @@ describe('NAV_TABS', () => {
'/brain', '/brain',
'/agent-world', '/agent-world',
'/connections', '/connections',
'/feedback',
]); ]);
}); });
@@ -36,7 +34,6 @@ describe('NAV_TABS', () => {
'nav.brain', 'nav.brain',
'nav.agentWorld', 'nav.agentWorld',
'nav.connections', 'nav.connections',
'nav.feedback',
]); ]);
}); });
@@ -47,7 +44,6 @@ describe('NAV_TABS', () => {
'tab-brain', 'tab-brain',
'tab-agent-world', 'tab-agent-world',
'tab-connections', 'tab-connections',
'tab-feedback',
]); ]);
}); });
@@ -56,6 +52,10 @@ describe('NAV_TABS', () => {
expect(NAV_TABS.find(t => t.id === 'settings')).toBeUndefined(); expect(NAV_TABS.find(t => t.id === 'settings')).toBeUndefined();
}); });
it('no longer contains a feedback tab (moved to the sidebar footer row)', () => {
expect(NAV_TABS.find(t => t.id === 'feedback')).toBeUndefined();
});
it('does not contain an activity tab', () => { it('does not contain an activity tab', () => {
expect(NAV_TABS.find(t => t.id === 'activity')).toBeUndefined(); expect(NAV_TABS.find(t => t.id === 'activity')).toBeUndefined();
}); });
+2 -2
View File
@@ -48,9 +48,9 @@ export const NAV_TABS: NavTab[] = [
path: '/connections', path: '/connections',
walkthroughAttr: 'tab-connections', walkthroughAttr: 'tab-connections',
}, },
{ id: 'feedback', labelKey: 'nav.feedback', path: '/feedback', walkthroughAttr: 'tab-feedback' },
// Settings is reached via the gear icon in the sidebar header, so it no // Settings is reached via the gear icon in the sidebar header, so it no
// longer has its own primary nav tab. // longer has its own primary nav tab. Feedback lives in a slim footer row
// pinned just above the status bar (see AppSidebar).
]; ];
// ── Avatar / account menu ───────────────────────────────────────────────────── // ── Avatar / account menu ─────────────────────────────────────────────────────
+5 -4
View File
@@ -4,7 +4,7 @@ import type { TranslationMap } from './types';
// English-identical values fall back to English via I18nContext.resolveEn(). // English-identical values fall back to English via I18nContext.resolveEn().
const messages: TranslationMap = { const messages: TranslationMap = {
'conversations.backgroundTasks.title': 'Background tasks', 'conversations.backgroundTasks.title': 'Background tasks',
'nav.feedback': 'الملاحظات', 'nav.feedback': 'شارك ملاحظاتك',
'feedback.board': 'لوحة الملاحظات', 'feedback.board': 'لوحة الملاحظات',
'feedback.empty': 'لا توجد ملاحظات بعد. كن أول من يشارك فكرة.', 'feedback.empty': 'لا توجد ملاحظات بعد. كن أول من يشارك فكرة.',
'feedback.loadMore': 'تحميل المزيد', 'feedback.loadMore': 'تحميل المزيد',
@@ -77,14 +77,15 @@ const messages: TranslationMap = {
'nav.noAgentProfiles': 'لم يتم العثور على ملفات وكلاء', 'nav.noAgentProfiles': 'لم يتم العثور على ملفات وكلاء',
'nav.activity': 'النشاط', 'nav.activity': 'النشاط',
'nav.brain': 'الدماغ', 'nav.brain': 'الدماغ',
'nav.agentWorld': 'Tiny.Place', 'nav.agentWorld': 'Tiny Place',
'nav.wallet': 'المحفظة', 'nav.wallet': 'المحفظة',
'agentWorld.description': 'agentWorld.description':
'Tiny.Place شبكة اجتماعية لوكلاء الذكاء الاصطناعي. استخدم OpenHuman للتفاعل والعثور على الوظائف ونشرها والتداول والنمو معًا.', 'Tiny.Place شبكة اجتماعية لوكلاء الذكاء الاصطناعي. استخدم OpenHuman للتفاعل والعثور على الوظائف ونشرها والتداول والنمو معًا.',
'agentWorld.world': 'العالم', 'agentWorld.world': 'العالم',
'agentWorld.world.booting': 'جارٍ تشغيل العارض...', 'agentWorld.world.booting': 'جارٍ تشغيل العارض...',
'agentWorld.world.title': 'عالم الوكلاء', 'agentWorld.world.title': 'Tiny Place',
'agentWorld.world.description': 'سجّل وكيلك في tiny.place ليبدأ بالحركة داخل العالم.', 'agentWorld.world.description':
'انضم إلى tiny.place ليتمكن وكيلك من التنسيق مع الوكلاء الآخرين: العثور على الوظائف ونشرها، والتداول، وتبادل الرسائل، والتعاون في المكافآت.',
'agentWorld.world.room': 'الغرفة', 'agentWorld.world.room': 'الغرفة',
'agentWorld.world.rooms.poker.name': 'بوكر', 'agentWorld.world.rooms.poker.name': 'بوكر',
'agentWorld.world.rooms.poker.description': 'ثمانية مقاعد حول طاولة مكسوة باللباد.', 'agentWorld.world.rooms.poker.description': 'ثمانية مقاعد حول طاولة مكسوة باللباد.',
+4 -4
View File
@@ -4,7 +4,7 @@ import type { TranslationMap } from './types';
// English-identical values fall back to English via I18nContext.resolveEn(). // English-identical values fall back to English via I18nContext.resolveEn().
const messages: TranslationMap = { const messages: TranslationMap = {
'conversations.backgroundTasks.title': 'Background tasks', 'conversations.backgroundTasks.title': 'Background tasks',
'nav.feedback': 'মতামত', 'nav.feedback': 'মতামত দিন',
'feedback.board': 'মতামত বোর্ড', 'feedback.board': 'মতামত বোর্ড',
'feedback.empty': 'এখনও কোনো মতামত নেই। প্রথম হয়ে একটি আইডিয়া শেয়ার করুন।', 'feedback.empty': 'এখনও কোনো মতামত নেই। প্রথম হয়ে একটি আইডিয়া শেয়ার করুন।',
'feedback.loadMore': 'আরও দেখুন', 'feedback.loadMore': 'আরও দেখুন',
@@ -78,15 +78,15 @@ const messages: TranslationMap = {
'nav.noAgentProfiles': 'কোনো এজেন্ট প্রোফাইল পাওয়া যায়নি', 'nav.noAgentProfiles': 'কোনো এজেন্ট প্রোফাইল পাওয়া যায়নি',
'nav.activity': 'কার্যকলাপ', 'nav.activity': 'কার্যকলাপ',
'nav.brain': 'ব্রেইন', 'nav.brain': 'ব্রেইন',
'nav.agentWorld': 'Tiny.Place', 'nav.agentWorld': 'Tiny Place',
'nav.wallet': 'ওয়ালেট', 'nav.wallet': 'ওয়ালেট',
'agentWorld.description': 'agentWorld.description':
'Tiny.Place হলো এআই এজেন্টদের জন্য একটি সোশ্যাল নেটওয়ার্ক। যোগাযোগ করতে, কাজ খুঁজতে ও পোস্ট করতে, লেনদেন করতে এবং একসাথে বেড়ে উঠতে OpenHuman ব্যবহার করুন।', 'Tiny.Place হলো এআই এজেন্টদের জন্য একটি সোশ্যাল নেটওয়ার্ক। যোগাযোগ করতে, কাজ খুঁজতে ও পোস্ট করতে, লেনদেন করতে এবং একসাথে বেড়ে উঠতে OpenHuman ব্যবহার করুন।',
'agentWorld.world': 'বিশ্ব', 'agentWorld.world': 'বিশ্ব',
'agentWorld.world.booting': 'রেন্ডারার চালু হচ্ছে...', 'agentWorld.world.booting': 'রেন্ডারার চালু হচ্ছে...',
'agentWorld.world.title': 'এজেন্ট বিশ্ব', 'agentWorld.world.title': 'Tiny Place',
'agentWorld.world.description': 'agentWorld.world.description':
'আপনার এজেন্টকে tiny.place-এ নিবন্ধন করুন যাতে সে চলাফেরা শুরু কর।', 'tiny.place-এ যোগ দিন যাতে আপনার এজেন্ট অন্য এজেন্টদের সাথে সমন্বয় করতে পারে — কাজ খুঁজে পাওয়া ও পোস্ট করা, লেনদেন, বার্তা পাঠানো এবং বাউন্টিতে একসাথে কাজ কর।',
'agentWorld.world.room': 'রুম', 'agentWorld.world.room': 'রুম',
'agentWorld.world.rooms.poker.name': 'পোকার', 'agentWorld.world.rooms.poker.name': 'পোকার',
'agentWorld.world.rooms.poker.description': 'ফেল্ট টেবিলের চারপাশে আটটি আসন।', 'agentWorld.world.rooms.poker.description': 'ফেল্ট টেবিলের চারপাশে আটটি আসন।',
+4 -4
View File
@@ -4,7 +4,7 @@ import type { TranslationMap } from './types';
// English-identical values fall back to English via I18nContext.resolveEn(). // English-identical values fall back to English via I18nContext.resolveEn().
const messages: TranslationMap = { const messages: TranslationMap = {
'conversations.backgroundTasks.title': 'Background tasks', 'conversations.backgroundTasks.title': 'Background tasks',
'nav.feedback': 'Feedback', 'nav.feedback': 'Feedback geben',
'feedback.board': 'Feedback-Board', 'feedback.board': 'Feedback-Board',
'feedback.empty': 'Noch kein Feedback. Teile als Erste:r eine Idee.', 'feedback.empty': 'Noch kein Feedback. Teile als Erste:r eine Idee.',
'feedback.loadMore': 'Mehr laden', 'feedback.loadMore': 'Mehr laden',
@@ -79,15 +79,15 @@ const messages: TranslationMap = {
'nav.noAgentProfiles': 'Keine Agentenprofile gefunden', 'nav.noAgentProfiles': 'Keine Agentenprofile gefunden',
'nav.activity': 'Aktivität', 'nav.activity': 'Aktivität',
'nav.brain': 'Gehirn', 'nav.brain': 'Gehirn',
'nav.agentWorld': 'Tiny.Place', 'nav.agentWorld': 'Tiny Place',
'nav.wallet': 'Wallet', 'nav.wallet': 'Wallet',
'agentWorld.description': 'agentWorld.description':
'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.', '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': 'Welt',
'agentWorld.world.booting': 'Renderer wird gestartet...', 'agentWorld.world.booting': 'Renderer wird gestartet...',
'agentWorld.world.title': 'Agentenwelt', 'agentWorld.world.title': 'Tiny Place',
'agentWorld.world.description': 'agentWorld.world.description':
'Registriere deinen Agenten bei tiny.place, damit er sich bewegen kann.', 'Tritt tiny.place bei, damit dein Agent sich mit anderen Agenten abstimmen kann: Jobs finden und ausschreiben, handeln, Nachrichten senden und bei Bounties zusammenarbeiten.',
'agentWorld.world.room': 'Raum', 'agentWorld.world.room': 'Raum',
'agentWorld.world.rooms.poker.name': 'Poker', 'agentWorld.world.rooms.poker.name': 'Poker',
'agentWorld.world.rooms.poker.description': 'Acht Sitze rund um einen Filztisch.', 'agentWorld.world.rooms.poker.description': 'Acht Sitze rund um einen Filztisch.',
+4 -4
View File
@@ -23,16 +23,16 @@ const en: TranslationMap = {
'nav.noAgentProfiles': 'No agent profiles found', 'nav.noAgentProfiles': 'No agent profiles found',
'nav.activity': 'Activity', 'nav.activity': 'Activity',
'nav.brain': 'Brain', 'nav.brain': 'Brain',
'nav.agentWorld': 'Tiny.Place', 'nav.agentWorld': 'Tiny Place',
'nav.wallet': 'Wallet', 'nav.wallet': 'Wallet',
// Agent World section sub-navigation labels // Agent World section sub-navigation labels
'agentWorld.description': 'agentWorld.description':
'Tiny.Place is a social network for AI agents. Use OpenHuman to interact, find and post jobs, trade, and grow together.', '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': 'World',
'agentWorld.world.booting': 'Booting renderer...', 'agentWorld.world.booting': 'Booting renderer...',
'agentWorld.world.title': 'Agent World', 'agentWorld.world.title': 'Tiny Place',
'agentWorld.world.description': 'agentWorld.world.description':
'Register your agent in tiny.place to get it to start moving around.', 'Join tiny.place so your agent can coordinate with other agents — find and post jobs, trade, message, and team up on bounties.',
'agentWorld.world.room': 'Room', 'agentWorld.world.room': 'Room',
'agentWorld.world.rooms.poker.name': 'Poker', 'agentWorld.world.rooms.poker.name': 'Poker',
'agentWorld.world.rooms.poker.description': 'Eight seats around a felt table.', 'agentWorld.world.rooms.poker.description': 'Eight seats around a felt table.',
@@ -77,7 +77,7 @@ const en: TranslationMap = {
'nav.avatarMenu.rewards': 'Rewards', 'nav.avatarMenu.rewards': 'Rewards',
'nav.avatarMenu.invites': 'Invite a friend', 'nav.avatarMenu.invites': 'Invite a friend',
'nav.avatarMenu.wallet': 'Wallet', 'nav.avatarMenu.wallet': 'Wallet',
'nav.feedback': 'Feedback', 'nav.feedback': 'Share Feedback',
// Brain — full-page memory knowledge-graph surface // Brain — full-page memory knowledge-graph surface
'brain.subtitle': 'Your knowledge graph, memory sources, and controls.', 'brain.subtitle': 'Your knowledge graph, memory sources, and controls.',
+5 -4
View File
@@ -4,7 +4,7 @@ import type { TranslationMap } from './types';
// English-identical values fall back to English via I18nContext.resolveEn(). // English-identical values fall back to English via I18nContext.resolveEn().
const messages: TranslationMap = { const messages: TranslationMap = {
'conversations.backgroundTasks.title': 'Background tasks', 'conversations.backgroundTasks.title': 'Background tasks',
'nav.feedback': 'Opiniones', 'nav.feedback': 'Compartir opiniones',
'feedback.board': 'Tablero de opiniones', 'feedback.board': 'Tablero de opiniones',
'feedback.empty': 'Aún no hay opiniones. Sé el primero en compartir una idea.', 'feedback.empty': 'Aún no hay opiniones. Sé el primero en compartir una idea.',
'feedback.loadMore': 'Cargar más', 'feedback.loadMore': 'Cargar más',
@@ -78,14 +78,15 @@ const messages: TranslationMap = {
'nav.noAgentProfiles': 'No se encontraron perfiles de agente', 'nav.noAgentProfiles': 'No se encontraron perfiles de agente',
'nav.activity': 'Actividad', 'nav.activity': 'Actividad',
'nav.brain': 'Cerebro', 'nav.brain': 'Cerebro',
'nav.agentWorld': 'Tiny.Place', 'nav.agentWorld': 'Tiny Place',
'nav.wallet': 'Cartera', 'nav.wallet': 'Cartera',
'agentWorld.description': 'agentWorld.description':
'Tiny.Place es una red social para agentes de IA. Usa OpenHuman para interactuar, encontrar y publicar trabajos, comerciar y crecer juntos.', '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': 'Mundo',
'agentWorld.world.booting': 'Iniciando renderizador...', 'agentWorld.world.booting': 'Iniciando renderizador...',
'agentWorld.world.title': 'Mundo de agentes', 'agentWorld.world.title': 'Tiny Place',
'agentWorld.world.description': 'Registra tu agente en tiny.place para que empiece a moverse.', '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.',
'agentWorld.world.room': 'Sala', 'agentWorld.world.room': 'Sala',
'agentWorld.world.rooms.poker.name': 'Póker', 'agentWorld.world.rooms.poker.name': 'Póker',
'agentWorld.world.rooms.poker.description': 'Ocho asientos alrededor de una mesa de fieltro.', 'agentWorld.world.rooms.poker.description': 'Ocho asientos alrededor de una mesa de fieltro.',
+4 -4
View File
@@ -4,7 +4,7 @@ import type { TranslationMap } from './types';
// English-identical values fall back to English via I18nContext.resolveEn(). // English-identical values fall back to English via I18nContext.resolveEn().
const messages: TranslationMap = { const messages: TranslationMap = {
'conversations.backgroundTasks.title': 'Background tasks', 'conversations.backgroundTasks.title': 'Background tasks',
'nav.feedback': 'Suggestions', 'nav.feedback': 'Donner mon avis',
'feedback.board': 'Tableau des suggestions', 'feedback.board': 'Tableau des suggestions',
'feedback.empty': 'Aucune suggestion pour le moment. Soyez le premier à partager une idée.', 'feedback.empty': 'Aucune suggestion pour le moment. Soyez le premier à partager une idée.',
'feedback.loadMore': 'Charger plus', 'feedback.loadMore': 'Charger plus',
@@ -78,15 +78,15 @@ const messages: TranslationMap = {
'nav.noAgentProfiles': "Aucun profil d'agent trouvé", 'nav.noAgentProfiles': "Aucun profil d'agent trouvé",
'nav.activity': 'Activité', 'nav.activity': 'Activité',
'nav.brain': 'Cerveau', 'nav.brain': 'Cerveau',
'nav.agentWorld': 'Tiny.Place', 'nav.agentWorld': 'Tiny Place',
'nav.wallet': 'Portefeuille', 'nav.wallet': 'Portefeuille',
'agentWorld.description': 'agentWorld.description':
'Tiny.Place est un réseau social pour les agents IA. Utilisez OpenHuman pour interagir, trouver et publier des missions, échanger et grandir ensemble.', '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': 'Monde',
'agentWorld.world.booting': 'Démarrage du moteur de rendu...', 'agentWorld.world.booting': 'Démarrage du moteur de rendu...',
'agentWorld.world.title': 'Monde des agents', 'agentWorld.world.title': 'Tiny Place',
'agentWorld.world.description': 'agentWorld.world.description':
'Enregistrez votre agent sur tiny.place pour le faire commencer à bouger.', 'Rejoignez tiny.place pour que votre agent collabore avec dautres agents : trouver et publier des missions, échanger, discuter et coopérer sur des primes.',
'agentWorld.world.room': 'Salle', 'agentWorld.world.room': 'Salle',
'agentWorld.world.rooms.poker.name': 'Poker', 'agentWorld.world.rooms.poker.name': 'Poker',
'agentWorld.world.rooms.poker.description': 'Huit sièges autour dune table en feutre.', 'agentWorld.world.rooms.poker.description': 'Huit sièges autour dune table en feutre.',
+5 -4
View File
@@ -4,7 +4,7 @@ import type { TranslationMap } from './types';
// English-identical values fall back to English via I18nContext.resolveEn(). // English-identical values fall back to English via I18nContext.resolveEn().
const messages: TranslationMap = { const messages: TranslationMap = {
'conversations.backgroundTasks.title': 'Background tasks', 'conversations.backgroundTasks.title': 'Background tasks',
'nav.feedback': 'फ़ीडबैक', 'nav.feedback': 'फ़ीडबैक साझा करें',
'feedback.board': 'फ़ीडबैक बोर्ड', 'feedback.board': 'फ़ीडबैक बोर्ड',
'feedback.empty': 'अभी तक कोई फ़ीडबैक नहीं है। कोई विचार साझा करने वाले पहले व्यक्ति बनें।', 'feedback.empty': 'अभी तक कोई फ़ीडबैक नहीं है। कोई विचार साझा करने वाले पहले व्यक्ति बनें।',
'feedback.loadMore': 'और लोड करें', 'feedback.loadMore': 'और लोड करें',
@@ -78,14 +78,15 @@ const messages: TranslationMap = {
'nav.noAgentProfiles': 'कोई एजेंट प्रोफाइल नहीं मिला', 'nav.noAgentProfiles': 'कोई एजेंट प्रोफाइल नहीं मिला',
'nav.activity': 'गतिविधि', 'nav.activity': 'गतिविधि',
'nav.brain': 'ब्रेन', 'nav.brain': 'ब्रेन',
'nav.agentWorld': 'Tiny.Place', 'nav.agentWorld': 'Tiny Place',
'nav.wallet': 'वॉलेट', 'nav.wallet': 'वॉलेट',
'agentWorld.description': 'agentWorld.description':
'Tiny.Place एआई एजेंट्स के लिए एक सोशल नेटवर्क है। बातचीत करने, काम खोजने और पोस्ट करने, व्यापार करने और साथ मिलकर आगे बढ़ने के लिए OpenHuman का उपयोग करें।', 'Tiny.Place एआई एजेंट्स के लिए एक सोशल नेटवर्क है। बातचीत करने, काम खोजने और पोस्ट करने, व्यापार करने और साथ मिलकर आगे बढ़ने के लिए OpenHuman का उपयोग करें।',
'agentWorld.world': 'दुनिया', 'agentWorld.world': 'दुनिया',
'agentWorld.world.booting': 'रेंडरर शुरू हो रहा है...', 'agentWorld.world.booting': 'रेंडरर शुरू हो रहा है...',
'agentWorld.world.title': 'एजेंट दुनिया', 'agentWorld.world.title': 'Tiny Place',
'agentWorld.world.description': 'अपने एजेंट को tiny.place पर रजिस्टर करें ताकि वह चलना शुरू करे।', 'agentWorld.world.description':
'tiny.place से जुड़ें ताकि आपका एजेंट दूसरे एजेंट्स के साथ तालमेल कर सके — काम ढूँढना और पोस्ट करना, व्यापार करना, संदेश भेजना और बाउंटी पर मिलकर काम करना।',
'agentWorld.world.room': 'कमरा', 'agentWorld.world.room': 'कमरा',
'agentWorld.world.rooms.poker.name': 'पोकर', 'agentWorld.world.rooms.poker.name': 'पोकर',
'agentWorld.world.rooms.poker.description': 'फेल्ट टेबल के चारों ओर आठ सीटें।', 'agentWorld.world.rooms.poker.description': 'फेल्ट टेबल के चारों ओर आठ सीटें।',
+5 -4
View File
@@ -4,7 +4,7 @@ import type { TranslationMap } from './types';
// English-identical values fall back to English via I18nContext.resolveEn(). // English-identical values fall back to English via I18nContext.resolveEn().
const messages: TranslationMap = { const messages: TranslationMap = {
'conversations.backgroundTasks.title': 'Background tasks', 'conversations.backgroundTasks.title': 'Background tasks',
'nav.feedback': 'Masukan', 'nav.feedback': 'Bagikan masukan',
'feedback.board': 'Papan masukan', 'feedback.board': 'Papan masukan',
'feedback.empty': 'Belum ada masukan. Jadilah yang pertama membagikan ide.', 'feedback.empty': 'Belum ada masukan. Jadilah yang pertama membagikan ide.',
'feedback.loadMore': 'Muat lebih banyak', 'feedback.loadMore': 'Muat lebih banyak',
@@ -77,14 +77,15 @@ const messages: TranslationMap = {
'nav.noAgentProfiles': 'Profil agen tidak ditemukan', 'nav.noAgentProfiles': 'Profil agen tidak ditemukan',
'nav.activity': 'Aktivitas', 'nav.activity': 'Aktivitas',
'nav.brain': 'Otak', 'nav.brain': 'Otak',
'nav.agentWorld': 'Tiny.Place', 'nav.agentWorld': 'Tiny Place',
'nav.wallet': 'Dompet', 'nav.wallet': 'Dompet',
'agentWorld.description': 'agentWorld.description':
'Tiny.Place adalah jejaring sosial untuk agen AI. Gunakan OpenHuman untuk berinteraksi, menemukan dan memasang pekerjaan, berdagang, serta tumbuh bersama.', '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': 'Dunia',
'agentWorld.world.booting': 'Memulai perender...', 'agentWorld.world.booting': 'Memulai perender...',
'agentWorld.world.title': 'Dunia agen', 'agentWorld.world.title': 'Tiny Place',
'agentWorld.world.description': 'Daftarkan agen Anda di tiny.place agar mulai bergerak.', '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.',
'agentWorld.world.room': 'Ruang', 'agentWorld.world.room': 'Ruang',
'agentWorld.world.rooms.poker.name': 'Poker', 'agentWorld.world.rooms.poker.name': 'Poker',
'agentWorld.world.rooms.poker.description': 'Delapan kursi mengelilingi meja felt.', 'agentWorld.world.rooms.poker.description': 'Delapan kursi mengelilingi meja felt.',
+4 -4
View File
@@ -4,7 +4,7 @@ import type { TranslationMap } from './types';
// English-identical values fall back to English via I18nContext.resolveEn(). // English-identical values fall back to English via I18nContext.resolveEn().
const messages: TranslationMap = { const messages: TranslationMap = {
'conversations.backgroundTasks.title': 'Background tasks', 'conversations.backgroundTasks.title': 'Background tasks',
'nav.feedback': 'Feedback', 'nav.feedback': 'Condividi feedback',
'feedback.board': 'Bacheca dei feedback', 'feedback.board': 'Bacheca dei feedback',
'feedback.empty': 'Ancora nessun feedback. Sii il primo a condividere unidea.', 'feedback.empty': 'Ancora nessun feedback. Sii il primo a condividere unidea.',
'feedback.loadMore': 'Carica altro', 'feedback.loadMore': 'Carica altro',
@@ -78,15 +78,15 @@ const messages: TranslationMap = {
'nav.noAgentProfiles': 'Nessun profilo agente trovato', 'nav.noAgentProfiles': 'Nessun profilo agente trovato',
'nav.activity': 'Attività', 'nav.activity': 'Attività',
'nav.brain': 'Cervello', 'nav.brain': 'Cervello',
'nav.agentWorld': 'Tiny.Place', 'nav.agentWorld': 'Tiny Place',
'nav.wallet': 'Portafoglio', 'nav.wallet': 'Portafoglio',
'agentWorld.description': 'agentWorld.description':
'Tiny.Place è un social network per agenti IA. Usa OpenHuman per interagire, trovare e pubblicare lavori, scambiare e crescere insieme.', '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': 'Mondo',
'agentWorld.world.booting': 'Avvio del renderer...', 'agentWorld.world.booting': 'Avvio del renderer...',
'agentWorld.world.title': 'Mondo degli agenti', 'agentWorld.world.title': 'Tiny Place',
'agentWorld.world.description': 'agentWorld.world.description':
'Registra il tuo agente su tiny.place per farlo iniziare a muoversi.', 'Unisciti a tiny.place per far coordinare il tuo agente con altri agenti: trovare e pubblicare lavori, scambiare, messaggiare e collaborare alle taglie.',
'agentWorld.world.room': 'Stanza', 'agentWorld.world.room': 'Stanza',
'agentWorld.world.rooms.poker.name': 'Poker', 'agentWorld.world.rooms.poker.name': 'Poker',
'agentWorld.world.rooms.poker.description': 'Otto posti attorno a un tavolo in feltro.', 'agentWorld.world.rooms.poker.description': 'Otto posti attorno a un tavolo in feltro.',
+4 -4
View File
@@ -4,7 +4,7 @@ import type { TranslationMap } from './types';
// English-identical values fall back to English via I18nContext.resolveEn(). // English-identical values fall back to English via I18nContext.resolveEn().
const messages: TranslationMap = { const messages: TranslationMap = {
'conversations.backgroundTasks.title': 'Background tasks', 'conversations.backgroundTasks.title': 'Background tasks',
'nav.feedback': '피드백', 'nav.feedback': '피드백 보내기',
'feedback.board': '피드백 보드', 'feedback.board': '피드백 보드',
'feedback.empty': '아직 피드백이 없습니다. 가장 먼저 아이디어를 공유해 보세요.', 'feedback.empty': '아직 피드백이 없습니다. 가장 먼저 아이디어를 공유해 보세요.',
'feedback.loadMore': '더 불러오기', 'feedback.loadMore': '더 불러오기',
@@ -78,15 +78,15 @@ const messages: TranslationMap = {
'nav.noAgentProfiles': '에이전트 프로필을 찾을 수 없습니다', 'nav.noAgentProfiles': '에이전트 프로필을 찾을 수 없습니다',
'nav.activity': '활동', 'nav.activity': '활동',
'nav.brain': '브레인', 'nav.brain': '브레인',
'nav.agentWorld': 'Tiny.Place', 'nav.agentWorld': 'Tiny Place',
'nav.wallet': '지갑', 'nav.wallet': '지갑',
'agentWorld.description': 'agentWorld.description':
'Tiny.Place는 AI 에이전트를 위한 소셜 네트워크입니다. OpenHuman을 사용해 소통하고, 일자리를 찾고 올리며, 거래하고 함께 성장하세요.', 'Tiny.Place는 AI 에이전트를 위한 소셜 네트워크입니다. OpenHuman을 사용해 소통하고, 일자리를 찾고 올리며, 거래하고 함께 성장하세요.',
'agentWorld.world': '월드', 'agentWorld.world': '월드',
'agentWorld.world.booting': '렌더러를 시작하는 중...', 'agentWorld.world.booting': '렌더러를 시작하는 중...',
'agentWorld.world.title': '에이전트 월드', 'agentWorld.world.title': 'Tiny Place',
'agentWorld.world.description': 'agentWorld.world.description':
'tiny.place에 에이전트를 등록하면 월드 안에서 움직이기 시작합니다.', 'tiny.place에 참여하면 에이전트가 다른 에이전트와 협업할 수 있습니다: 작업을 찾고 게시하고, 거래하고, 메시지를 주고받고, 바운티에 함께 참여하세요.',
'agentWorld.world.room': '방', 'agentWorld.world.room': '방',
'agentWorld.world.rooms.poker.name': '포커', 'agentWorld.world.rooms.poker.name': '포커',
'agentWorld.world.rooms.poker.description': '펠트 테이블을 둘러싼 여덟 좌석.', 'agentWorld.world.rooms.poker.description': '펠트 테이블을 둘러싼 여덟 좌석.',
+4 -4
View File
@@ -4,7 +4,7 @@ import type { TranslationMap } from './types';
// English-identical values fall back to English via I18nContext.resolveEn(). // English-identical values fall back to English via I18nContext.resolveEn().
const messages: TranslationMap = { const messages: TranslationMap = {
'conversations.backgroundTasks.title': 'Background tasks', 'conversations.backgroundTasks.title': 'Background tasks',
'nav.feedback': 'Opinie', 'nav.feedback': 'Podziel się opinią',
'feedback.board': 'Tablica opinii', 'feedback.board': 'Tablica opinii',
'feedback.empty': 'feedback.empty':
'Nie ma jeszcze żadnych opinii. Bądź pierwszą osobą, która podzieli się pomysłem.', 'Nie ma jeszcze żadnych opinii. Bądź pierwszą osobą, która podzieli się pomysłem.',
@@ -78,15 +78,15 @@ const messages: TranslationMap = {
'nav.noAgentProfiles': 'Nie znaleziono profili agentów', 'nav.noAgentProfiles': 'Nie znaleziono profili agentów',
'nav.activity': 'Aktywność', 'nav.activity': 'Aktywność',
'nav.brain': 'Mózg', 'nav.brain': 'Mózg',
'nav.agentWorld': 'Tiny.Place', 'nav.agentWorld': 'Tiny Place',
'nav.wallet': 'Portfel', 'nav.wallet': 'Portfel',
'agentWorld.description': 'agentWorld.description':
'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ć.', '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': 'Świat',
'agentWorld.world.booting': 'Uruchamianie renderera...', 'agentWorld.world.booting': 'Uruchamianie renderera...',
'agentWorld.world.title': 'Świat agentów', 'agentWorld.world.title': 'Tiny Place',
'agentWorld.world.description': 'agentWorld.world.description':
'Zarejestruj swojego agenta w tiny.place, aby zaczął się poruszać.', '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.',
'agentWorld.world.room': 'Pokój', 'agentWorld.world.room': 'Pokój',
'agentWorld.world.rooms.poker.name': 'Poker', 'agentWorld.world.rooms.poker.name': 'Poker',
'agentWorld.world.rooms.poker.description': 'Osiem miejsc wokół stołu z filcem.', 'agentWorld.world.rooms.poker.description': 'Osiem miejsc wokół stołu z filcem.',
+4 -4
View File
@@ -4,7 +4,7 @@ import type { TranslationMap } from './types';
// English-identical values fall back to English via I18nContext.resolveEn(). // English-identical values fall back to English via I18nContext.resolveEn().
const messages: TranslationMap = { const messages: TranslationMap = {
'conversations.backgroundTasks.title': 'Background tasks', 'conversations.backgroundTasks.title': 'Background tasks',
'nav.feedback': 'Feedback', 'nav.feedback': 'Enviar feedback',
'feedback.board': 'Quadro de feedback', 'feedback.board': 'Quadro de feedback',
'feedback.empty': 'Ainda não há feedback. Seja o primeiro a compartilhar uma ideia.', 'feedback.empty': 'Ainda não há feedback. Seja o primeiro a compartilhar uma ideia.',
'feedback.loadMore': 'Carregar mais', 'feedback.loadMore': 'Carregar mais',
@@ -78,15 +78,15 @@ const messages: TranslationMap = {
'nav.noAgentProfiles': 'Nenhum perfil de agente encontrado', 'nav.noAgentProfiles': 'Nenhum perfil de agente encontrado',
'nav.activity': 'Atividade', 'nav.activity': 'Atividade',
'nav.brain': 'Cérebro', 'nav.brain': 'Cérebro',
'nav.agentWorld': 'Tiny.Place', 'nav.agentWorld': 'Tiny Place',
'nav.wallet': 'Carteira', 'nav.wallet': 'Carteira',
'agentWorld.description': 'agentWorld.description':
'Tiny.Place é uma rede social para agentes de IA. Use o OpenHuman para interagir, encontrar e publicar trabalhos, negociar e crescer juntos.', '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': 'Mundo',
'agentWorld.world.booting': 'Iniciando renderizador...', 'agentWorld.world.booting': 'Iniciando renderizador...',
'agentWorld.world.title': 'Mundo dos agentes', 'agentWorld.world.title': 'Tiny Place',
'agentWorld.world.description': 'agentWorld.world.description':
'Registre seu agente no tiny.place para que ele comece a se mover.', 'Junte-se ao tiny.place para que seu agente coordene com outros agentes: encontrar e publicar trabalhos, negociar, enviar mensagens e colaborar em recompensas.',
'agentWorld.world.room': 'Sala', 'agentWorld.world.room': 'Sala',
'agentWorld.world.rooms.poker.name': 'Pôquer', 'agentWorld.world.rooms.poker.name': 'Pôquer',
'agentWorld.world.rooms.poker.description': 'Oito assentos ao redor de uma mesa de feltro.', 'agentWorld.world.rooms.poker.description': 'Oito assentos ao redor de uma mesa de feltro.',
+5 -4
View File
@@ -4,7 +4,7 @@ import type { TranslationMap } from './types';
// English-identical values fall back to English via I18nContext.resolveEn(). // English-identical values fall back to English via I18nContext.resolveEn().
const messages: TranslationMap = { const messages: TranslationMap = {
'conversations.backgroundTasks.title': 'Background tasks', 'conversations.backgroundTasks.title': 'Background tasks',
'nav.feedback': 'Отзывы', 'nav.feedback': 'Поделиться отзывом',
'feedback.board': 'Доска отзывов', 'feedback.board': 'Доска отзывов',
'feedback.empty': 'Пока нет отзывов. Поделитесь идеей первым.', 'feedback.empty': 'Пока нет отзывов. Поделитесь идеей первым.',
'feedback.loadMore': 'Загрузить ещё', 'feedback.loadMore': 'Загрузить ещё',
@@ -78,14 +78,15 @@ const messages: TranslationMap = {
'nav.noAgentProfiles': 'Профили агентов не найдены', 'nav.noAgentProfiles': 'Профили агентов не найдены',
'nav.activity': 'Активность', 'nav.activity': 'Активность',
'nav.brain': 'Мозг', 'nav.brain': 'Мозг',
'nav.agentWorld': 'Tiny.Place', 'nav.agentWorld': 'Tiny Place',
'nav.wallet': 'Кошелёк', 'nav.wallet': 'Кошелёк',
'agentWorld.description': 'agentWorld.description':
'Tiny.Place — это социальная сеть для ИИ-агентов. Используйте OpenHuman, чтобы взаимодействовать, находить и публиковать задания, торговать и расти вместе.', 'Tiny.Place — это социальная сеть для ИИ-агентов. Используйте OpenHuman, чтобы взаимодействовать, находить и публиковать задания, торговать и расти вместе.',
'agentWorld.world': 'Мир', 'agentWorld.world': 'Мир',
'agentWorld.world.booting': 'Запуск рендерера...', 'agentWorld.world.booting': 'Запуск рендерера...',
'agentWorld.world.title': 'Мир агентов', 'agentWorld.world.title': 'Tiny Place',
'agentWorld.world.description': 'Зарегистрируйте агента в tiny.place, чтобы он начал двигаться.', 'agentWorld.world.description':
'Присоединяйтесь к tiny.place, чтобы ваш агент взаимодействовал с другими агентами: находил и публиковал задания, торговал, обменивался сообщениями и работал над наградами.',
'agentWorld.world.room': 'Комната', 'agentWorld.world.room': 'Комната',
'agentWorld.world.rooms.poker.name': 'Покер', 'agentWorld.world.rooms.poker.name': 'Покер',
'agentWorld.world.rooms.poker.description': 'Восемь мест вокруг стола с сукном.', 'agentWorld.world.rooms.poker.description': 'Восемь мест вокруг стола с сукном.',
+5 -4
View File
@@ -4,7 +4,7 @@ import type { TranslationMap } from './types';
// English-identical values fall back to English via I18nContext.resolveEn(). // English-identical values fall back to English via I18nContext.resolveEn().
const messages: TranslationMap = { const messages: TranslationMap = {
'conversations.backgroundTasks.title': 'Background tasks', 'conversations.backgroundTasks.title': 'Background tasks',
'nav.feedback': '反馈', 'nav.feedback': '分享反馈',
'feedback.board': '反馈板', 'feedback.board': '反馈板',
'feedback.empty': '还没有反馈。来分享第一个想法吧。', 'feedback.empty': '还没有反馈。来分享第一个想法吧。',
'feedback.loadMore': '加载更多', 'feedback.loadMore': '加载更多',
@@ -77,14 +77,15 @@ const messages: TranslationMap = {
'nav.noAgentProfiles': '未找到代理档案', 'nav.noAgentProfiles': '未找到代理档案',
'nav.activity': '动态', 'nav.activity': '动态',
'nav.brain': '大脑', 'nav.brain': '大脑',
'nav.agentWorld': 'Tiny.Place', 'nav.agentWorld': 'Tiny Place',
'nav.wallet': '钱包', 'nav.wallet': '钱包',
'agentWorld.description': 'agentWorld.description':
'Tiny.Place 是面向 AI 智能体的社交网络。使用 OpenHuman 来互动、查找和发布工作、交易并共同成长。', 'Tiny.Place 是面向 AI 智能体的社交网络。使用 OpenHuman 来互动、查找和发布工作、交易并共同成长。',
'agentWorld.world': '世界', 'agentWorld.world': '世界',
'agentWorld.world.booting': '正在启动渲染器...', 'agentWorld.world.booting': '正在启动渲染器...',
'agentWorld.world.title': '代理世界', 'agentWorld.world.title': 'Tiny Place',
'agentWorld.world.description': '在 tiny.place 注册你的代理,让它开始移动。', 'agentWorld.world.description':
'加入 tiny.place,让你的代理与其他代理协作:查找和发布任务、交易、收发消息以及共同完成悬赏。',
'agentWorld.world.room': '房间', 'agentWorld.world.room': '房间',
'agentWorld.world.rooms.poker.name': '扑克', 'agentWorld.world.rooms.poker.name': '扑克',
'agentWorld.world.rooms.poker.description': '八个座位围绕一张毡面牌桌。', 'agentWorld.world.rooms.poker.description': '八个座位围绕一张毡面牌桌。',
+1
View File
@@ -52,6 +52,7 @@ const ALL_LOCALES = [...Object.keys(NATIVE_SCRIPT), ...LATIN_LOCALES];
// locales. These are reviewed exceptions — a value flagged here is expected, not a bug. // locales. These are reviewed exceptions — a value flagged here is expected, not a bug.
// A key NOT in this set that the detector flags is a genuine untranslated string to fix. // A key NOT in this set that the detector flags is a genuine untranslated string to fix.
const INTENTIONAL_ENGLISH = new Set([ const INTENTIONAL_ENGLISH = new Set([
"agentWorld.world.title", // "Tiny Place" — brand/product name, same in every locale
"app.connectionIndicator.coreOffline", "app.connectionIndicator.coreOffline",
"channels.activeRouteValue", "channels.activeRouteValue",
"composio.integrationSlugsExample", "composio.integrationSlugsExample",
+3 -3
View File
@@ -22,9 +22,9 @@ pub use ops as rpc;
pub use ops::*; pub use ops::*;
pub use schema::{ pub use schema::{
action_dir_env_override, clear_active_user, default_action_dir, default_projects_dir, action_dir_env_override, active_user_marker_path, clear_active_user, default_action_dir,
default_root_openhuman_dir, pre_login_user_dir, read_active_user_id, resolve_action_dir, default_projects_dir, default_root_openhuman_dir, pre_login_user_dir, read_active_user_id,
user_openhuman_dir, write_active_user_id, PRE_LOGIN_USER_ID, resolve_action_dir, user_openhuman_dir, write_active_user_id, PRE_LOGIN_USER_ID,
}; };
#[allow(unused_imports)] #[allow(unused_imports)]
pub use schema::{ pub use schema::{
+69 -41
View File
@@ -135,67 +135,93 @@ pub(crate) fn reset_local_data_remove_error(path: &Path, error: &std::io::Error)
} }
pub(crate) fn reset_local_data_marker_remove_error(path: &Path, error: &std::io::Error) -> String { pub(crate) fn reset_local_data_marker_remove_error(path: &Path, error: &std::io::Error) -> String {
// This is called for every root-level marker (active_workspace.toml,
// active_user.toml, …), so the wording is derived from the actual file
// name rather than hardcoded to one marker.
let marker_name = path
.file_name()
.and_then(|name| name.to_str())
.unwrap_or("marker");
if is_windows_file_lock_error(error) { if is_windows_file_lock_error(error) {
tracing::warn!( tracing::warn!(
marker = %path.display(), marker = %path.display(),
error = %error, error = %error,
"[config] reset_local_data: Windows file lock blocked active workspace marker deletion" "[config] reset_local_data: Windows file lock blocked marker deletion"
); );
return format!( return format!(
"Failed to remove active workspace marker {} because it is locked by another OpenHuman window or process. Close all OpenHuman windows and try again. ({error})", "Failed to remove marker {} ({marker_name}) because it is locked by another OpenHuman window or process. Close all OpenHuman windows and try again. ({error})",
path.display() path.display()
); );
} }
format!("Failed to remove active workspace marker: {error}") format!(
"Failed to remove marker {} ({marker_name}): {error}",
path.display()
)
} }
/// Internal helper to reset local data by removing specific directories and markers. /// Internal helper to reset local data for the **active user only**.
///
/// Removes the current user's data directory (`~/.openhuman/users/<id>`) plus
/// the two shared marker files at the root — `active_workspace.toml` and
/// `active_user.toml` — so the next launch boots signed-out into the
/// pre-login (`users/local`) scope.
///
/// It deliberately does **not** delete the shared root `~/.openhuman`
/// directory: that root holds every user's `users/<other>` subtree, and
/// wiping it during a single user's "Clear App Data" destroyed sibling
/// accounts' data (the scoping bug this replaces). The root is left in place;
/// only the current user's slice and the active markers are removed.
pub(crate) async fn reset_local_data_for_paths( pub(crate) async fn reset_local_data_for_paths(
current_openhuman_dir: &Path, current_openhuman_dir: &Path,
default_openhuman_dir: &Path, default_openhuman_dir: &Path,
) -> Result<RpcOutcome<serde_json::Value>, String> { ) -> Result<RpcOutcome<serde_json::Value>, String> {
let active_workspace_marker = active_workspace_marker_path(default_openhuman_dir); let active_workspace_marker = active_workspace_marker_path(default_openhuman_dir);
let active_user_marker =
crate::openhuman::config::active_user_marker_path(default_openhuman_dir);
tracing::debug!( tracing::debug!(
current_dir = %current_openhuman_dir.display(), current_dir = %current_openhuman_dir.display(),
default_dir = %default_openhuman_dir.display(), default_dir = %default_openhuman_dir.display(),
marker = %active_workspace_marker.display(), workspace_marker = %active_workspace_marker.display(),
"[config] reset_local_data: starting" user_marker = %active_user_marker.display(),
"[config] reset_local_data: starting (user-scoped)"
); );
let mut removed_paths = Vec::new(); let mut removed_paths = Vec::new();
if active_workspace_marker.exists() { // Remove the two shared root-level markers so the current user is signed
if let Err(error) = tokio::fs::remove_file(&active_workspace_marker).await { // out and any non-default workspace pointer is dropped. Each is a single
return Err(reset_local_data_marker_remove_error( // file under the root; the root itself is preserved for sibling users.
&active_workspace_marker, for marker in [&active_workspace_marker, &active_user_marker] {
&error, if marker.exists() {
)); if let Err(error) = tokio::fs::remove_file(marker).await {
return Err(reset_local_data_marker_remove_error(marker, &error));
}
tracing::debug!(
marker = %marker.display(),
"[config] reset_local_data: removed marker"
);
removed_paths.push(marker.display().to_string());
} }
tracing::debug!(
marker = %active_workspace_marker.display(),
"[config] reset_local_data: removed active workspace marker"
);
removed_paths.push(active_workspace_marker.display().to_string());
} }
for target_dir in [current_openhuman_dir, default_openhuman_dir] { // Remove only the active user's directory — NOT the shared root, which
if !target_dir.exists() { // contains other users' `users/<id>` subtrees.
tracing::debug!( if current_openhuman_dir.exists() {
dir = %target_dir.display(), if let Err(error) = tokio::fs::remove_dir_all(current_openhuman_dir).await {
"[config] reset_local_data: directory already absent" return Err(reset_local_data_remove_error(current_openhuman_dir, &error));
);
continue;
}
if let Err(error) = tokio::fs::remove_dir_all(target_dir).await {
return Err(reset_local_data_remove_error(target_dir, &error));
} }
tracing::debug!( tracing::debug!(
dir = %target_dir.display(), dir = %current_openhuman_dir.display(),
"[config] reset_local_data: removed directory" "[config] reset_local_data: removed current user directory"
);
removed_paths.push(current_openhuman_dir.display().to_string());
} else {
tracing::debug!(
dir = %current_openhuman_dir.display(),
"[config] reset_local_data: current user directory already absent"
); );
removed_paths.push(target_dir.display().to_string());
} }
Ok(RpcOutcome::new( Ok(RpcOutcome::new(
@@ -204,16 +230,11 @@ pub(crate) async fn reset_local_data_for_paths(
"current_openhuman_dir": current_openhuman_dir.display().to_string(), "current_openhuman_dir": current_openhuman_dir.display().to_string(),
"default_openhuman_dir": default_openhuman_dir.display().to_string(), "default_openhuman_dir": default_openhuman_dir.display().to_string(),
}), }),
vec![ vec![format!(
format!( "reset local data for active user dir {} (shared root {} preserved)",
"reset local data for active config dir {}", current_openhuman_dir.display(),
current_openhuman_dir.display() default_openhuman_dir.display()
), )],
format!(
"removed default data dir {} if present",
default_openhuman_dir.display()
),
],
)) ))
} }
@@ -537,11 +558,18 @@ pub async fn get_data_paths() -> Result<RpcOutcome<serde_json::Value>, String> {
let current_openhuman_dir = config_openhuman_dir(&config); let current_openhuman_dir = config_openhuman_dir(&config);
let default_openhuman_dir = default_openhuman_dir(); let default_openhuman_dir = default_openhuman_dir();
let active_workspace_marker = active_workspace_marker_path(&default_openhuman_dir); let active_workspace_marker = active_workspace_marker_path(&default_openhuman_dir);
// The active-user marker lives at the *shared* root `~/.openhuman`, not
// inside the per-user dir. A clear removes it (to sign the current user
// out) but must leave the sibling `users/<other>` dirs and the root
// itself intact — see `reset_local_data_for_paths`.
let active_user_marker =
crate::openhuman::config::active_user_marker_path(&default_openhuman_dir);
Ok(RpcOutcome::new( Ok(RpcOutcome::new(
json!({ json!({
"current_openhuman_dir": current_openhuman_dir.display().to_string(), "current_openhuman_dir": current_openhuman_dir.display().to_string(),
"default_openhuman_dir": default_openhuman_dir.display().to_string(), "default_openhuman_dir": default_openhuman_dir.display().to_string(),
"active_workspace_marker_path": active_workspace_marker.display().to_string(), "active_workspace_marker_path": active_workspace_marker.display().to_string(),
"active_user_marker_path": active_user_marker.display().to_string(),
}), }),
vec![format!( vec![format!(
"data paths resolved (current={}, default={})", "data paths resolved (current={}, default={})",
+64 -8
View File
@@ -2,19 +2,22 @@ use super::*;
use tempfile::tempdir; use tempfile::tempdir;
#[tokio::test] #[tokio::test]
async fn reset_local_data_removes_current_dir_default_dir_and_marker() { async fn reset_local_data_removes_active_user_and_markers_only() {
let temp = tempdir().unwrap(); let temp = tempdir().unwrap();
let default_openhuman_dir = temp.path().join("default-openhuman"); let default_openhuman_dir = temp.path().join("default-openhuman");
let current_openhuman_dir = temp.path().join("custom-openhuman"); // Active user lives under the shared root's `users/` tree, mirroring the
let marker = active_workspace_marker_path(&default_openhuman_dir); // real layout (`~/.openhuman/users/<id>`).
let current_openhuman_dir = default_openhuman_dir.join("users").join("active-user");
let workspace_marker = active_workspace_marker_path(&default_openhuman_dir);
let user_marker = crate::openhuman::config::active_user_marker_path(&default_openhuman_dir);
tokio::fs::create_dir_all(default_openhuman_dir.join("workspace"))
.await
.unwrap();
tokio::fs::create_dir_all(current_openhuman_dir.join("workspace")) tokio::fs::create_dir_all(current_openhuman_dir.join("workspace"))
.await .await
.unwrap(); .unwrap();
tokio::fs::write(&marker, "config_dir = '/tmp/custom-openhuman'\n") tokio::fs::write(&workspace_marker, "config_dir = 'users/active-user'\n")
.await
.unwrap();
tokio::fs::write(&user_marker, "user_id = 'active-user'\n")
.await .await
.unwrap(); .unwrap();
@@ -22,8 +25,12 @@ async fn reset_local_data_removes_current_dir_default_dir_and_marker() {
.await .await
.unwrap(); .unwrap();
// Active user's slice and both shared markers are gone …
assert!(!current_openhuman_dir.exists()); assert!(!current_openhuman_dir.exists());
assert!(!default_openhuman_dir.exists()); assert!(!workspace_marker.exists());
assert!(!user_marker.exists());
// … but the shared root itself survives.
assert!(default_openhuman_dir.exists());
assert!(outcome assert!(outcome
.value .value
.get("removed_paths") .get("removed_paths")
@@ -31,6 +38,55 @@ async fn reset_local_data_removes_current_dir_default_dir_and_marker() {
.is_some_and(|paths| !paths.is_empty())); .is_some_and(|paths| !paths.is_empty()));
} }
#[tokio::test]
async fn reset_local_data_preserves_sibling_users() {
let temp = tempdir().unwrap();
let default_openhuman_dir = temp.path().join("default-openhuman");
let current_openhuman_dir = default_openhuman_dir.join("users").join("active-user");
let sibling_user_dir = default_openhuman_dir.join("users").join("other-user");
let sibling_file = sibling_user_dir.join("config.toml");
tokio::fs::create_dir_all(current_openhuman_dir.join("workspace"))
.await
.unwrap();
tokio::fs::create_dir_all(&sibling_user_dir).await.unwrap();
tokio::fs::write(&sibling_file, "api_key = 'sibling'\n")
.await
.unwrap();
reset_local_data_for_paths(&current_openhuman_dir, &default_openhuman_dir)
.await
.unwrap();
// The active user is wiped; the sibling account is untouched — this is the
// regression this fix addresses.
assert!(!current_openhuman_dir.exists());
assert!(sibling_user_dir.exists());
assert!(sibling_file.exists());
}
#[tokio::test]
async fn reset_local_data_tolerates_absent_paths() {
let temp = tempdir().unwrap();
let default_openhuman_dir = temp.path().join("default-openhuman");
let current_openhuman_dir = default_openhuman_dir.join("users").join("active-user");
tokio::fs::create_dir_all(&default_openhuman_dir)
.await
.unwrap();
// No current user dir, no markers — a fresh / already-cleared install.
let outcome = reset_local_data_for_paths(&current_openhuman_dir, &default_openhuman_dir)
.await
.unwrap();
assert!(default_openhuman_dir.exists());
assert!(outcome
.value
.get("removed_paths")
.and_then(|value| value.as_array())
.is_some_and(|paths| paths.is_empty()));
}
// ── env_flag_enabled ──────────────────────────────────────────── // ── env_flag_enabled ────────────────────────────────────────────
use crate::openhuman::config::TEST_ENV_LOCK as ENV_LOCK; use crate::openhuman::config::TEST_ENV_LOCK as ENV_LOCK;
+2 -2
View File
@@ -6,8 +6,8 @@ use std::path::{Path, PathBuf};
use tokio::fs; use tokio::fs;
pub use load_user_state::{ pub use load_user_state::{
clear_active_user, pre_login_user_dir, read_active_user_id, user_openhuman_dir, active_user_marker_path, clear_active_user, pre_login_user_dir, read_active_user_id,
write_active_user_id, PRE_LOGIN_USER_ID, user_openhuman_dir, write_active_user_id, PRE_LOGIN_USER_ID,
}; };
#[path = "../load_user_state.rs"] #[path = "../load_user_state.rs"]
+3 -3
View File
@@ -11,9 +11,9 @@ pub(crate) use env::EnvLookup;
pub(crate) use env::ProcessEnv; pub(crate) use env::ProcessEnv;
pub use dirs::{ pub use dirs::{
action_dir_env_override, clear_active_user, default_action_dir, default_projects_dir, action_dir_env_override, active_user_marker_path, clear_active_user, default_action_dir,
default_root_openhuman_dir, pre_login_user_dir, read_active_user_id, resolve_action_dir, default_projects_dir, default_root_openhuman_dir, pre_login_user_dir, read_active_user_id,
user_openhuman_dir, write_active_user_id, ACTION_DIR_ENV_VAR, resolve_action_dir, user_openhuman_dir, write_active_user_id, ACTION_DIR_ENV_VAR,
MEMORY_SYNC_INTERVAL_SECS_ENV_VAR, PRE_LOGIN_USER_ID, PROJECTS_DIR_ENV_VAR, MEMORY_SYNC_INTERVAL_SECS_ENV_VAR, PRE_LOGIN_USER_ID, PROJECTS_DIR_ENV_VAR,
}; };
+14 -2
View File
@@ -10,10 +10,22 @@ struct ActiveUserState {
user_id: String, user_id: String,
} }
/// Returns the path to the active-user marker:
/// `{default_openhuman_dir}/active_user.toml`.
///
/// This marker is **shared across all users** — it lives at the root
/// `~/.openhuman` dir, not inside any per-user directory — so it records
/// *which* user is currently active. Clearing one user's data must remove
/// this marker (to sign that user out) without touching the sibling
/// `users/<other>` directories.
pub fn active_user_marker_path(default_openhuman_dir: &Path) -> PathBuf {
default_openhuman_dir.join(ACTIVE_USER_STATE_FILE)
}
/// Reads the active user id from `{default_openhuman_dir}/active_user.toml`. /// Reads the active user id from `{default_openhuman_dir}/active_user.toml`.
/// Returns `None` when the file does not exist, is empty, or cannot be parsed. /// Returns `None` when the file does not exist, is empty, or cannot be parsed.
pub fn read_active_user_id(default_openhuman_dir: &Path) -> Option<String> { pub fn read_active_user_id(default_openhuman_dir: &Path) -> Option<String> {
let path = default_openhuman_dir.join(ACTIVE_USER_STATE_FILE); let path = active_user_marker_path(default_openhuman_dir);
let contents = std::fs::read_to_string(&path).ok()?; let contents = std::fs::read_to_string(&path).ok()?;
let state: ActiveUserState = toml::from_str(&contents).ok()?; let state: ActiveUserState = toml::from_str(&contents).ok()?;
let id = state.user_id.trim().to_string(); let id = state.user_id.trim().to_string();
@@ -77,7 +89,7 @@ pub fn write_active_user_id(default_openhuman_dir: &Path, user_id: &str) -> Resu
/// Removes the active user marker. After this, the next config load will /// Removes the active user marker. After this, the next config load will
/// use the default (unauthenticated) openhuman directory. /// use the default (unauthenticated) openhuman directory.
pub fn clear_active_user(default_openhuman_dir: &Path) -> Result<()> { pub fn clear_active_user(default_openhuman_dir: &Path) -> Result<()> {
let path = default_openhuman_dir.join(ACTIVE_USER_STATE_FILE); let path = active_user_marker_path(default_openhuman_dir);
if path.exists() { if path.exists() {
std::fs::remove_file(&path) std::fs::remove_file(&path)
.with_context(|| format!("Failed to remove active user state: {}", path.display()))?; .with_context(|| format!("Failed to remove active user state: {}", path.display()))?;
+3 -3
View File
@@ -24,9 +24,9 @@ mod identity_cost;
mod learning; mod learning;
mod load; mod load;
pub use load::{ pub use load::{
action_dir_env_override, clear_active_user, default_action_dir, default_projects_dir, action_dir_env_override, active_user_marker_path, clear_active_user, default_action_dir,
default_root_openhuman_dir, pre_login_user_dir, read_active_user_id, resolve_action_dir, default_projects_dir, default_root_openhuman_dir, pre_login_user_dir, read_active_user_id,
user_openhuman_dir, write_active_user_id, PRE_LOGIN_USER_ID, resolve_action_dir, user_openhuman_dir, write_active_user_id, PRE_LOGIN_USER_ID,
}; };
pub mod claude_agent_sdk; pub mod claude_agent_sdk;
pub use claude_agent_sdk::ClaudeAgentSdkConfig; pub use claude_agent_sdk::ClaudeAgentSdkConfig;