From 95d517ca26b3968c35d14e1983d44827a8dc3b3d Mon Sep 17 00:00:00 2001 From: Steven Enamakel <31011319+senamakel@users.noreply.github.com> Date: Mon, 22 Jun 2026 18:01:23 -0700 Subject: [PATCH] feat: move feedback to sidebar footer, rename World card, fix Clear App Data scoping (#3945) --- app/src-tauri/src/local_data_reset.rs | 46 +++++--- app/src/agentworld/pages/WorldSection.tsx | 4 +- .../components/layout/shell/AppSidebar.tsx | 39 ++++++- app/src/config/__tests__/navConfig.test.ts | 12 +- app/src/config/navConfig.ts | 4 +- app/src/lib/i18n/ar.ts | 9 +- app/src/lib/i18n/bn.ts | 8 +- app/src/lib/i18n/de.ts | 8 +- app/src/lib/i18n/en.ts | 8 +- app/src/lib/i18n/es.ts | 9 +- app/src/lib/i18n/fr.ts | 8 +- app/src/lib/i18n/hi.ts | 9 +- app/src/lib/i18n/id.ts | 9 +- app/src/lib/i18n/it.ts | 8 +- app/src/lib/i18n/ko.ts | 8 +- app/src/lib/i18n/pl.ts | 8 +- app/src/lib/i18n/pt.ts | 8 +- app/src/lib/i18n/ru.ts | 9 +- app/src/lib/i18n/zh-CN.ts | 9 +- scripts/i18n-find-english.ts | 1 + src/openhuman/config/mod.rs | 6 +- src/openhuman/config/ops/loader.rs | 110 +++++++++++------- src/openhuman/config/ops_tests.rs | 72 ++++++++++-- src/openhuman/config/schema/load/dirs.rs | 4 +- src/openhuman/config/schema/load/mod.rs | 6 +- .../config/schema/load_user_state.rs | 16 ++- src/openhuman/config/schema/mod.rs | 6 +- 27 files changed, 302 insertions(+), 142 deletions(-) diff --git a/app/src-tauri/src/local_data_reset.rs b/app/src-tauri/src/local_data_reset.rs index 77777fc90..c9d731903 100644 --- a/app/src-tauri/src/local_data_reset.rs +++ b/app/src-tauri/src/local_data_reset.rs @@ -22,12 +22,15 @@ use crate::reset_reboot_schedule; /// 3. Shut down the embedded core. `CoreProcessHandle::shutdown` cancels /// the cancellation token and awaits the tokio task, which drops the /// SQLite pool, log writer, etc. — releasing every Windows file handle. -/// 4. Remove the three paths (current data dir, default data dir, active -/// workspace marker) from this process. Missing entries are non-fatal. +/// 4. Remove the **active user's** slice from this process: the current data +/// 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/` subtrees survive. Missing entries are +/// non-fatal. /// 5. Restart the embedded core via `ensure_running`. /// -/// Returns `Ok(())` only when the core is back up and the directories are -/// gone (or were already absent). Any step's `Err` short-circuits and +/// Returns `Ok(())` only when the core is back up and the active user's data +/// 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. #[tauri::command] pub async fn reset_local_data( @@ -43,10 +46,11 @@ pub async fn reset_local_data( // that logic here. let paths = fetch_data_paths().await?; 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.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 ───────────────────────────────────── @@ -85,20 +89,25 @@ pub async fn reset_local_data( // `?` — 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 // 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/` 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/` 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 { remove_path_if_exists( &paths.active_workspace_marker_path, "active workspace marker", ) .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?; - 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(()) } .await; @@ -130,8 +139,14 @@ pub async fn reset_local_data( /// Resolved data paths returned by `config_get_data_paths`. struct ResolvedDataPaths { 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/` subtree (deleting it wiped sibling accounts' data). default_openhuman_dir: 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) -> bool { @@ -306,10 +321,15 @@ async fn fetch_data_paths() -> Result { .get("active_workspace_marker_path") .and_then(|v| v.as_str()) .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 { current_openhuman_dir: std::path::PathBuf::from(current), default_openhuman_dir: std::path::PathBuf::from(default), active_workspace_marker_path: std::path::PathBuf::from(marker), + active_user_marker_path: std::path::PathBuf::from(user_marker), }) } diff --git a/app/src/agentworld/pages/WorldSection.tsx b/app/src/agentworld/pages/WorldSection.tsx index 84786accd..90436999b 100644 --- a/app/src/agentworld/pages/WorldSection.tsx +++ b/app/src/agentworld/pages/WorldSection.tsx @@ -96,12 +96,12 @@ export default function WorldSection() {

- {t('agentWorld.world.title', 'Agent World')} + {t('agentWorld.world.title', 'Tiny Place')}

{t( '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.' )}

diff --git a/app/src/components/layout/shell/AppSidebar.tsx b/app/src/components/layout/shell/AppSidebar.tsx index 32e673761..59b84bfd7 100644 --- a/app/src/components/layout/shell/AppSidebar.tsx +++ b/app/src/components/layout/shell/AppSidebar.tsx @@ -1,6 +1,10 @@ +import { useLocation, useNavigate } from 'react-router-dom'; + import { useT } from '../../../lib/i18n/I18nContext'; +import { trackEvent } from '../../../services/analytics'; import { APP_VERSION } from '../../../utils/config'; import ConnectionIndicator from '../../ConnectionIndicator'; +import { NavIcon } from './navIcons'; import SidebarAppRail from './SidebarAppRail'; import SidebarHeader from './SidebarHeader'; import SidebarNav from './SidebarNav'; @@ -27,6 +31,22 @@ import { SidebarSlotOutlet } from './SidebarSlot'; */ export default function AppSidebar() { 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 (
@@ -46,9 +66,26 @@ export default function AppSidebar() { app rail above its thread list) can order them via Tailwind `order-*`. */}
+ {/* 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). */} + {/* App-wide footer: connectivity status + build/version, pinned to the bottom of the sidebar. */} -
+
· diff --git a/app/src/config/__tests__/navConfig.test.ts b/app/src/config/__tests__/navConfig.test.ts index 1c8497769..8c7783b7d 100644 --- a/app/src/config/__tests__/navConfig.test.ts +++ b/app/src/config/__tests__/navConfig.test.ts @@ -3,8 +3,8 @@ import { describe, expect, it } from 'vitest'; import { AVATAR_MENU_ITEMS, NAV_TABS } from '../navConfig'; describe('NAV_TABS', () => { - it('has exactly 6 entries', () => { - expect(NAV_TABS).toHaveLength(6); + it('has exactly 5 entries', () => { + expect(NAV_TABS).toHaveLength(5); }); it('has the correct ids in order', () => { @@ -14,7 +14,6 @@ describe('NAV_TABS', () => { 'brain', 'agent-world', 'connections', - 'feedback', ]); }); @@ -25,7 +24,6 @@ describe('NAV_TABS', () => { '/brain', '/agent-world', '/connections', - '/feedback', ]); }); @@ -36,7 +34,6 @@ describe('NAV_TABS', () => { 'nav.brain', 'nav.agentWorld', 'nav.connections', - 'nav.feedback', ]); }); @@ -47,7 +44,6 @@ describe('NAV_TABS', () => { 'tab-brain', 'tab-agent-world', 'tab-connections', - 'tab-feedback', ]); }); @@ -56,6 +52,10 @@ describe('NAV_TABS', () => { 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', () => { expect(NAV_TABS.find(t => t.id === 'activity')).toBeUndefined(); }); diff --git a/app/src/config/navConfig.ts b/app/src/config/navConfig.ts index 440417488..843308926 100644 --- a/app/src/config/navConfig.ts +++ b/app/src/config/navConfig.ts @@ -48,9 +48,9 @@ export const NAV_TABS: NavTab[] = [ path: '/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 - // 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 ───────────────────────────────────────────────────── diff --git a/app/src/lib/i18n/ar.ts b/app/src/lib/i18n/ar.ts index c96592bde..54ae03040 100644 --- a/app/src/lib/i18n/ar.ts +++ b/app/src/lib/i18n/ar.ts @@ -4,7 +4,7 @@ import type { TranslationMap } from './types'; // English-identical values fall back to English via I18nContext.resolveEn(). const messages: TranslationMap = { 'conversations.backgroundTasks.title': 'Background tasks', - 'nav.feedback': 'الملاحظات', + 'nav.feedback': 'شارك ملاحظاتك', 'feedback.board': 'لوحة الملاحظات', 'feedback.empty': 'لا توجد ملاحظات بعد. كن أول من يشارك فكرة.', 'feedback.loadMore': 'تحميل المزيد', @@ -77,14 +77,15 @@ const messages: TranslationMap = { 'nav.noAgentProfiles': 'لم يتم العثور على ملفات وكلاء', 'nav.activity': 'النشاط', 'nav.brain': 'الدماغ', - 'nav.agentWorld': 'Tiny.Place', + 'nav.agentWorld': 'Tiny Place', 'nav.wallet': 'المحفظة', 'agentWorld.description': 'Tiny.Place شبكة اجتماعية لوكلاء الذكاء الاصطناعي. استخدم OpenHuman للتفاعل والعثور على الوظائف ونشرها والتداول والنمو معًا.', 'agentWorld.world': 'العالم', 'agentWorld.world.booting': 'جارٍ تشغيل العارض...', - 'agentWorld.world.title': 'عالم الوكلاء', - 'agentWorld.world.description': 'سجّل وكيلك في tiny.place ليبدأ بالحركة داخل العالم.', + 'agentWorld.world.title': 'Tiny Place', + 'agentWorld.world.description': + 'انضم إلى tiny.place ليتمكن وكيلك من التنسيق مع الوكلاء الآخرين: العثور على الوظائف ونشرها، والتداول، وتبادل الرسائل، والتعاون في المكافآت.', 'agentWorld.world.room': 'الغرفة', 'agentWorld.world.rooms.poker.name': 'بوكر', 'agentWorld.world.rooms.poker.description': 'ثمانية مقاعد حول طاولة مكسوة باللباد.', diff --git a/app/src/lib/i18n/bn.ts b/app/src/lib/i18n/bn.ts index b87d27496..04b7c4d11 100644 --- a/app/src/lib/i18n/bn.ts +++ b/app/src/lib/i18n/bn.ts @@ -4,7 +4,7 @@ import type { TranslationMap } from './types'; // English-identical values fall back to English via I18nContext.resolveEn(). const messages: TranslationMap = { 'conversations.backgroundTasks.title': 'Background tasks', - 'nav.feedback': 'মতামত', + 'nav.feedback': 'মতামত দিন', 'feedback.board': 'মতামত বোর্ড', 'feedback.empty': 'এখনও কোনো মতামত নেই। প্রথম হয়ে একটি আইডিয়া শেয়ার করুন।', 'feedback.loadMore': 'আরও দেখুন', @@ -78,15 +78,15 @@ const messages: TranslationMap = { 'nav.noAgentProfiles': 'কোনো এজেন্ট প্রোফাইল পাওয়া যায়নি', 'nav.activity': 'কার্যকলাপ', 'nav.brain': 'ব্রেইন', - 'nav.agentWorld': 'Tiny.Place', + 'nav.agentWorld': 'Tiny Place', 'nav.wallet': 'ওয়ালেট', 'agentWorld.description': 'Tiny.Place হলো এআই এজেন্টদের জন্য একটি সোশ্যাল নেটওয়ার্ক। যোগাযোগ করতে, কাজ খুঁজতে ও পোস্ট করতে, লেনদেন করতে এবং একসাথে বেড়ে উঠতে OpenHuman ব্যবহার করুন।', 'agentWorld.world': 'বিশ্ব', 'agentWorld.world.booting': 'রেন্ডারার চালু হচ্ছে...', - 'agentWorld.world.title': 'এজেন্ট বিশ্ব', + 'agentWorld.world.title': 'Tiny Place', 'agentWorld.world.description': - 'আপনার এজেন্টকে tiny.place-এ নিবন্ধন করুন যাতে সে চলাফেরা শুরু করে।', + 'tiny.place-এ যোগ দিন যাতে আপনার এজেন্ট অন্য এজেন্টদের সাথে সমন্বয় করতে পারে — কাজ খুঁজে পাওয়া ও পোস্ট করা, লেনদেন, বার্তা পাঠানো এবং বাউন্টিতে একসাথে কাজ করা।', 'agentWorld.world.room': 'রুম', 'agentWorld.world.rooms.poker.name': 'পোকার', 'agentWorld.world.rooms.poker.description': 'ফেল্ট টেবিলের চারপাশে আটটি আসন।', diff --git a/app/src/lib/i18n/de.ts b/app/src/lib/i18n/de.ts index c7934a87c..fce46a28f 100644 --- a/app/src/lib/i18n/de.ts +++ b/app/src/lib/i18n/de.ts @@ -4,7 +4,7 @@ import type { TranslationMap } from './types'; // English-identical values fall back to English via I18nContext.resolveEn(). const messages: TranslationMap = { 'conversations.backgroundTasks.title': 'Background tasks', - 'nav.feedback': 'Feedback', + 'nav.feedback': 'Feedback geben', 'feedback.board': 'Feedback-Board', 'feedback.empty': 'Noch kein Feedback. Teile als Erste:r eine Idee.', 'feedback.loadMore': 'Mehr laden', @@ -79,15 +79,15 @@ const messages: TranslationMap = { 'nav.noAgentProfiles': 'Keine Agentenprofile gefunden', 'nav.activity': 'Aktivität', 'nav.brain': 'Gehirn', - 'nav.agentWorld': 'Tiny.Place', + 'nav.agentWorld': 'Tiny Place', 'nav.wallet': 'Wallet', '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.', 'agentWorld.world': 'Welt', 'agentWorld.world.booting': 'Renderer wird gestartet...', - 'agentWorld.world.title': 'Agentenwelt', + 'agentWorld.world.title': 'Tiny Place', '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.rooms.poker.name': 'Poker', 'agentWorld.world.rooms.poker.description': 'Acht Sitze rund um einen Filztisch.', diff --git a/app/src/lib/i18n/en.ts b/app/src/lib/i18n/en.ts index ee88877a1..0ed9442d7 100644 --- a/app/src/lib/i18n/en.ts +++ b/app/src/lib/i18n/en.ts @@ -23,16 +23,16 @@ const en: TranslationMap = { 'nav.noAgentProfiles': 'No agent profiles found', 'nav.activity': 'Activity', 'nav.brain': 'Brain', - 'nav.agentWorld': 'Tiny.Place', + 'nav.agentWorld': 'Tiny Place', 'nav.wallet': 'Wallet', // Agent World section sub-navigation labels 'agentWorld.description': '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.title': 'Agent World', + 'agentWorld.world.title': 'Tiny Place', '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.rooms.poker.name': 'Poker', 'agentWorld.world.rooms.poker.description': 'Eight seats around a felt table.', @@ -77,7 +77,7 @@ const en: TranslationMap = { 'nav.avatarMenu.rewards': 'Rewards', 'nav.avatarMenu.invites': 'Invite a friend', 'nav.avatarMenu.wallet': 'Wallet', - 'nav.feedback': 'Feedback', + 'nav.feedback': 'Share Feedback', // Brain — full-page memory knowledge-graph surface 'brain.subtitle': 'Your knowledge graph, memory sources, and controls.', diff --git a/app/src/lib/i18n/es.ts b/app/src/lib/i18n/es.ts index b0b5eeb83..f62112f9e 100644 --- a/app/src/lib/i18n/es.ts +++ b/app/src/lib/i18n/es.ts @@ -4,7 +4,7 @@ import type { TranslationMap } from './types'; // English-identical values fall back to English via I18nContext.resolveEn(). const messages: TranslationMap = { 'conversations.backgroundTasks.title': 'Background tasks', - 'nav.feedback': 'Opiniones', + 'nav.feedback': 'Compartir opiniones', 'feedback.board': 'Tablero de opiniones', 'feedback.empty': 'Aún no hay opiniones. Sé el primero en compartir una idea.', 'feedback.loadMore': 'Cargar más', @@ -78,14 +78,15 @@ const messages: TranslationMap = { 'nav.noAgentProfiles': 'No se encontraron perfiles de agente', 'nav.activity': 'Actividad', 'nav.brain': 'Cerebro', - 'nav.agentWorld': 'Tiny.Place', + 'nav.agentWorld': 'Tiny Place', 'nav.wallet': 'Cartera', 'agentWorld.description': '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.title': 'Mundo de agentes', - 'agentWorld.world.description': 'Registra tu agente en tiny.place para que empiece a moverse.', + '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.', 'agentWorld.world.room': 'Sala', 'agentWorld.world.rooms.poker.name': 'Póker', 'agentWorld.world.rooms.poker.description': 'Ocho asientos alrededor de una mesa de fieltro.', diff --git a/app/src/lib/i18n/fr.ts b/app/src/lib/i18n/fr.ts index a1b7f08f1..39951bcab 100644 --- a/app/src/lib/i18n/fr.ts +++ b/app/src/lib/i18n/fr.ts @@ -4,7 +4,7 @@ import type { TranslationMap } from './types'; // English-identical values fall back to English via I18nContext.resolveEn(). const messages: TranslationMap = { 'conversations.backgroundTasks.title': 'Background tasks', - 'nav.feedback': 'Suggestions', + 'nav.feedback': 'Donner mon avis', 'feedback.board': 'Tableau des suggestions', 'feedback.empty': 'Aucune suggestion pour le moment. Soyez le premier à partager une idée.', 'feedback.loadMore': 'Charger plus', @@ -78,15 +78,15 @@ const messages: TranslationMap = { 'nav.noAgentProfiles': "Aucun profil d'agent trouvé", 'nav.activity': 'Activité', 'nav.brain': 'Cerveau', - 'nav.agentWorld': 'Tiny.Place', + 'nav.agentWorld': 'Tiny Place', 'nav.wallet': 'Portefeuille', '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.', 'agentWorld.world': 'Monde', 'agentWorld.world.booting': 'Démarrage du moteur de rendu...', - 'agentWorld.world.title': 'Monde des agents', + 'agentWorld.world.title': 'Tiny Place', 'agentWorld.world.description': - 'Enregistrez votre agent sur tiny.place pour le faire commencer à bouger.', + '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.', 'agentWorld.world.room': 'Salle', 'agentWorld.world.rooms.poker.name': 'Poker', 'agentWorld.world.rooms.poker.description': 'Huit sièges autour d’une table en feutre.', diff --git a/app/src/lib/i18n/hi.ts b/app/src/lib/i18n/hi.ts index e61e84084..402d0b3a5 100644 --- a/app/src/lib/i18n/hi.ts +++ b/app/src/lib/i18n/hi.ts @@ -4,7 +4,7 @@ import type { TranslationMap } from './types'; // English-identical values fall back to English via I18nContext.resolveEn(). const messages: TranslationMap = { 'conversations.backgroundTasks.title': 'Background tasks', - 'nav.feedback': 'फ़ीडबैक', + 'nav.feedback': 'फ़ीडबैक साझा करें', 'feedback.board': 'फ़ीडबैक बोर्ड', 'feedback.empty': 'अभी तक कोई फ़ीडबैक नहीं है। कोई विचार साझा करने वाले पहले व्यक्ति बनें।', 'feedback.loadMore': 'और लोड करें', @@ -78,14 +78,15 @@ const messages: TranslationMap = { 'nav.noAgentProfiles': 'कोई एजेंट प्रोफाइल नहीं मिला', 'nav.activity': 'गतिविधि', 'nav.brain': 'ब्रेन', - 'nav.agentWorld': 'Tiny.Place', + 'nav.agentWorld': 'Tiny Place', 'nav.wallet': 'वॉलेट', 'agentWorld.description': 'Tiny.Place एआई एजेंट्स के लिए एक सोशल नेटवर्क है। बातचीत करने, काम खोजने और पोस्ट करने, व्यापार करने और साथ मिलकर आगे बढ़ने के लिए OpenHuman का उपयोग करें।', 'agentWorld.world': 'दुनिया', 'agentWorld.world.booting': 'रेंडरर शुरू हो रहा है...', - 'agentWorld.world.title': 'एजेंट दुनिया', - 'agentWorld.world.description': 'अपने एजेंट को tiny.place पर रजिस्टर करें ताकि वह चलना शुरू करे।', + 'agentWorld.world.title': 'Tiny Place', + 'agentWorld.world.description': + 'tiny.place से जुड़ें ताकि आपका एजेंट दूसरे एजेंट्स के साथ तालमेल कर सके — काम ढूँढना और पोस्ट करना, व्यापार करना, संदेश भेजना और बाउंटी पर मिलकर काम करना।', 'agentWorld.world.room': 'कमरा', 'agentWorld.world.rooms.poker.name': 'पोकर', 'agentWorld.world.rooms.poker.description': 'फेल्ट टेबल के चारों ओर आठ सीटें।', diff --git a/app/src/lib/i18n/id.ts b/app/src/lib/i18n/id.ts index db08f415a..3e5046c22 100644 --- a/app/src/lib/i18n/id.ts +++ b/app/src/lib/i18n/id.ts @@ -4,7 +4,7 @@ import type { TranslationMap } from './types'; // English-identical values fall back to English via I18nContext.resolveEn(). const messages: TranslationMap = { 'conversations.backgroundTasks.title': 'Background tasks', - 'nav.feedback': 'Masukan', + 'nav.feedback': 'Bagikan masukan', 'feedback.board': 'Papan masukan', 'feedback.empty': 'Belum ada masukan. Jadilah yang pertama membagikan ide.', 'feedback.loadMore': 'Muat lebih banyak', @@ -77,14 +77,15 @@ const messages: TranslationMap = { 'nav.noAgentProfiles': 'Profil agen tidak ditemukan', 'nav.activity': 'Aktivitas', 'nav.brain': 'Otak', - 'nav.agentWorld': 'Tiny.Place', + 'nav.agentWorld': 'Tiny Place', 'nav.wallet': 'Dompet', 'agentWorld.description': '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.title': 'Dunia agen', - 'agentWorld.world.description': 'Daftarkan agen Anda di tiny.place agar mulai bergerak.', + '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.', 'agentWorld.world.room': 'Ruang', 'agentWorld.world.rooms.poker.name': 'Poker', 'agentWorld.world.rooms.poker.description': 'Delapan kursi mengelilingi meja felt.', diff --git a/app/src/lib/i18n/it.ts b/app/src/lib/i18n/it.ts index 5f03a07f7..651f88f0a 100644 --- a/app/src/lib/i18n/it.ts +++ b/app/src/lib/i18n/it.ts @@ -4,7 +4,7 @@ import type { TranslationMap } from './types'; // English-identical values fall back to English via I18nContext.resolveEn(). const messages: TranslationMap = { 'conversations.backgroundTasks.title': 'Background tasks', - 'nav.feedback': 'Feedback', + 'nav.feedback': 'Condividi feedback', 'feedback.board': 'Bacheca dei feedback', 'feedback.empty': 'Ancora nessun feedback. Sii il primo a condividere un’idea.', 'feedback.loadMore': 'Carica altro', @@ -78,15 +78,15 @@ const messages: TranslationMap = { 'nav.noAgentProfiles': 'Nessun profilo agente trovato', 'nav.activity': 'Attività', 'nav.brain': 'Cervello', - 'nav.agentWorld': 'Tiny.Place', + 'nav.agentWorld': 'Tiny Place', 'nav.wallet': 'Portafoglio', 'agentWorld.description': '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.title': 'Mondo degli agenti', + 'agentWorld.world.title': 'Tiny Place', '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.rooms.poker.name': 'Poker', 'agentWorld.world.rooms.poker.description': 'Otto posti attorno a un tavolo in feltro.', diff --git a/app/src/lib/i18n/ko.ts b/app/src/lib/i18n/ko.ts index 2154ed647..a6a4321d1 100644 --- a/app/src/lib/i18n/ko.ts +++ b/app/src/lib/i18n/ko.ts @@ -4,7 +4,7 @@ import type { TranslationMap } from './types'; // English-identical values fall back to English via I18nContext.resolveEn(). const messages: TranslationMap = { 'conversations.backgroundTasks.title': 'Background tasks', - 'nav.feedback': '피드백', + 'nav.feedback': '피드백 보내기', 'feedback.board': '피드백 보드', 'feedback.empty': '아직 피드백이 없습니다. 가장 먼저 아이디어를 공유해 보세요.', 'feedback.loadMore': '더 불러오기', @@ -78,15 +78,15 @@ const messages: TranslationMap = { 'nav.noAgentProfiles': '에이전트 프로필을 찾을 수 없습니다', 'nav.activity': '활동', 'nav.brain': '브레인', - 'nav.agentWorld': 'Tiny.Place', + 'nav.agentWorld': 'Tiny Place', 'nav.wallet': '지갑', 'agentWorld.description': 'Tiny.Place는 AI 에이전트를 위한 소셜 네트워크입니다. OpenHuman을 사용해 소통하고, 일자리를 찾고 올리며, 거래하고 함께 성장하세요.', 'agentWorld.world': '월드', 'agentWorld.world.booting': '렌더러를 시작하는 중...', - 'agentWorld.world.title': '에이전트 월드', + 'agentWorld.world.title': 'Tiny Place', 'agentWorld.world.description': - 'tiny.place에 에이전트를 등록하면 월드 안에서 움직이기 시작합니다.', + 'tiny.place에 참여하면 에이전트가 다른 에이전트와 협업할 수 있습니다: 작업을 찾고 게시하고, 거래하고, 메시지를 주고받고, 바운티에 함께 참여하세요.', 'agentWorld.world.room': '방', 'agentWorld.world.rooms.poker.name': '포커', 'agentWorld.world.rooms.poker.description': '펠트 테이블을 둘러싼 여덟 좌석.', diff --git a/app/src/lib/i18n/pl.ts b/app/src/lib/i18n/pl.ts index a193bab68..093af23e7 100644 --- a/app/src/lib/i18n/pl.ts +++ b/app/src/lib/i18n/pl.ts @@ -4,7 +4,7 @@ import type { TranslationMap } from './types'; // English-identical values fall back to English via I18nContext.resolveEn(). const messages: TranslationMap = { 'conversations.backgroundTasks.title': 'Background tasks', - 'nav.feedback': 'Opinie', + 'nav.feedback': 'Podziel się opinią', 'feedback.board': 'Tablica opinii', 'feedback.empty': '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.activity': 'Aktywność', 'nav.brain': 'Mózg', - 'nav.agentWorld': 'Tiny.Place', + 'nav.agentWorld': 'Tiny Place', 'nav.wallet': 'Portfel', '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ć.', 'agentWorld.world': 'Świat', 'agentWorld.world.booting': 'Uruchamianie renderera...', - 'agentWorld.world.title': 'Świat agentów', + 'agentWorld.world.title': 'Tiny Place', '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.rooms.poker.name': 'Poker', 'agentWorld.world.rooms.poker.description': 'Osiem miejsc wokół stołu z filcem.', diff --git a/app/src/lib/i18n/pt.ts b/app/src/lib/i18n/pt.ts index 9eed871a1..f953f9e13 100644 --- a/app/src/lib/i18n/pt.ts +++ b/app/src/lib/i18n/pt.ts @@ -4,7 +4,7 @@ import type { TranslationMap } from './types'; // English-identical values fall back to English via I18nContext.resolveEn(). const messages: TranslationMap = { 'conversations.backgroundTasks.title': 'Background tasks', - 'nav.feedback': 'Feedback', + 'nav.feedback': 'Enviar feedback', 'feedback.board': 'Quadro de feedback', 'feedback.empty': 'Ainda não há feedback. Seja o primeiro a compartilhar uma ideia.', 'feedback.loadMore': 'Carregar mais', @@ -78,15 +78,15 @@ const messages: TranslationMap = { 'nav.noAgentProfiles': 'Nenhum perfil de agente encontrado', 'nav.activity': 'Atividade', 'nav.brain': 'Cérebro', - 'nav.agentWorld': 'Tiny.Place', + 'nav.agentWorld': 'Tiny Place', 'nav.wallet': 'Carteira', 'agentWorld.description': '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.title': 'Mundo dos agentes', + 'agentWorld.world.title': 'Tiny Place', '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.rooms.poker.name': 'Pôquer', 'agentWorld.world.rooms.poker.description': 'Oito assentos ao redor de uma mesa de feltro.', diff --git a/app/src/lib/i18n/ru.ts b/app/src/lib/i18n/ru.ts index ee34c7d68..ed57cf295 100644 --- a/app/src/lib/i18n/ru.ts +++ b/app/src/lib/i18n/ru.ts @@ -4,7 +4,7 @@ import type { TranslationMap } from './types'; // English-identical values fall back to English via I18nContext.resolveEn(). const messages: TranslationMap = { 'conversations.backgroundTasks.title': 'Background tasks', - 'nav.feedback': 'Отзывы', + 'nav.feedback': 'Поделиться отзывом', 'feedback.board': 'Доска отзывов', 'feedback.empty': 'Пока нет отзывов. Поделитесь идеей первым.', 'feedback.loadMore': 'Загрузить ещё', @@ -78,14 +78,15 @@ const messages: TranslationMap = { 'nav.noAgentProfiles': 'Профили агентов не найдены', 'nav.activity': 'Активность', 'nav.brain': 'Мозг', - 'nav.agentWorld': 'Tiny.Place', + 'nav.agentWorld': 'Tiny Place', 'nav.wallet': 'Кошелёк', 'agentWorld.description': 'Tiny.Place — это социальная сеть для ИИ-агентов. Используйте OpenHuman, чтобы взаимодействовать, находить и публиковать задания, торговать и расти вместе.', 'agentWorld.world': 'Мир', 'agentWorld.world.booting': 'Запуск рендерера...', - 'agentWorld.world.title': 'Мир агентов', - 'agentWorld.world.description': 'Зарегистрируйте агента в tiny.place, чтобы он начал двигаться.', + 'agentWorld.world.title': 'Tiny Place', + 'agentWorld.world.description': + 'Присоединяйтесь к tiny.place, чтобы ваш агент взаимодействовал с другими агентами: находил и публиковал задания, торговал, обменивался сообщениями и работал над наградами.', 'agentWorld.world.room': 'Комната', 'agentWorld.world.rooms.poker.name': 'Покер', 'agentWorld.world.rooms.poker.description': 'Восемь мест вокруг стола с сукном.', diff --git a/app/src/lib/i18n/zh-CN.ts b/app/src/lib/i18n/zh-CN.ts index 92b640637..a10467d0e 100644 --- a/app/src/lib/i18n/zh-CN.ts +++ b/app/src/lib/i18n/zh-CN.ts @@ -4,7 +4,7 @@ import type { TranslationMap } from './types'; // English-identical values fall back to English via I18nContext.resolveEn(). const messages: TranslationMap = { 'conversations.backgroundTasks.title': 'Background tasks', - 'nav.feedback': '反馈', + 'nav.feedback': '分享反馈', 'feedback.board': '反馈板', 'feedback.empty': '还没有反馈。来分享第一个想法吧。', 'feedback.loadMore': '加载更多', @@ -77,14 +77,15 @@ const messages: TranslationMap = { 'nav.noAgentProfiles': '未找到代理档案', 'nav.activity': '动态', 'nav.brain': '大脑', - 'nav.agentWorld': 'Tiny.Place', + 'nav.agentWorld': 'Tiny Place', 'nav.wallet': '钱包', 'agentWorld.description': 'Tiny.Place 是面向 AI 智能体的社交网络。使用 OpenHuman 来互动、查找和发布工作、交易并共同成长。', 'agentWorld.world': '世界', 'agentWorld.world.booting': '正在启动渲染器...', - 'agentWorld.world.title': '代理世界', - 'agentWorld.world.description': '在 tiny.place 注册你的代理,让它开始移动。', + 'agentWorld.world.title': 'Tiny Place', + 'agentWorld.world.description': + '加入 tiny.place,让你的代理与其他代理协作:查找和发布任务、交易、收发消息以及共同完成悬赏。', 'agentWorld.world.room': '房间', 'agentWorld.world.rooms.poker.name': '扑克', 'agentWorld.world.rooms.poker.description': '八个座位围绕一张毡面牌桌。', diff --git a/scripts/i18n-find-english.ts b/scripts/i18n-find-english.ts index f8edc91fb..13db87b8d 100644 --- a/scripts/i18n-find-english.ts +++ b/scripts/i18n-find-english.ts @@ -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. // A key NOT in this set that the detector flags is a genuine untranslated string to fix. const INTENTIONAL_ENGLISH = new Set([ + "agentWorld.world.title", // "Tiny Place" — brand/product name, same in every locale "app.connectionIndicator.coreOffline", "channels.activeRouteValue", "composio.integrationSlugsExample", diff --git a/src/openhuman/config/mod.rs b/src/openhuman/config/mod.rs index c7e48e6e6..d3f17e861 100644 --- a/src/openhuman/config/mod.rs +++ b/src/openhuman/config/mod.rs @@ -22,9 +22,9 @@ pub use ops as rpc; pub use ops::*; pub use schema::{ - action_dir_env_override, clear_active_user, default_action_dir, default_projects_dir, - default_root_openhuman_dir, pre_login_user_dir, read_active_user_id, resolve_action_dir, - user_openhuman_dir, write_active_user_id, PRE_LOGIN_USER_ID, + action_dir_env_override, active_user_marker_path, clear_active_user, default_action_dir, + default_projects_dir, default_root_openhuman_dir, pre_login_user_dir, read_active_user_id, + resolve_action_dir, user_openhuman_dir, write_active_user_id, PRE_LOGIN_USER_ID, }; #[allow(unused_imports)] pub use schema::{ diff --git a/src/openhuman/config/ops/loader.rs b/src/openhuman/config/ops/loader.rs index f740c9d3d..7ce1c1848 100644 --- a/src/openhuman/config/ops/loader.rs +++ b/src/openhuman/config/ops/loader.rs @@ -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 { + // 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) { tracing::warn!( marker = %path.display(), 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!( - "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() ); } - 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/`) 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/` 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( current_openhuman_dir: &Path, default_openhuman_dir: &Path, ) -> Result, String> { 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!( current_dir = %current_openhuman_dir.display(), default_dir = %default_openhuman_dir.display(), - marker = %active_workspace_marker.display(), - "[config] reset_local_data: starting" + workspace_marker = %active_workspace_marker.display(), + user_marker = %active_user_marker.display(), + "[config] reset_local_data: starting (user-scoped)" ); let mut removed_paths = Vec::new(); - if active_workspace_marker.exists() { - if let Err(error) = tokio::fs::remove_file(&active_workspace_marker).await { - return Err(reset_local_data_marker_remove_error( - &active_workspace_marker, - &error, - )); + // Remove the two shared root-level markers so the current user is signed + // out and any non-default workspace pointer is dropped. Each is a single + // file under the root; the root itself is preserved for sibling users. + for marker in [&active_workspace_marker, &active_user_marker] { + 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] { - if !target_dir.exists() { - tracing::debug!( - dir = %target_dir.display(), - "[config] reset_local_data: directory already absent" - ); - continue; - } - - if let Err(error) = tokio::fs::remove_dir_all(target_dir).await { - return Err(reset_local_data_remove_error(target_dir, &error)); + // Remove only the active user's directory — NOT the shared root, which + // contains other users' `users/` subtrees. + if current_openhuman_dir.exists() { + if let Err(error) = tokio::fs::remove_dir_all(current_openhuman_dir).await { + return Err(reset_local_data_remove_error(current_openhuman_dir, &error)); } tracing::debug!( - dir = %target_dir.display(), - "[config] reset_local_data: removed directory" + dir = %current_openhuman_dir.display(), + "[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( @@ -204,16 +230,11 @@ pub(crate) async fn reset_local_data_for_paths( "current_openhuman_dir": current_openhuman_dir.display().to_string(), "default_openhuman_dir": default_openhuman_dir.display().to_string(), }), - vec![ - format!( - "reset local data for active config dir {}", - current_openhuman_dir.display() - ), - format!( - "removed default data dir {} if present", - default_openhuman_dir.display() - ), - ], + vec![format!( + "reset local data for active user dir {} (shared root {} preserved)", + current_openhuman_dir.display(), + default_openhuman_dir.display() + )], )) } @@ -537,11 +558,18 @@ pub async fn get_data_paths() -> Result, String> { let current_openhuman_dir = config_openhuman_dir(&config); let default_openhuman_dir = 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/` 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( json!({ "current_openhuman_dir": current_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_user_marker_path": active_user_marker.display().to_string(), }), vec![format!( "data paths resolved (current={}, default={})", diff --git a/src/openhuman/config/ops_tests.rs b/src/openhuman/config/ops_tests.rs index ae2db12a2..da16ccbe2 100644 --- a/src/openhuman/config/ops_tests.rs +++ b/src/openhuman/config/ops_tests.rs @@ -2,19 +2,22 @@ use super::*; use tempfile::tempdir; #[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 default_openhuman_dir = temp.path().join("default-openhuman"); - let current_openhuman_dir = temp.path().join("custom-openhuman"); - let marker = active_workspace_marker_path(&default_openhuman_dir); + // Active user lives under the shared root's `users/` tree, mirroring the + // real layout (`~/.openhuman/users/`). + 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")) .await .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 .unwrap(); @@ -22,8 +25,12 @@ async fn reset_local_data_removes_current_dir_default_dir_and_marker() { .await .unwrap(); + // Active user's slice and both shared markers are gone … 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 .value .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())); } +#[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(¤t_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(¤t_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 ──────────────────────────────────────────── use crate::openhuman::config::TEST_ENV_LOCK as ENV_LOCK; diff --git a/src/openhuman/config/schema/load/dirs.rs b/src/openhuman/config/schema/load/dirs.rs index d15f2fd9d..388d7b80d 100644 --- a/src/openhuman/config/schema/load/dirs.rs +++ b/src/openhuman/config/schema/load/dirs.rs @@ -6,8 +6,8 @@ use std::path::{Path, PathBuf}; use tokio::fs; pub use load_user_state::{ - clear_active_user, pre_login_user_dir, read_active_user_id, user_openhuman_dir, - write_active_user_id, PRE_LOGIN_USER_ID, + active_user_marker_path, clear_active_user, pre_login_user_dir, read_active_user_id, + user_openhuman_dir, write_active_user_id, PRE_LOGIN_USER_ID, }; #[path = "../load_user_state.rs"] diff --git a/src/openhuman/config/schema/load/mod.rs b/src/openhuman/config/schema/load/mod.rs index 1b6137ced..43fd7ef0a 100644 --- a/src/openhuman/config/schema/load/mod.rs +++ b/src/openhuman/config/schema/load/mod.rs @@ -11,9 +11,9 @@ pub(crate) use env::EnvLookup; pub(crate) use env::ProcessEnv; pub use dirs::{ - action_dir_env_override, clear_active_user, default_action_dir, default_projects_dir, - default_root_openhuman_dir, pre_login_user_dir, read_active_user_id, resolve_action_dir, - user_openhuman_dir, write_active_user_id, ACTION_DIR_ENV_VAR, + action_dir_env_override, active_user_marker_path, clear_active_user, default_action_dir, + default_projects_dir, default_root_openhuman_dir, pre_login_user_dir, read_active_user_id, + 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, }; diff --git a/src/openhuman/config/schema/load_user_state.rs b/src/openhuman/config/schema/load_user_state.rs index 490903249..f4b0712ca 100644 --- a/src/openhuman/config/schema/load_user_state.rs +++ b/src/openhuman/config/schema/load_user_state.rs @@ -10,10 +10,22 @@ struct ActiveUserState { 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/` 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`. /// 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 { - 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 state: ActiveUserState = toml::from_str(&contents).ok()?; 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 /// use the default (unauthenticated) openhuman directory. 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() { std::fs::remove_file(&path) .with_context(|| format!("Failed to remove active user state: {}", path.display()))?; diff --git a/src/openhuman/config/schema/mod.rs b/src/openhuman/config/schema/mod.rs index d42397792..d1450b583 100644 --- a/src/openhuman/config/schema/mod.rs +++ b/src/openhuman/config/schema/mod.rs @@ -24,9 +24,9 @@ mod identity_cost; mod learning; mod load; pub use load::{ - action_dir_env_override, clear_active_user, default_action_dir, default_projects_dir, - default_root_openhuman_dir, pre_login_user_dir, read_active_user_id, resolve_action_dir, - user_openhuman_dir, write_active_user_id, PRE_LOGIN_USER_ID, + action_dir_env_override, active_user_marker_path, clear_active_user, default_action_dir, + default_projects_dir, default_root_openhuman_dir, pre_login_user_dir, read_active_user_id, + resolve_action_dir, user_openhuman_dir, write_active_user_id, PRE_LOGIN_USER_ID, }; pub mod claude_agent_sdk; pub use claude_agent_sdk::ClaudeAgentSdkConfig;