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
/// 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/<id>` 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/<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 {
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/<id>` 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<i32>) -> bool {
@@ -306,10 +321,15 @@ async fn fetch_data_paths() -> Result<ResolvedDataPaths, String> {
.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),
})
}
+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">
<h1 className="text-lg font-semibold text-white">
{t('agentWorld.world.title', 'Agent World')}
{t('agentWorld.world.title', 'Tiny Place')}
</h1>
<p className="mt-1 text-xs leading-relaxed text-neutral-300">
{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.'
)}
</p>
</div>
+38 -1
View File
@@ -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 (
<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">
@@ -46,9 +66,26 @@ export default function AppSidebar() {
app rail above its thread list) can order them via Tailwind `order-*`. */}
<SidebarSlotOutlet className="flex h-full flex-col" />
</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
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 />
&middot;
<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';
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();
});
+2 -2
View File
@@ -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 ─────────────────────────────────────────────────────
+5 -4
View File
@@ -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': 'ثمانية مقاعد حول طاولة مكسوة باللباد.',
+4 -4
View File
@@ -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': 'ফেল্ট টেবিলের চারপাশে আটটি আসন।',
+4 -4
View File
@@ -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.',
+4 -4
View File
@@ -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.',
+5 -4
View File
@@ -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.',
+4 -4
View File
@@ -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 dautres 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 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().
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': 'फेल्ट टेबल के चारों ओर आठ सीटें।',
+5 -4
View File
@@ -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.',
+4 -4
View File
@@ -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 unidea.',
'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.',
+4 -4
View File
@@ -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': '펠트 테이블을 둘러싼 여덟 좌석.',
+4 -4
View File
@@ -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.',
+4 -4
View File
@@ -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.',
+5 -4
View File
@@ -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': 'Восемь мест вокруг стола с сукном.',
+5 -4
View File
@@ -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': '八个座位围绕一张毡面牌桌。',
+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.
// 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",
+3 -3
View File
@@ -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::{
+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 {
// 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/<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(
current_openhuman_dir: &Path,
default_openhuman_dir: &Path,
) -> Result<RpcOutcome<serde_json::Value>, 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/<id>` 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<RpcOutcome<serde_json::Value>, 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/<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(
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={})",
+64 -8
View File
@@ -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/<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"))
.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(&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 ────────────────────────────────────────────
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;
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"]
+3 -3
View File
@@ -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,
};
+14 -2
View File
@@ -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/<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`.
/// 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> {
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()))?;
+3 -3
View File
@@ -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;