mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
feat: move feedback to sidebar footer, rename World card, fix Clear App Data scoping (#3945)
This commit is contained in:
@@ -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),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 />
|
||||
·
|
||||
<span className="text-[10px] text-stone-400 dark:text-neutral-500">
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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 ─────────────────────────────────────────────────────
|
||||
|
||||
@@ -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,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,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.',
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -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,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.',
|
||||
|
||||
@@ -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': 'फेल्ट टेबल के चारों ओर आठ सीटें।',
|
||||
|
||||
@@ -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,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.',
|
||||
|
||||
@@ -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,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,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.',
|
||||
|
||||
@@ -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': 'Восемь мест вокруг стола с сукном.',
|
||||
|
||||
@@ -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': '八个座位围绕一张毡面牌桌。',
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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::{
|
||||
|
||||
@@ -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={})",
|
||||
|
||||
@@ -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(¤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;
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
@@ -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()))?;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user