mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-28 05:12:33 +00:00
[1/5] feat(intelligence): surface own tiny.place identity + relay in Orchestration tab (#4588)
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import RelayBadge from './RelayBadge';
|
||||
|
||||
vi.mock('../../lib/i18n/I18nContext', () => ({ useT: () => ({ t: (k: string) => k }) }));
|
||||
|
||||
describe('RelayBadge', () => {
|
||||
it('renders nothing when relay info is absent', () => {
|
||||
const { container } = render(<RelayBadge relay={null} />);
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
|
||||
it('shows the staging label + base url and tags the network', () => {
|
||||
render(
|
||||
<RelayBadge relay={{ baseUrl: 'https://staging-api.tiny.place', network: 'staging' }} />
|
||||
);
|
||||
const badge = screen.getByTestId('tinyplace-relay-badge');
|
||||
expect(badge).toHaveAttribute('data-network', 'staging');
|
||||
expect(badge).toHaveAttribute('title', 'https://staging-api.tiny.place');
|
||||
expect(badge).toHaveTextContent('tinyplaceOrchestration.relay.staging');
|
||||
});
|
||||
|
||||
it('shows the production label for the prod relay', () => {
|
||||
render(<RelayBadge relay={{ baseUrl: 'https://api.tiny.place', network: 'prod' }} />);
|
||||
const badge = screen.getByTestId('tinyplace-relay-badge');
|
||||
expect(badge).toHaveAttribute('data-network', 'prod');
|
||||
expect(badge).toHaveTextContent('tinyplaceOrchestration.relay.prod');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* RelayBadge — a small always-visible pill naming the tiny.place relay the core
|
||||
* is talking to (STAGING / PRODUCTION).
|
||||
*
|
||||
* Why it exists: during live debugging, claudebot sat on `staging-api.tiny.place`
|
||||
* while the core silently defaulted to prod `api.tiny.place`, producing a mystery
|
||||
* 404. Nothing on screen said which relay either side used. Surfacing the target
|
||||
* relay as a first-class chip makes a mismatch obvious instead of opaque.
|
||||
*
|
||||
* Presentational only — the parent fetches {@link RelayInfo} via
|
||||
* `orchestrationClient.relayInfo()` and passes it down.
|
||||
*/
|
||||
import { useT } from '../../lib/i18n/I18nContext';
|
||||
import type { RelayInfo } from '../../lib/orchestration/orchestrationClient';
|
||||
|
||||
export interface RelayBadgeProps {
|
||||
relay: RelayInfo | null;
|
||||
}
|
||||
|
||||
export default function RelayBadge({ relay }: RelayBadgeProps): React.ReactElement | null {
|
||||
const { t } = useT();
|
||||
if (!relay) return null;
|
||||
|
||||
const isStaging = relay.network === 'staging';
|
||||
const label = isStaging
|
||||
? t('tinyplaceOrchestration.relay.staging')
|
||||
: t('tinyplaceOrchestration.relay.prod');
|
||||
|
||||
// Staging is the everyday dev target (amber, "you're on the test relay");
|
||||
// production is the live one (sage). Colour, not just text, so it's scannable.
|
||||
const tone = isStaging
|
||||
? 'border-amber-400 text-amber-700 dark:border-amber-500/60 dark:text-amber-300'
|
||||
: 'border-sage-400 text-sage-700 dark:border-sage-500/60 dark:text-sage-300';
|
||||
|
||||
return (
|
||||
<span
|
||||
data-testid="tinyplace-relay-badge"
|
||||
data-network={relay.network}
|
||||
title={relay.baseUrl}
|
||||
className={`inline-flex flex-none items-center rounded-md border px-1.5 py-0.5 font-mono text-[10px] font-bold uppercase tracking-wide ${tone}`}>
|
||||
{label}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import type { SelfIdentity } from '../../lib/orchestration/orchestrationClient';
|
||||
import SelfIdentityCard from './SelfIdentityCard';
|
||||
|
||||
vi.mock('../../lib/i18n/I18nContext', () => ({ useT: () => ({ t: (k: string) => k }) }));
|
||||
|
||||
const discoverable: SelfIdentity = {
|
||||
agentId: '6wNaBJkatir4B86cw5ykHZWQ3xoNaKygX5vAU9MQbHSh',
|
||||
handles: [{ username: 'openhuman', primary: true }],
|
||||
primaryHandle: 'openhuman',
|
||||
cardPublished: true,
|
||||
keyPublished: true,
|
||||
discoverable: true,
|
||||
};
|
||||
|
||||
describe('SelfIdentityCard', () => {
|
||||
beforeEach(() => {
|
||||
Object.assign(navigator, { clipboard: { writeText: vi.fn().mockResolvedValue(undefined) } });
|
||||
});
|
||||
|
||||
it('shows a loading state before the identity resolves', () => {
|
||||
render(<SelfIdentityCard identity={null} loading />);
|
||||
expect(screen.getByTestId('tinyplace-self-identity')).toHaveTextContent(
|
||||
'tinyplaceOrchestration.identity.loading'
|
||||
);
|
||||
});
|
||||
|
||||
it('renders nothing once loaded with no identity', () => {
|
||||
const { container } = render(<SelfIdentityCard identity={null} loading={false} />);
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
|
||||
it('renders the primary handle, shortened address and discoverable status', () => {
|
||||
render(<SelfIdentityCard identity={discoverable} loading={false} />);
|
||||
expect(screen.getByText('@openhuman')).toBeInTheDocument();
|
||||
// Address is shortened but the full value is preserved in the title.
|
||||
expect(screen.getByText('6wNaBJ…QbHSh')).toHaveAttribute('title', discoverable.agentId);
|
||||
const status = screen.getByTestId('tinyplace-self-identity-status');
|
||||
expect(status).toHaveAttribute('data-discoverable', 'true');
|
||||
});
|
||||
|
||||
it('flags an un-messageable identity and shows the register hint', () => {
|
||||
const undiscoverable: SelfIdentity = {
|
||||
agentId: 'addrWithNoCardPublishedYet',
|
||||
handles: [],
|
||||
cardPublished: false,
|
||||
keyPublished: false,
|
||||
discoverable: false,
|
||||
};
|
||||
render(<SelfIdentityCard identity={undiscoverable} loading={false} />);
|
||||
expect(screen.getByText('tinyplaceOrchestration.identity.noHandle')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('tinyplace-self-identity-status')).toHaveAttribute(
|
||||
'data-discoverable',
|
||||
'false'
|
||||
);
|
||||
expect(
|
||||
screen.getByText('tinyplaceOrchestration.identity.undiscoverableHint')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('copies the address to the clipboard on click', async () => {
|
||||
render(<SelfIdentityCard identity={discoverable} loading={false} />);
|
||||
fireEvent.click(screen.getByTestId('tinyplace-self-identity-copy'));
|
||||
expect(navigator.clipboard.writeText).toHaveBeenCalledWith(discoverable.agentId);
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId('tinyplace-self-identity-copy')).toHaveTextContent(
|
||||
'tinyplaceOrchestration.identity.copied'
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,134 @@
|
||||
/**
|
||||
* SelfIdentityCard — pinned at the top of the roster, shows OpenHuman's *own*
|
||||
* tiny.place identity so the user can hand it to a peer to be messaged inbound.
|
||||
*
|
||||
* Why it exists: the tab previously showed peers/contacts but never OpenHuman's
|
||||
* own agent id, so there was no way to say "here's my address, DM me". Worse, a
|
||||
* fresh identity can accept contacts yet stay un-messageable until it registers a
|
||||
* @handle (which is what publishes its directory card + Signal key) — every send
|
||||
* to it 404s on `/directory/agents/<addr>`. This card surfaces both the address
|
||||
* and the discoverability gap instead of leaving it a mystery.
|
||||
*
|
||||
* Presentational only — the parent fetches {@link SelfIdentity} via
|
||||
* `orchestrationClient.selfIdentity()` and owns refresh.
|
||||
*/
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import { useT } from '../../lib/i18n/I18nContext';
|
||||
import type { SelfIdentity } from '../../lib/orchestration/orchestrationClient';
|
||||
|
||||
export interface SelfIdentityCardProps {
|
||||
identity: SelfIdentity | null;
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
function shortAddress(address: string): string {
|
||||
if (address.length <= 14) return address;
|
||||
return `${address.slice(0, 6)}…${address.slice(-5)}`;
|
||||
}
|
||||
|
||||
export default function SelfIdentityCard({
|
||||
identity,
|
||||
loading,
|
||||
}: SelfIdentityCardProps): React.ReactElement {
|
||||
const { t } = useT();
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const address = identity?.agentId ?? '';
|
||||
const onCopy = useCallback(() => {
|
||||
if (!address) return;
|
||||
void navigator.clipboard?.writeText(address).then(
|
||||
() => {
|
||||
setCopied(true);
|
||||
window.setTimeout(() => setCopied(false), 1500);
|
||||
},
|
||||
() => setCopied(false)
|
||||
);
|
||||
}, [address]);
|
||||
|
||||
if (loading && !identity) {
|
||||
return (
|
||||
<section
|
||||
data-testid="tinyplace-self-identity"
|
||||
className="border-b border-line bg-surface-muted/40 px-4 py-3 text-[11px] text-content-faint">
|
||||
{t('tinyplaceOrchestration.identity.loading')}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
if (!identity) return <></>;
|
||||
|
||||
const primaryHandle = identity.primaryHandle ?? identity.handles[0]?.username;
|
||||
|
||||
return (
|
||||
<section
|
||||
data-testid="tinyplace-self-identity"
|
||||
className="border-b border-line bg-surface-muted/40 px-4 py-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
aria-hidden
|
||||
className="flex h-6 w-6 flex-none items-center justify-center rounded-md bg-sage-500 font-mono text-[11px] font-bold text-white">
|
||||
OH
|
||||
</span>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="truncate text-xs font-semibold text-content">
|
||||
{primaryHandle ? `@${primaryHandle}` : t('tinyplaceOrchestration.identity.noHandle')}
|
||||
</div>
|
||||
<div className="truncate font-mono text-[10px] text-content-faint" title={address}>
|
||||
{shortAddress(address)}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
data-testid="tinyplace-self-identity-copy"
|
||||
onClick={onCopy}
|
||||
disabled={!address}
|
||||
className="flex-none rounded-md px-1.5 py-0.5 text-[11px] font-semibold text-ocean-600 transition hover:bg-ocean-500/10 disabled:opacity-40 dark:text-ocean-300">
|
||||
{copied
|
||||
? t('tinyplaceOrchestration.identity.copied')
|
||||
: t('tinyplaceOrchestration.identity.copy')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="mt-2 flex flex-wrap items-center gap-1.5">
|
||||
{identity.discoverable ? (
|
||||
<span
|
||||
data-testid="tinyplace-self-identity-status"
|
||||
data-discoverable="true"
|
||||
className="inline-flex items-center rounded-full bg-sage-500/10 px-2 py-0.5 text-[10px] font-semibold text-sage-700 dark:text-sage-300">
|
||||
{t('tinyplaceOrchestration.identity.discoverable')}
|
||||
</span>
|
||||
) : (
|
||||
<span
|
||||
data-testid="tinyplace-self-identity-status"
|
||||
data-discoverable="false"
|
||||
className="inline-flex items-center rounded-full bg-coral-500/10 px-2 py-0.5 text-[10px] font-semibold text-coral-700 dark:text-coral-300">
|
||||
{t('tinyplaceOrchestration.identity.undiscoverable')}
|
||||
</span>
|
||||
)}
|
||||
<span
|
||||
className="inline-flex items-center gap-1 rounded-full bg-surface-strong px-2 py-0.5 text-[10px] text-content-muted"
|
||||
title={t('tinyplaceOrchestration.identity.card')}>
|
||||
{t('tinyplaceOrchestration.identity.card')}:{' '}
|
||||
{identity.cardPublished
|
||||
? t('tinyplaceOrchestration.identity.published')
|
||||
: t('tinyplaceOrchestration.identity.notPublished')}
|
||||
</span>
|
||||
<span
|
||||
className="inline-flex items-center gap-1 rounded-full bg-surface-strong px-2 py-0.5 text-[10px] text-content-muted"
|
||||
title={t('tinyplaceOrchestration.identity.key')}>
|
||||
{t('tinyplaceOrchestration.identity.key')}:{' '}
|
||||
{identity.keyPublished
|
||||
? t('tinyplaceOrchestration.identity.published')
|
||||
: t('tinyplaceOrchestration.identity.notPublished')}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{!identity.discoverable ? (
|
||||
<p className="mt-2 rounded-md bg-coral-50 px-2 py-1 text-[10px] text-coral-700 dark:bg-coral-500/10 dark:text-coral-300">
|
||||
{t('tinyplaceOrchestration.identity.undiscoverableHint')}
|
||||
</p>
|
||||
) : null}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -30,6 +30,8 @@ vi.mock('../../lib/orchestration/orchestrationClient', async importOriginal => {
|
||||
sendMasterMessage: vi.fn(),
|
||||
markRead: vi.fn(),
|
||||
status: vi.fn(),
|
||||
selfIdentity: vi.fn(),
|
||||
relayInfo: vi.fn(),
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -50,6 +52,8 @@ const messagesListMock = vi.mocked(orchestrationClient.messagesList);
|
||||
const sendMasterMock = vi.mocked(orchestrationClient.sendMasterMessage);
|
||||
const markReadMock = vi.mocked(orchestrationClient.markRead);
|
||||
const statusMock = vi.mocked(orchestrationClient.status);
|
||||
const selfIdentityMock = vi.mocked(orchestrationClient.selfIdentity);
|
||||
const relayInfoMock = vi.mocked(orchestrationClient.relayInfo);
|
||||
|
||||
const pairingListMock = vi.mocked(apiClient.orchestrationPairing.list);
|
||||
const pairingLinkMock = vi.mocked(apiClient.orchestrationPairing.linkSession);
|
||||
@@ -105,6 +109,18 @@ describe('TinyPlaceOrchestrationTab', () => {
|
||||
sendMasterMock.mockResolvedValue({ ok: true, messageId: 'm-1' });
|
||||
markReadMock.mockResolvedValue({ ok: true });
|
||||
statusMock.mockResolvedValue({});
|
||||
selfIdentityMock.mockResolvedValue({
|
||||
agentId: '6wNaBJkatir4B86cw5ykHZWQ3xoNaKygX5vAU9MQbHSh',
|
||||
handles: [{ username: 'openhuman', primary: true }],
|
||||
primaryHandle: 'openhuman',
|
||||
cardPublished: true,
|
||||
keyPublished: true,
|
||||
discoverable: true,
|
||||
});
|
||||
relayInfoMock.mockResolvedValue({
|
||||
baseUrl: 'https://staging-api.tiny.place',
|
||||
network: 'staging',
|
||||
});
|
||||
pairingListMock.mockResolvedValue({
|
||||
records: [],
|
||||
contacts: { contacts: [] },
|
||||
@@ -191,6 +207,19 @@ describe('TinyPlaceOrchestrationTab', () => {
|
||||
expect(screen.getByText('OpenHuman app session')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('keeps the relay badge visible when identity discovery fails (locked wallet)', async () => {
|
||||
// selfIdentity() builds the tiny.place client from the wallet and can reject;
|
||||
// relayInfo() only reads the base URL and must stay visible regardless.
|
||||
selfIdentityMock.mockRejectedValue(new Error('wallet locked'));
|
||||
|
||||
render(<TinyPlaceOrchestrationTab />);
|
||||
|
||||
const badge = await screen.findByTestId('tinyplace-relay-badge');
|
||||
expect(badge).toHaveAttribute('data-network', 'staging');
|
||||
// Identity read failed → its card must not render.
|
||||
expect(screen.queryByTestId('tinyplace-self-identity')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('loads and renders messages for the opened chat', async () => {
|
||||
sessionsListMock.mockResolvedValue({
|
||||
sessions: [
|
||||
|
||||
@@ -9,6 +9,11 @@ import {
|
||||
PaymentRequiredError,
|
||||
} from '../../lib/agentworld/invokeApiClient';
|
||||
import { useT } from '../../lib/i18n/I18nContext';
|
||||
import {
|
||||
orchestrationClient,
|
||||
type RelayInfo,
|
||||
type SelfIdentity,
|
||||
} from '../../lib/orchestration/orchestrationClient';
|
||||
import {
|
||||
type ChatMessage,
|
||||
type ChatWindow,
|
||||
@@ -17,6 +22,8 @@ import {
|
||||
} from '../../lib/orchestration/useOrchestrationChats';
|
||||
import { subconsciousTrigger } from '../../utils/tauriCommands/subconscious';
|
||||
import Button from '../ui/Button';
|
||||
import RelayBadge from './RelayBadge';
|
||||
import SelfIdentityCard from './SelfIdentityCard';
|
||||
|
||||
const debug = debugFactory('brain:tinyplace-orchestration');
|
||||
|
||||
@@ -213,6 +220,12 @@ export default function TinyPlaceOrchestrationTab() {
|
||||
// Which contact rows are expanded to reveal their nested sessions.
|
||||
const [expandedContacts, setExpandedContacts] = useState<Record<string, boolean>>({});
|
||||
const [creatingSession, setCreatingSession] = useState<string | null>(null);
|
||||
// Own tiny.place identity (discoverability) + the relay the core is on. Both
|
||||
// best-effort: a failed read leaves the card/badge hidden rather than erroring
|
||||
// the whole tab.
|
||||
const [selfIdentity, setSelfIdentity] = useState<SelfIdentity | null>(null);
|
||||
const [identityLoading, setIdentityLoading] = useState(true);
|
||||
const [relayInfo, setRelayInfo] = useState<RelayInfo | null>(null);
|
||||
const mountedRef = useRef(true);
|
||||
|
||||
const toggleContact = useCallback((address: string) => {
|
||||
@@ -257,6 +270,40 @@ export default function TinyPlaceOrchestrationTab() {
|
||||
}
|
||||
}, []);
|
||||
|
||||
const loadIdentity = useCallback(async () => {
|
||||
debug('[tinyplace-orchestration] identity load entry');
|
||||
// Identity and relay are independent reads: selfIdentity() builds the
|
||||
// tiny.place client from the wallet and can reject (locked/unconfigured
|
||||
// wallet), but relayInfo() only reads the configured base URL and must
|
||||
// stay visible regardless. Settle them separately so one failure never
|
||||
// hides the other. Neither failure may break the chat surface.
|
||||
const [identityResult, relayResult] = await Promise.allSettled([
|
||||
orchestrationClient.selfIdentity(),
|
||||
orchestrationClient.relayInfo(),
|
||||
]);
|
||||
if (!mountedRef.current) return;
|
||||
if (identityResult.status === 'fulfilled') {
|
||||
debug(
|
||||
'[tinyplace-orchestration] identity load ok discoverable=%s',
|
||||
identityResult.value.discoverable
|
||||
);
|
||||
setSelfIdentity(identityResult.value);
|
||||
} else {
|
||||
const reason = identityResult.reason;
|
||||
const message = reason instanceof Error ? reason.message : String(reason);
|
||||
debug('[tinyplace-orchestration] identity load error %s', message);
|
||||
}
|
||||
if (relayResult.status === 'fulfilled') {
|
||||
debug('[tinyplace-orchestration] relay load ok network=%s', relayResult.value.network);
|
||||
setRelayInfo(relayResult.value);
|
||||
} else {
|
||||
const reason = relayResult.reason;
|
||||
const message = reason instanceof Error ? reason.message : String(reason);
|
||||
debug('[tinyplace-orchestration] relay load error %s', message);
|
||||
}
|
||||
setIdentityLoading(false);
|
||||
}, []);
|
||||
|
||||
const runPairingAction = useCallback(
|
||||
async (actionId: string, action: () => Promise<unknown>) => {
|
||||
debug('[tinyplace-orchestration] pairing action entry id=%s', actionId);
|
||||
@@ -297,7 +344,8 @@ export default function TinyPlaceOrchestrationTab() {
|
||||
const refreshAll = useCallback(() => {
|
||||
void refresh();
|
||||
void loadPairing();
|
||||
}, [refresh, loadPairing]);
|
||||
void loadIdentity();
|
||||
}, [refresh, loadPairing, loadIdentity]);
|
||||
|
||||
const submitComposer = useCallback(
|
||||
(event: FormEvent<HTMLFormElement>) => {
|
||||
@@ -316,12 +364,15 @@ export default function TinyPlaceOrchestrationTab() {
|
||||
|
||||
useEffect(() => {
|
||||
mountedRef.current = true;
|
||||
const handle = window.setTimeout(() => void loadPairing(), 0);
|
||||
const handle = window.setTimeout(() => {
|
||||
void loadPairing();
|
||||
void loadIdentity();
|
||||
}, 0);
|
||||
return () => {
|
||||
window.clearTimeout(handle);
|
||||
mountedRef.current = false;
|
||||
};
|
||||
}, [loadPairing]);
|
||||
}, [loadPairing, loadIdentity]);
|
||||
|
||||
const pinned = chats.filter(chat => chat.pinned);
|
||||
const sessions = chats
|
||||
@@ -416,9 +467,12 @@ export default function TinyPlaceOrchestrationTab() {
|
||||
<div className="border-b border-line px-4 py-3">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<h3 className="truncate text-sm font-semibold text-content">
|
||||
{t('tinyplaceOrchestration.title')}
|
||||
</h3>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<h3 className="truncate text-sm font-semibold text-content">
|
||||
{t('tinyplaceOrchestration.title')}
|
||||
</h3>
|
||||
<RelayBadge relay={relayInfo} />
|
||||
</div>
|
||||
<p className="mt-0.5 truncate text-[11px] text-content-muted">
|
||||
{t('tinyplaceOrchestration.subtitle')}
|
||||
</p>
|
||||
@@ -444,6 +498,8 @@ export default function TinyPlaceOrchestrationTab() {
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<SelfIdentityCard identity={selfIdentity} loading={identityLoading} />
|
||||
|
||||
<section className="border-b border-line px-4 py-3">
|
||||
<form className="space-y-2" onSubmit={submitLink}>
|
||||
<label
|
||||
|
||||
@@ -288,6 +288,20 @@ const messages: TranslationMap = {
|
||||
'tinyplaceOrchestration.composer.send': 'إرسال',
|
||||
'tinyplaceOrchestration.composer.sendFailed': 'فشل إرسال الرسالة',
|
||||
'tinyplaceOrchestration.steering.label': 'التوجيه',
|
||||
'tinyplaceOrchestration.relay.staging': 'بيئة التجهيز',
|
||||
'tinyplaceOrchestration.relay.prod': 'الإنتاج',
|
||||
'tinyplaceOrchestration.identity.loading': 'جارٍ تحميل الهوية…',
|
||||
'tinyplaceOrchestration.identity.noHandle': 'لا يوجد @handle بعد',
|
||||
'tinyplaceOrchestration.identity.copy': 'نسخ',
|
||||
'tinyplaceOrchestration.identity.copied': 'تم النسخ',
|
||||
'tinyplaceOrchestration.identity.discoverable': 'قابل للاكتشاف',
|
||||
'tinyplaceOrchestration.identity.undiscoverable': 'غير قابل للاكتشاف',
|
||||
'tinyplaceOrchestration.identity.undiscoverableHint':
|
||||
'سجّل @handle حتى يتمكن الآخرون من مراسلتك.',
|
||||
'tinyplaceOrchestration.identity.card': 'بطاقة الدليل',
|
||||
'tinyplaceOrchestration.identity.key': 'مفتاح التشفير',
|
||||
'tinyplaceOrchestration.identity.published': 'منشور',
|
||||
'tinyplaceOrchestration.identity.notPublished': 'غير منشور',
|
||||
'tinyplaceOrchestration.roster.instances': 'مثيلات',
|
||||
'tinyplaceOrchestration.roster.empty': 'لا توجد مثيلات وكيل بعد',
|
||||
'tinyplaceOrchestration.roster.other': 'أخرى',
|
||||
|
||||
@@ -295,6 +295,20 @@ const messages: TranslationMap = {
|
||||
'tinyplaceOrchestration.composer.send': 'পাঠান',
|
||||
'tinyplaceOrchestration.composer.sendFailed': 'বার্তা পাঠাতে ব্যর্থ',
|
||||
'tinyplaceOrchestration.steering.label': 'নির্দেশনা',
|
||||
'tinyplaceOrchestration.relay.staging': 'স্টেজিং',
|
||||
'tinyplaceOrchestration.relay.prod': 'প্রোডাকশন',
|
||||
'tinyplaceOrchestration.identity.loading': 'পরিচয় লোড হচ্ছে…',
|
||||
'tinyplaceOrchestration.identity.noHandle': 'এখনও কোনো @handle নেই',
|
||||
'tinyplaceOrchestration.identity.copy': 'কপি করুন',
|
||||
'tinyplaceOrchestration.identity.copied': 'কপি হয়েছে',
|
||||
'tinyplaceOrchestration.identity.discoverable': 'খুঁজে পাওয়া যায়',
|
||||
'tinyplaceOrchestration.identity.undiscoverable': 'খুঁজে পাওয়া যায় না',
|
||||
'tinyplaceOrchestration.identity.undiscoverableHint':
|
||||
'একটি @handle নিবন্ধন করুন যাতে অন্য এজেন্টরা আপনাকে বার্তা পাঠাতে পারে।',
|
||||
'tinyplaceOrchestration.identity.card': 'ডিরেক্টরি কার্ড',
|
||||
'tinyplaceOrchestration.identity.key': 'এনক্রিপশন কী',
|
||||
'tinyplaceOrchestration.identity.published': 'প্রকাশিত',
|
||||
'tinyplaceOrchestration.identity.notPublished': 'অপ্রকাশিত',
|
||||
'tinyplaceOrchestration.roster.instances': 'ইনস্ট্যান্স',
|
||||
'tinyplaceOrchestration.roster.empty': 'এখনও কোনো এজেন্ট ইনস্ট্যান্স নেই',
|
||||
'tinyplaceOrchestration.roster.other': 'অন্যান্য',
|
||||
|
||||
@@ -302,6 +302,20 @@ const messages: TranslationMap = {
|
||||
'tinyplaceOrchestration.composer.send': 'Senden',
|
||||
'tinyplaceOrchestration.composer.sendFailed': 'Nachricht konnte nicht gesendet werden',
|
||||
'tinyplaceOrchestration.steering.label': 'Steuerung',
|
||||
'tinyplaceOrchestration.relay.staging': 'Staging',
|
||||
'tinyplaceOrchestration.relay.prod': 'Produktion',
|
||||
'tinyplaceOrchestration.identity.loading': 'Identität wird geladen…',
|
||||
'tinyplaceOrchestration.identity.noHandle': 'Noch kein @handle',
|
||||
'tinyplaceOrchestration.identity.copy': 'Kopieren',
|
||||
'tinyplaceOrchestration.identity.copied': 'Kopiert',
|
||||
'tinyplaceOrchestration.identity.discoverable': 'Auffindbar',
|
||||
'tinyplaceOrchestration.identity.undiscoverable': 'Nicht auffindbar',
|
||||
'tinyplaceOrchestration.identity.undiscoverableHint':
|
||||
'Registriere ein @handle, damit Peers dir schreiben können.',
|
||||
'tinyplaceOrchestration.identity.card': 'Verzeichniskarte',
|
||||
'tinyplaceOrchestration.identity.key': 'Verschlüsselungsschlüssel',
|
||||
'tinyplaceOrchestration.identity.published': 'Veröffentlicht',
|
||||
'tinyplaceOrchestration.identity.notPublished': 'Nicht veröffentlicht',
|
||||
'tinyplaceOrchestration.roster.instances': 'Instanzen',
|
||||
'tinyplaceOrchestration.roster.empty': 'Noch keine Agent-Instanzen',
|
||||
'tinyplaceOrchestration.roster.other': 'Andere',
|
||||
|
||||
@@ -4223,6 +4223,20 @@ const en: TranslationMap = {
|
||||
'tinyplaceOrchestration.composer.send': 'Send',
|
||||
'tinyplaceOrchestration.composer.sendFailed': 'Failed to send message',
|
||||
'tinyplaceOrchestration.steering.label': 'Steering',
|
||||
'tinyplaceOrchestration.relay.staging': 'Staging',
|
||||
'tinyplaceOrchestration.relay.prod': 'Production',
|
||||
'tinyplaceOrchestration.identity.loading': 'Loading identity…',
|
||||
'tinyplaceOrchestration.identity.noHandle': 'No @handle yet',
|
||||
'tinyplaceOrchestration.identity.copy': 'Copy',
|
||||
'tinyplaceOrchestration.identity.copied': 'Copied',
|
||||
'tinyplaceOrchestration.identity.discoverable': 'Discoverable',
|
||||
'tinyplaceOrchestration.identity.undiscoverable': 'Not discoverable',
|
||||
'tinyplaceOrchestration.identity.undiscoverableHint':
|
||||
'Register a @handle so peers can message you.',
|
||||
'tinyplaceOrchestration.identity.card': 'Directory card',
|
||||
'tinyplaceOrchestration.identity.key': 'Encryption key',
|
||||
'tinyplaceOrchestration.identity.published': 'Published',
|
||||
'tinyplaceOrchestration.identity.notPublished': 'Not published',
|
||||
'tinyplaceOrchestration.roster.instances': 'Instances',
|
||||
'tinyplaceOrchestration.roster.empty': 'No agent instances yet',
|
||||
'tinyplaceOrchestration.roster.other': 'Other',
|
||||
|
||||
@@ -299,6 +299,20 @@ const messages: TranslationMap = {
|
||||
'tinyplaceOrchestration.composer.send': 'Enviar',
|
||||
'tinyplaceOrchestration.composer.sendFailed': 'No se pudo enviar el mensaje',
|
||||
'tinyplaceOrchestration.steering.label': 'Dirección',
|
||||
'tinyplaceOrchestration.relay.staging': 'Staging',
|
||||
'tinyplaceOrchestration.relay.prod': 'Producción',
|
||||
'tinyplaceOrchestration.identity.loading': 'Cargando identidad…',
|
||||
'tinyplaceOrchestration.identity.noHandle': 'Aún sin @handle',
|
||||
'tinyplaceOrchestration.identity.copy': 'Copiar',
|
||||
'tinyplaceOrchestration.identity.copied': 'Copiado',
|
||||
'tinyplaceOrchestration.identity.discoverable': 'Detectable',
|
||||
'tinyplaceOrchestration.identity.undiscoverable': 'No detectable',
|
||||
'tinyplaceOrchestration.identity.undiscoverableHint':
|
||||
'Registra un @handle para que otros agentes puedan escribirte.',
|
||||
'tinyplaceOrchestration.identity.card': 'Tarjeta de directorio',
|
||||
'tinyplaceOrchestration.identity.key': 'Clave de cifrado',
|
||||
'tinyplaceOrchestration.identity.published': 'Publicado',
|
||||
'tinyplaceOrchestration.identity.notPublished': 'No publicado',
|
||||
'tinyplaceOrchestration.roster.instances': 'Instancias',
|
||||
'tinyplaceOrchestration.roster.empty': 'Aún no hay instancias de agente',
|
||||
'tinyplaceOrchestration.roster.other': 'Otros',
|
||||
|
||||
@@ -299,6 +299,20 @@ const messages: TranslationMap = {
|
||||
'tinyplaceOrchestration.composer.send': 'Envoyer',
|
||||
'tinyplaceOrchestration.composer.sendFailed': 'Échec de l’envoi du message',
|
||||
'tinyplaceOrchestration.steering.label': 'Pilotage',
|
||||
'tinyplaceOrchestration.relay.staging': 'Staging',
|
||||
'tinyplaceOrchestration.relay.prod': 'Production',
|
||||
'tinyplaceOrchestration.identity.loading': 'Chargement de l’identité…',
|
||||
'tinyplaceOrchestration.identity.noHandle': 'Pas encore de @handle',
|
||||
'tinyplaceOrchestration.identity.copy': 'Copier',
|
||||
'tinyplaceOrchestration.identity.copied': 'Copié',
|
||||
'tinyplaceOrchestration.identity.discoverable': 'Découvrable',
|
||||
'tinyplaceOrchestration.identity.undiscoverable': 'Non découvrable',
|
||||
'tinyplaceOrchestration.identity.undiscoverableHint':
|
||||
'Enregistre un @handle pour que les pairs puissent te contacter.',
|
||||
'tinyplaceOrchestration.identity.card': 'Fiche d’annuaire',
|
||||
'tinyplaceOrchestration.identity.key': 'Clé de chiffrement',
|
||||
'tinyplaceOrchestration.identity.published': 'Publié',
|
||||
'tinyplaceOrchestration.identity.notPublished': 'Non publié',
|
||||
'tinyplaceOrchestration.roster.instances': 'Instances',
|
||||
'tinyplaceOrchestration.roster.empty': 'Aucune instance d’agent',
|
||||
'tinyplaceOrchestration.roster.other': 'Autre',
|
||||
|
||||
@@ -295,6 +295,20 @@ const messages: TranslationMap = {
|
||||
'tinyplaceOrchestration.composer.send': 'भेजें',
|
||||
'tinyplaceOrchestration.composer.sendFailed': 'संदेश भेजने में विफल',
|
||||
'tinyplaceOrchestration.steering.label': 'मार्गदर्शन',
|
||||
'tinyplaceOrchestration.relay.staging': 'स्टेजिंग',
|
||||
'tinyplaceOrchestration.relay.prod': 'प्रोडक्शन',
|
||||
'tinyplaceOrchestration.identity.loading': 'पहचान लोड हो रही है…',
|
||||
'tinyplaceOrchestration.identity.noHandle': 'अभी तक कोई @handle नहीं',
|
||||
'tinyplaceOrchestration.identity.copy': 'कॉपी करें',
|
||||
'tinyplaceOrchestration.identity.copied': 'कॉपी हो गया',
|
||||
'tinyplaceOrchestration.identity.discoverable': 'खोजने योग्य',
|
||||
'tinyplaceOrchestration.identity.undiscoverable': 'खोजने योग्य नहीं',
|
||||
'tinyplaceOrchestration.identity.undiscoverableHint':
|
||||
'एक @handle पंजीकृत करें ताकि अन्य एजेंट आपको संदेश भेज सकें।',
|
||||
'tinyplaceOrchestration.identity.card': 'डायरेक्टरी कार्ड',
|
||||
'tinyplaceOrchestration.identity.key': 'एन्क्रिप्शन कुंजी',
|
||||
'tinyplaceOrchestration.identity.published': 'प्रकाशित',
|
||||
'tinyplaceOrchestration.identity.notPublished': 'अप्रकाशित',
|
||||
'tinyplaceOrchestration.roster.instances': 'इंस्टेंस',
|
||||
'tinyplaceOrchestration.roster.empty': 'अभी तक कोई एजेंट इंस्टेंस नहीं',
|
||||
'tinyplaceOrchestration.roster.other': 'अन्य',
|
||||
|
||||
@@ -296,6 +296,20 @@ const messages: TranslationMap = {
|
||||
'tinyplaceOrchestration.composer.send': 'Kirim',
|
||||
'tinyplaceOrchestration.composer.sendFailed': 'Gagal mengirim pesan',
|
||||
'tinyplaceOrchestration.steering.label': 'Pengarahan',
|
||||
'tinyplaceOrchestration.relay.staging': 'Staging',
|
||||
'tinyplaceOrchestration.relay.prod': 'Produksi',
|
||||
'tinyplaceOrchestration.identity.loading': 'Memuat identitas…',
|
||||
'tinyplaceOrchestration.identity.noHandle': 'Belum ada @handle',
|
||||
'tinyplaceOrchestration.identity.copy': 'Salin',
|
||||
'tinyplaceOrchestration.identity.copied': 'Tersalin',
|
||||
'tinyplaceOrchestration.identity.discoverable': 'Dapat ditemukan',
|
||||
'tinyplaceOrchestration.identity.undiscoverable': 'Tidak dapat ditemukan',
|
||||
'tinyplaceOrchestration.identity.undiscoverableHint':
|
||||
'Daftarkan @handle agar rekan dapat mengirimi Anda pesan.',
|
||||
'tinyplaceOrchestration.identity.card': 'Kartu direktori',
|
||||
'tinyplaceOrchestration.identity.key': 'Kunci enkripsi',
|
||||
'tinyplaceOrchestration.identity.published': 'Diterbitkan',
|
||||
'tinyplaceOrchestration.identity.notPublished': 'Belum diterbitkan',
|
||||
'tinyplaceOrchestration.roster.instances': 'Instansi',
|
||||
'tinyplaceOrchestration.roster.empty': 'Belum ada instansi agen',
|
||||
'tinyplaceOrchestration.roster.other': 'Lainnya',
|
||||
|
||||
@@ -299,6 +299,20 @@ const messages: TranslationMap = {
|
||||
'tinyplaceOrchestration.composer.send': 'Invia',
|
||||
'tinyplaceOrchestration.composer.sendFailed': 'Invio del messaggio non riuscito',
|
||||
'tinyplaceOrchestration.steering.label': 'Direzione',
|
||||
'tinyplaceOrchestration.relay.staging': 'Staging',
|
||||
'tinyplaceOrchestration.relay.prod': 'Produzione',
|
||||
'tinyplaceOrchestration.identity.loading': 'Caricamento identità…',
|
||||
'tinyplaceOrchestration.identity.noHandle': 'Ancora nessun @handle',
|
||||
'tinyplaceOrchestration.identity.copy': 'Copia',
|
||||
'tinyplaceOrchestration.identity.copied': 'Copiato',
|
||||
'tinyplaceOrchestration.identity.discoverable': 'Rilevabile',
|
||||
'tinyplaceOrchestration.identity.undiscoverable': 'Non rilevabile',
|
||||
'tinyplaceOrchestration.identity.undiscoverableHint':
|
||||
'Registra un @handle così altri agenti possono scriverti.',
|
||||
'tinyplaceOrchestration.identity.card': 'Scheda directory',
|
||||
'tinyplaceOrchestration.identity.key': 'Chiave di crittografia',
|
||||
'tinyplaceOrchestration.identity.published': 'Pubblicato',
|
||||
'tinyplaceOrchestration.identity.notPublished': 'Non pubblicato',
|
||||
'tinyplaceOrchestration.roster.instances': 'Istanze',
|
||||
'tinyplaceOrchestration.roster.empty': 'Nessuna istanza agente',
|
||||
'tinyplaceOrchestration.roster.other': 'Altro',
|
||||
|
||||
@@ -292,6 +292,20 @@ const messages: TranslationMap = {
|
||||
'tinyplaceOrchestration.composer.send': '보내기',
|
||||
'tinyplaceOrchestration.composer.sendFailed': '메시지를 보내지 못했습니다',
|
||||
'tinyplaceOrchestration.steering.label': '조종',
|
||||
'tinyplaceOrchestration.relay.staging': '스테이징',
|
||||
'tinyplaceOrchestration.relay.prod': '프로덕션',
|
||||
'tinyplaceOrchestration.identity.loading': '신원 불러오는 중…',
|
||||
'tinyplaceOrchestration.identity.noHandle': '아직 @handle 없음',
|
||||
'tinyplaceOrchestration.identity.copy': '복사',
|
||||
'tinyplaceOrchestration.identity.copied': '복사됨',
|
||||
'tinyplaceOrchestration.identity.discoverable': '검색 가능',
|
||||
'tinyplaceOrchestration.identity.undiscoverable': '검색 불가',
|
||||
'tinyplaceOrchestration.identity.undiscoverableHint':
|
||||
'다른 에이전트가 메시지를 보낼 수 있도록 @handle을 등록하세요.',
|
||||
'tinyplaceOrchestration.identity.card': '디렉터리 카드',
|
||||
'tinyplaceOrchestration.identity.key': '암호화 키',
|
||||
'tinyplaceOrchestration.identity.published': '게시됨',
|
||||
'tinyplaceOrchestration.identity.notPublished': '게시 안 됨',
|
||||
'tinyplaceOrchestration.roster.instances': '인스턴스',
|
||||
'tinyplaceOrchestration.roster.empty': '아직 에이전트 인스턴스가 없습니다',
|
||||
'tinyplaceOrchestration.roster.other': '기타',
|
||||
|
||||
@@ -300,6 +300,20 @@ const messages: TranslationMap = {
|
||||
'tinyplaceOrchestration.composer.send': 'Wyślij',
|
||||
'tinyplaceOrchestration.composer.sendFailed': 'Nie udało się wysłać wiadomości',
|
||||
'tinyplaceOrchestration.steering.label': 'Sterowanie',
|
||||
'tinyplaceOrchestration.relay.staging': 'Staging',
|
||||
'tinyplaceOrchestration.relay.prod': 'Produkcja',
|
||||
'tinyplaceOrchestration.identity.loading': 'Ładowanie tożsamości…',
|
||||
'tinyplaceOrchestration.identity.noHandle': 'Brak @handle',
|
||||
'tinyplaceOrchestration.identity.copy': 'Kopiuj',
|
||||
'tinyplaceOrchestration.identity.copied': 'Skopiowano',
|
||||
'tinyplaceOrchestration.identity.discoverable': 'Wykrywalny',
|
||||
'tinyplaceOrchestration.identity.undiscoverable': 'Niewykrywalny',
|
||||
'tinyplaceOrchestration.identity.undiscoverableHint':
|
||||
'Zarejestruj @handle, aby inni mogli do Ciebie pisać.',
|
||||
'tinyplaceOrchestration.identity.card': 'Karta katalogu',
|
||||
'tinyplaceOrchestration.identity.key': 'Klucz szyfrowania',
|
||||
'tinyplaceOrchestration.identity.published': 'Opublikowano',
|
||||
'tinyplaceOrchestration.identity.notPublished': 'Nie opublikowano',
|
||||
'tinyplaceOrchestration.roster.instances': 'Instancje',
|
||||
'tinyplaceOrchestration.roster.empty': 'Brak instancji agentów',
|
||||
'tinyplaceOrchestration.roster.other': 'Inne',
|
||||
|
||||
@@ -298,6 +298,20 @@ const messages: TranslationMap = {
|
||||
'tinyplaceOrchestration.composer.send': 'Enviar',
|
||||
'tinyplaceOrchestration.composer.sendFailed': 'Falha ao enviar mensagem',
|
||||
'tinyplaceOrchestration.steering.label': 'Direção',
|
||||
'tinyplaceOrchestration.relay.staging': 'Staging',
|
||||
'tinyplaceOrchestration.relay.prod': 'Produção',
|
||||
'tinyplaceOrchestration.identity.loading': 'Carregando identidade…',
|
||||
'tinyplaceOrchestration.identity.noHandle': 'Ainda sem @handle',
|
||||
'tinyplaceOrchestration.identity.copy': 'Copiar',
|
||||
'tinyplaceOrchestration.identity.copied': 'Copiado',
|
||||
'tinyplaceOrchestration.identity.discoverable': 'Descobrível',
|
||||
'tinyplaceOrchestration.identity.undiscoverable': 'Não descobrível',
|
||||
'tinyplaceOrchestration.identity.undiscoverableHint':
|
||||
'Registre um @handle para que outros agentes possam lhe enviar mensagens.',
|
||||
'tinyplaceOrchestration.identity.card': 'Cartão de diretório',
|
||||
'tinyplaceOrchestration.identity.key': 'Chave de criptografia',
|
||||
'tinyplaceOrchestration.identity.published': 'Publicado',
|
||||
'tinyplaceOrchestration.identity.notPublished': 'Não publicado',
|
||||
'tinyplaceOrchestration.roster.instances': 'Instâncias',
|
||||
'tinyplaceOrchestration.roster.empty': 'Nenhuma instância de agente ainda',
|
||||
'tinyplaceOrchestration.roster.other': 'Outros',
|
||||
|
||||
@@ -300,6 +300,20 @@ const messages: TranslationMap = {
|
||||
'tinyplaceOrchestration.composer.send': 'Отправить',
|
||||
'tinyplaceOrchestration.composer.sendFailed': 'Не удалось отправить сообщение',
|
||||
'tinyplaceOrchestration.steering.label': 'Управление',
|
||||
'tinyplaceOrchestration.relay.staging': 'Стейджинг',
|
||||
'tinyplaceOrchestration.relay.prod': 'Продакшн',
|
||||
'tinyplaceOrchestration.identity.loading': 'Загрузка личности…',
|
||||
'tinyplaceOrchestration.identity.noHandle': 'Пока нет @handle',
|
||||
'tinyplaceOrchestration.identity.copy': 'Копировать',
|
||||
'tinyplaceOrchestration.identity.copied': 'Скопировано',
|
||||
'tinyplaceOrchestration.identity.discoverable': 'Обнаруживаемый',
|
||||
'tinyplaceOrchestration.identity.undiscoverable': 'Не обнаруживается',
|
||||
'tinyplaceOrchestration.identity.undiscoverableHint':
|
||||
'Зарегистрируйте @handle, чтобы другие агенты могли писать вам.',
|
||||
'tinyplaceOrchestration.identity.card': 'Карточка каталога',
|
||||
'tinyplaceOrchestration.identity.key': 'Ключ шифрования',
|
||||
'tinyplaceOrchestration.identity.published': 'Опубликовано',
|
||||
'tinyplaceOrchestration.identity.notPublished': 'Не опубликовано',
|
||||
'tinyplaceOrchestration.roster.instances': 'Экземпляры',
|
||||
'tinyplaceOrchestration.roster.empty': 'Пока нет экземпляров агентов',
|
||||
'tinyplaceOrchestration.roster.other': 'Другое',
|
||||
|
||||
@@ -276,6 +276,20 @@ const messages: TranslationMap = {
|
||||
'tinyplaceOrchestration.composer.send': '发送',
|
||||
'tinyplaceOrchestration.composer.sendFailed': '消息发送失败',
|
||||
'tinyplaceOrchestration.steering.label': '引导',
|
||||
'tinyplaceOrchestration.relay.staging': '预发布',
|
||||
'tinyplaceOrchestration.relay.prod': '生产',
|
||||
'tinyplaceOrchestration.identity.loading': '正在加载身份…',
|
||||
'tinyplaceOrchestration.identity.noHandle': '尚无 @handle',
|
||||
'tinyplaceOrchestration.identity.copy': '复制',
|
||||
'tinyplaceOrchestration.identity.copied': '已复制',
|
||||
'tinyplaceOrchestration.identity.discoverable': '可被发现',
|
||||
'tinyplaceOrchestration.identity.undiscoverable': '不可被发现',
|
||||
'tinyplaceOrchestration.identity.undiscoverableHint':
|
||||
'注册一个 @handle,以便其他代理可以给你发消息。',
|
||||
'tinyplaceOrchestration.identity.card': '目录卡片',
|
||||
'tinyplaceOrchestration.identity.key': '加密密钥',
|
||||
'tinyplaceOrchestration.identity.published': '已发布',
|
||||
'tinyplaceOrchestration.identity.notPublished': '未发布',
|
||||
'tinyplaceOrchestration.roster.instances': '实例',
|
||||
'tinyplaceOrchestration.roster.empty': '暂无代理实例',
|
||||
'tinyplaceOrchestration.roster.other': '其他',
|
||||
|
||||
@@ -101,6 +101,35 @@ export interface OrchestrationMessageEvent {
|
||||
chatKind: string;
|
||||
}
|
||||
|
||||
/** One @handle this agent's wallet holds (reverse-resolved from the directory). */
|
||||
export interface SelfHandle {
|
||||
username: string;
|
||||
primary: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* This agent's own tiny.place identity and whether peers can reach it.
|
||||
*
|
||||
* `discoverable` is the bottom line: a peer can DM this agent only when both its
|
||||
* directory card (`cardPublished`) and its Signal encryption key (`keyPublished`)
|
||||
* are live. A fresh identity can accept contacts yet stay un-messageable until it
|
||||
* registers a @handle — the card surfaces that gap instead of a mystery 404.
|
||||
*/
|
||||
export interface SelfIdentity {
|
||||
agentId: string;
|
||||
handles: SelfHandle[];
|
||||
primaryHandle?: string;
|
||||
cardPublished: boolean;
|
||||
keyPublished: boolean;
|
||||
discoverable: boolean;
|
||||
}
|
||||
|
||||
/** The relay endpoint the core talks to, plus a coarse network label. */
|
||||
export interface RelayInfo {
|
||||
baseUrl: string;
|
||||
network: 'staging' | 'prod';
|
||||
}
|
||||
|
||||
// ── Internal helper ───────────────────────────────────────────────────────────
|
||||
|
||||
function safeParseJson(s: string): unknown {
|
||||
@@ -172,6 +201,16 @@ export const orchestrationClient = {
|
||||
|
||||
/** Current orchestration status (active steering directive, tick timing). */
|
||||
status: () => call<OrchestrationStatus>('openhuman.orchestration_status', {}),
|
||||
|
||||
/**
|
||||
* This agent's own tiny.place identity + discoverability (agent id, @handles,
|
||||
* whether its directory card and Signal key are published, whether peers can
|
||||
* DM it). Powers the SelfIdentityCard.
|
||||
*/
|
||||
selfIdentity: () => call<SelfIdentity>('openhuman.orchestration_self_identity', {}),
|
||||
|
||||
/** The relay endpoint + network label the core is talking to (RelayBadge). */
|
||||
relayInfo: () => call<RelayInfo>('openhuman.orchestration_relay_info', {}),
|
||||
};
|
||||
|
||||
export type OrchestrationClient = typeof orchestrationClient;
|
||||
|
||||
@@ -834,12 +834,135 @@ fn session_send_plaintext(session_id: &str, body: &str) -> anyhow::Result<String
|
||||
.map_err(|e| anyhow::anyhow!("envelope encode: {e}"))
|
||||
}
|
||||
|
||||
// ── Self-identity composition (orchestration_self_identity read model) ────────
|
||||
|
||||
/// One @handle this agent's wallet holds (reverse-resolved from the directory).
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct HandleEntry {
|
||||
pub(crate) username: String,
|
||||
pub(crate) primary: bool,
|
||||
}
|
||||
|
||||
/// This agent's own tiny.place identity and whether peers can reach it.
|
||||
///
|
||||
/// `discoverable` is the bottom line the UI cares about: a peer can DM this
|
||||
/// agent only if both its directory card AND its Signal encryption key are
|
||||
/// published. A fresh identity can accept contacts yet still be un-messageable
|
||||
/// until it registers a @handle (which is what publishes both), so the
|
||||
/// `SelfIdentityCard` surfaces the gap instead of leaving it a mystery 404.
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct SelfIdentity {
|
||||
pub(crate) agent_id: String,
|
||||
pub(crate) handles: Vec<HandleEntry>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub(crate) primary_handle: Option<String>,
|
||||
pub(crate) card_published: bool,
|
||||
pub(crate) key_published: bool,
|
||||
pub(crate) discoverable: bool,
|
||||
}
|
||||
|
||||
/// Pure composition of the three tinyplace reads into the renderer shape. Kept
|
||||
/// here (business logic) so the parsing/discoverability rules are unit-testable
|
||||
/// without a live tiny.place client; the `schemas` handler supplies the reads.
|
||||
///
|
||||
/// `reverse` is the raw `directory_reverse` JSON (`{ identities: [...] }`), or
|
||||
/// `None` on a reverse miss. Discoverable = card live AND encryption key
|
||||
/// published + current — either gap leaves the agent un-messageable.
|
||||
pub(crate) fn build_self_identity(
|
||||
agent_id: String,
|
||||
key_published: bool,
|
||||
reverse: Option<&Value>,
|
||||
card_published: bool,
|
||||
) -> SelfIdentity {
|
||||
let mut handles: Vec<HandleEntry> = Vec::new();
|
||||
let mut primary_handle: Option<String> = None;
|
||||
if let Some(idents) = reverse
|
||||
.and_then(|r| r.get("identities"))
|
||||
.and_then(Value::as_array)
|
||||
{
|
||||
for ident in idents {
|
||||
let username = ident
|
||||
.get("username")
|
||||
.and_then(Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|s| !s.is_empty());
|
||||
let Some(username) = username else { continue };
|
||||
let primary = ident
|
||||
.get("primary")
|
||||
.and_then(Value::as_bool)
|
||||
.unwrap_or(false);
|
||||
if primary && primary_handle.is_none() {
|
||||
primary_handle = Some(username.to_string());
|
||||
}
|
||||
handles.push(HandleEntry {
|
||||
username: username.to_string(),
|
||||
primary,
|
||||
});
|
||||
}
|
||||
}
|
||||
// Fall back to the first handle when none is flagged primary.
|
||||
if primary_handle.is_none() {
|
||||
primary_handle = handles.first().map(|h| h.username.clone());
|
||||
}
|
||||
SelfIdentity {
|
||||
agent_id,
|
||||
handles,
|
||||
primary_handle,
|
||||
card_published,
|
||||
key_published,
|
||||
discoverable: card_published && key_published,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::openhuman::orchestration::types::OrchestrationMessage;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use tinyagents::graph::checkpoint::Checkpointer;
|
||||
|
||||
#[test]
|
||||
fn self_identity_marks_published_identity_discoverable() {
|
||||
let reverse = serde_json::json!({
|
||||
"identities": [
|
||||
{ "username": " ", "primary": false }, // blank → skipped
|
||||
{ "username": "openhuman", "primary": false },
|
||||
{ "username": "oh_primary", "primary": true },
|
||||
]
|
||||
});
|
||||
let id = build_self_identity("addr123".to_string(), true, Some(&reverse), true);
|
||||
assert_eq!(id.agent_id, "addr123");
|
||||
assert_eq!(id.handles.len(), 2, "blank username skipped");
|
||||
assert_eq!(id.primary_handle.as_deref(), Some("oh_primary"));
|
||||
assert!(id.card_published && id.key_published && id.discoverable);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn self_identity_primary_falls_back_to_first_handle() {
|
||||
let reverse = serde_json::json!({
|
||||
"identities": [ { "username": "solo" } ] // no primary flag
|
||||
});
|
||||
let id = build_self_identity("addr".to_string(), true, Some(&reverse), true);
|
||||
assert_eq!(id.primary_handle.as_deref(), Some("solo"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn self_identity_undiscoverable_when_card_or_key_missing() {
|
||||
// No reverse (handle-less), card present but key not published → the
|
||||
// exact un-messageable case the SelfIdentityCard must flag.
|
||||
let no_key = build_self_identity("addr".to_string(), false, None, true);
|
||||
assert!(no_key.handles.is_empty());
|
||||
assert!(no_key.primary_handle.is_none());
|
||||
assert!(!no_key.discoverable, "key not published → not discoverable");
|
||||
|
||||
let no_card = build_self_identity("addr".to_string(), true, None, false);
|
||||
assert!(
|
||||
!no_card.discoverable,
|
||||
"card not published → not discoverable"
|
||||
);
|
||||
}
|
||||
use tinyagents::graph::SqliteCheckpointer;
|
||||
|
||||
fn test_config(tmp: &tempfile::TempDir) -> Config {
|
||||
|
||||
@@ -28,6 +28,8 @@ pub fn all_controller_schemas() -> Vec<ControllerSchema> {
|
||||
schema_for("orchestration_send_master_message"),
|
||||
schema_for("orchestration_mark_read"),
|
||||
schema_for("orchestration_status"),
|
||||
schema_for("orchestration_self_identity"),
|
||||
schema_for("orchestration_relay_info"),
|
||||
]
|
||||
}
|
||||
|
||||
@@ -57,6 +59,14 @@ pub fn all_registered_controllers() -> Vec<RegisteredController> {
|
||||
schema: schema_for("orchestration_status"),
|
||||
handler: handle_status,
|
||||
},
|
||||
RegisteredController {
|
||||
schema: schema_for("orchestration_self_identity"),
|
||||
handler: handle_self_identity,
|
||||
},
|
||||
RegisteredController {
|
||||
schema: schema_for("orchestration_relay_info"),
|
||||
handler: handle_relay_info,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
@@ -120,6 +130,23 @@ fn schema_for(function: &str) -> ControllerSchema {
|
||||
inputs: vec![],
|
||||
outputs: vec![json_output("result", "OrchestrationStatus.")],
|
||||
},
|
||||
"orchestration_self_identity" => ControllerSchema {
|
||||
namespace: "orchestration",
|
||||
function: "self_identity",
|
||||
description: "This agent's own tiny.place identity + discoverability: agent id (address), reverse-resolved @handles, whether its directory card and Signal encryption key are published, and whether peers can therefore DM it. Composes the tinyplace signal/directory reads.",
|
||||
inputs: vec![],
|
||||
outputs: vec![json_output(
|
||||
"result",
|
||||
"{ agentId, handles: {username, primary}[], primaryHandle?, cardPublished, keyPublished, discoverable }.",
|
||||
)],
|
||||
},
|
||||
"orchestration_relay_info" => ControllerSchema {
|
||||
namespace: "orchestration",
|
||||
function: "relay_info",
|
||||
description: "The tiny.place relay endpoint the core is talking to, plus a coarse network label (staging | prod) for the renderer's relay badge.",
|
||||
inputs: vec![],
|
||||
outputs: vec![json_output("result", "{ baseUrl, network }.")],
|
||||
},
|
||||
other => unreachable!("unknown orchestration schema: {other}"),
|
||||
}
|
||||
}
|
||||
@@ -177,6 +204,13 @@ struct OrchestrationStatus {
|
||||
last_error: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct RelayInfo {
|
||||
base_url: String,
|
||||
network: String,
|
||||
}
|
||||
|
||||
/// Resolve the `chat` param to a store session id. `master` / `subconscious` map
|
||||
/// to their pinned session ids; anything else is treated as a harness session id.
|
||||
fn chat_to_session_id(chat: &str) -> &str {
|
||||
@@ -568,6 +602,93 @@ fn handle_status(_params: Map<String, Value>) -> ControllerFuture {
|
||||
})
|
||||
}
|
||||
|
||||
/// Own tiny.place identity + discoverability, composed from the internal
|
||||
/// tinyplace signal/directory reads. Delegates like `send_master` does
|
||||
/// (`crate::openhuman::tinyplace::handle_tinyplace_*`), so there is no new
|
||||
/// tiny.place logic here — only aggregation into the shape the renderer needs.
|
||||
fn handle_self_identity(_params: Map<String, Value>) -> ControllerFuture {
|
||||
Box::pin(async move {
|
||||
// 1. Key status → own agent id + whether the encryption key is published
|
||||
// to the directory and current. `encryptionKeyPublished` is false when
|
||||
// the card is missing OR the published key is stale (wrong wallet).
|
||||
let key_status =
|
||||
crate::openhuman::tinyplace::handle_tinyplace_signal_key_status(Map::new())
|
||||
.await
|
||||
.map_err(|e| format!("self_identity key_status: {e}"))?;
|
||||
let agent_id = key_status
|
||||
.get("agentId")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or_default()
|
||||
.to_string();
|
||||
let key_published = key_status
|
||||
.get("encryptionKeyPublished")
|
||||
.and_then(Value::as_bool)
|
||||
.unwrap_or(false);
|
||||
log::debug!(
|
||||
target: LOG,
|
||||
"[orchestration_rpc] self_identity agent_id_len={} key_published={key_published}",
|
||||
agent_id.len()
|
||||
);
|
||||
|
||||
// 2. Reverse-resolve any @handles this wallet holds. Best-effort: a
|
||||
// handle-less identity is normal (and is exactly the un-messageable
|
||||
// case the card must flag), so a reverse miss is not an error.
|
||||
let reverse = if agent_id.is_empty() {
|
||||
None
|
||||
} else {
|
||||
let mut rev_params = Map::new();
|
||||
rev_params.insert("cryptoId".to_string(), Value::from(agent_id.clone()));
|
||||
match crate::openhuman::tinyplace::handle_tinyplace_directory_reverse(rev_params).await
|
||||
{
|
||||
Ok(reverse) => Some(reverse),
|
||||
Err(e) => {
|
||||
log::debug!(target: LOG, "[orchestration_rpc] self_identity reverse miss: {e}");
|
||||
None
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 3. Directory card presence: get_agent(self) 404s when no card is
|
||||
// published. Ok → a card is live; Err → treat as unpublished.
|
||||
let card_published = if agent_id.is_empty() {
|
||||
false
|
||||
} else {
|
||||
let mut card_params = Map::new();
|
||||
card_params.insert("agentId".to_string(), Value::from(agent_id.clone()));
|
||||
crate::openhuman::tinyplace::handle_tinyplace_directory_get_agent(card_params)
|
||||
.await
|
||||
.is_ok()
|
||||
};
|
||||
|
||||
let identity = super::ops::build_self_identity(
|
||||
agent_id,
|
||||
key_published,
|
||||
reverse.as_ref(),
|
||||
card_published,
|
||||
);
|
||||
log::debug!(
|
||||
target: LOG,
|
||||
"[orchestration_rpc] self_identity handles={} card_published={card_published} discoverable={}",
|
||||
identity.handles.len(),
|
||||
identity.discoverable
|
||||
);
|
||||
to_json(identity)
|
||||
})
|
||||
}
|
||||
|
||||
/// Relay endpoint + network label for the renderer's relay badge. Reads only the
|
||||
/// configured base URL (no client build / wallet unlock), so it always resolves.
|
||||
fn handle_relay_info(_params: Map<String, Value>) -> ControllerFuture {
|
||||
Box::pin(async move {
|
||||
let (base_url, network) = crate::openhuman::tinyplace::ops::relay_endpoint();
|
||||
log::debug!(target: LOG, "[orchestration_rpc] relay_info network={network}");
|
||||
to_json(RelayInfo {
|
||||
base_url,
|
||||
network: network.to_string(),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// ── helpers ─────────────────────────────────────────────────────────────────
|
||||
|
||||
async fn load_config(action: &str) -> Result<Config, String> {
|
||||
@@ -625,8 +746,16 @@ mod tests {
|
||||
#[test]
|
||||
fn schemas_use_orchestration_namespace() {
|
||||
let schemas = all_controller_schemas();
|
||||
assert_eq!(schemas.len(), 6);
|
||||
assert_eq!(schemas.len(), 8);
|
||||
assert!(schemas.iter().all(|s| s.namespace == "orchestration"));
|
||||
assert_eq!(
|
||||
schema_for("orchestration_self_identity").function,
|
||||
"self_identity"
|
||||
);
|
||||
assert_eq!(
|
||||
schema_for("orchestration_relay_info").function,
|
||||
"relay_info"
|
||||
);
|
||||
assert_eq!(
|
||||
schema_for("orchestration_messages_list").function,
|
||||
"messages_list"
|
||||
|
||||
@@ -29,7 +29,9 @@ pub(crate) mod agent;
|
||||
mod agent_tools;
|
||||
mod manifest;
|
||||
pub(crate) use manifest::{
|
||||
acknowledge_message, decrypt_envelope, handle_tinyplace_signal_send_message,
|
||||
acknowledge_message, decrypt_envelope, handle_tinyplace_directory_get_agent,
|
||||
handle_tinyplace_directory_reverse, handle_tinyplace_signal_key_status,
|
||||
handle_tinyplace_signal_send_message,
|
||||
};
|
||||
pub(crate) mod ops;
|
||||
mod payment;
|
||||
|
||||
@@ -19,6 +19,29 @@ pub(crate) fn global_state() -> &'static TinyPlaceState {
|
||||
TINYPLACE_STATE.get_or_init(TinyPlaceState::from_env)
|
||||
}
|
||||
|
||||
/// Active relay endpoint the tiny.place client talks to, plus a coarse network
|
||||
/// label the renderer surfaces as a badge. A base URL containing `staging`
|
||||
/// resolves to `"staging"`; anything else is treated as `"prod"`.
|
||||
///
|
||||
/// This reads only the configured base URL (no client build, no wallet unlock),
|
||||
/// so it is safe to call at any time.
|
||||
pub(crate) fn relay_endpoint() -> (String, &'static str) {
|
||||
let base_url = global_state().base_url.clone();
|
||||
let network = relay_network_label(&base_url);
|
||||
(base_url, network)
|
||||
}
|
||||
|
||||
/// Coarse network label for a relay base URL: `staging` when the host carries a
|
||||
/// `staging` marker, else `prod`. Kept pure so it is unit-testable without the
|
||||
/// process-global state.
|
||||
pub(crate) fn relay_network_label(base_url: &str) -> &'static str {
|
||||
if base_url.contains("staging") {
|
||||
"staging"
|
||||
} else {
|
||||
"prod"
|
||||
}
|
||||
}
|
||||
|
||||
// ── Error mapping ─────────────────────────────────────────────────────────────
|
||||
|
||||
/// Map a SDK [`tinyplace::Error`] to a [`String`] the controller layer returns.
|
||||
@@ -69,3 +92,18 @@ fn extract_error_reason(body: &serde_json::Value) -> Option<String> {
|
||||
other => Some(other.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn relay_network_label_classifies_staging_and_prod() {
|
||||
assert_eq!(
|
||||
relay_network_label("https://staging-api.tiny.place"),
|
||||
"staging"
|
||||
);
|
||||
assert_eq!(relay_network_label("https://api.tiny.place"), "prod");
|
||||
assert_eq!(relay_network_label(""), "prod");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user