feat(orchestration): receive typed v2 harness-session stream (#4652)

This commit is contained in:
oxoxDev
2026-07-07 23:39:27 -07:00
committed by GitHub
parent 63163ea1be
commit 2dccaf663f
35 changed files with 2271 additions and 156 deletions
@@ -16,6 +16,19 @@ const approval: AttentionItem = {
createdAt: '2026-07-06T10:00:00Z',
};
// A REMOTE agent session parked on an approval: same `approval` kind as a local
// gate approval, but its action opens the orchestration chat window instead of
// the local approval surface.
const remoteApproval: AttentionItem = {
id: 'remote-approval:h-remote',
kind: 'approval',
instanceId: 'h-remote',
title: 'Claude · deploy',
summary: 'approve `rm -rf build/`',
action: { type: 'open-session', sessionId: 'h-remote' },
createdAt: '2026-07-06T12:00:00Z',
};
const needsInput: AttentionItem = {
id: 'needs-input:run-1',
kind: 'needs-input',
@@ -50,6 +63,21 @@ describe('AttentionQueueItem', () => {
expect(onAction).toHaveBeenCalledWith({ type: 'approval', requestId: 'req-1' });
});
it('renders a remote approval (approval kind) whose action opens its session', () => {
const onAction = vi.fn();
render(<AttentionQueueItem item={remoteApproval} onAction={onAction} />);
const row = screen.getByTestId('attention-item-remote-approval:h-remote');
expect(row).toHaveAttribute('data-kind', 'approval');
// Approval kind → the amber "Review" verb, and the summary is shown.
expect(screen.getByText('Claude · deploy')).toBeInTheDocument();
expect(screen.getByText('approve `rm -rf build/`')).toBeInTheDocument();
const action = screen.getByTestId('attention-item-action');
expect(action).toHaveTextContent('tinyplaceOrchestration.attention.review');
// Clicking routes the open-session action so the tab opens the chat window.
fireEvent.click(action);
expect(onAction).toHaveBeenCalledWith({ type: 'open-session', sessionId: 'h-remote' });
});
it('renders a needs-input row with the Open action carrying its thread', () => {
const onAction = vi.fn();
render(<AttentionQueueItem item={needsInput} onAction={onAction} />);
@@ -74,4 +74,43 @@ describe('MessageBubble', () => {
render(<MessageBubble message={message({ encrypted: true, body: '••••' })} />);
expect(screen.getByText('••••')).toHaveClass('text-content-muted');
});
it('renders a tool_call as a monospace command row with ▶ + tool name', () => {
const { container } = render(
<MessageBubble message={message({ eventKind: 'tool_call', toolName: 'Bash', body: 'ls' })} />
);
expect(container.querySelector('[data-event-kind="tool_call"]')).not.toBeNull();
expect(screen.getByText('▶')).toBeInTheDocument();
expect(screen.getByText('Bash')).toBeInTheDocument();
expect(container.querySelector('p.font-mono')?.textContent).toBe('ls');
});
it('renders a tool_result with the ↳ glyph', () => {
render(
<MessageBubble
message={message({ eventKind: 'tool_result', toolName: 'Bash', body: 'ok' })}
/>
);
expect(screen.getByText('↳')).toBeInTheDocument();
});
it('renders agent_thinking italic + muted with the ∴ glyph', () => {
const { container } = render(
<MessageBubble message={message({ eventKind: 'agent_thinking', body: 'considering…' })} />
);
expect(container.querySelector('p.italic')).not.toBeNull();
expect(screen.getByText('∴')).toBeInTheDocument();
});
it('renders an error row with the ✕ glyph', () => {
render(<MessageBubble message={message({ eventKind: 'error', body: 'boom' })} />);
expect(screen.getByText('✕')).toBeInTheDocument();
});
it('falls back to the plain-dot style for a legacy v1 row (no eventKind)', () => {
const { container } = render(<MessageBubble message={message({ body: 'plain' })} />);
expect(container.querySelector('[data-event-kind="v1"]')).not.toBeNull();
expect(container.querySelector('div.rounded-full')).not.toBeNull();
expect(container.querySelector('p.font-mono')).toBeNull();
});
});
@@ -77,19 +77,97 @@ export function ChatListButton({
);
}
/**
* Per-kind presentation for a harness (v2) event row. Differentiates the typed
* stream (tool_call / tool_result / agent_thinking / approval_request / error)
* from plain agent/user messages so the orchestration thread reads like a live
* activity log rather than undifferentiated text. Legacy v1 rows (no
* `eventKind`) fall through to the default agent-message style.
*/
interface BubbleStyle {
/** Leading marker: a glyph when the kind has one, else a colored dot. */
dot: string;
glyph?: string;
/** Render the body in a monospace block (tool command / output). */
mono?: boolean;
/** Body text tone. */
tone: string;
/** Optional left-accent on the bubble. */
accent: string;
}
function bubbleStyle(kind: ChatMessage['eventKind']): BubbleStyle {
switch (kind) {
case 'tool_call':
return {
dot: 'text-ocean-500',
glyph: '▶',
mono: true,
tone: 'text-content',
accent: 'border-l-2 border-l-ocean-400',
};
case 'tool_result':
return {
dot: 'text-sage-500',
glyph: '↳',
mono: true,
tone: 'text-content-muted',
accent: 'border-l-2 border-l-sage-400',
};
case 'agent_thinking':
return {
dot: 'text-content-faint',
glyph: '∴',
tone: 'italic text-content-faint',
accent: '',
};
case 'approval_request':
return {
dot: 'text-amber-500',
glyph: '⚠',
tone: 'text-content',
accent: 'border-l-2 border-l-amber-400',
};
case 'error':
return {
dot: 'text-coral-500',
glyph: '✕',
tone: 'text-coral-600 dark:text-coral-300',
accent: 'border-l-2 border-l-coral-400',
};
case 'user_prompt':
return { dot: 'text-sage-500', tone: 'text-content', accent: '' };
default:
return { dot: 'text-ocean-500', tone: 'text-content', accent: '' };
}
}
export function MessageBubble({ message }: { message: ChatMessage }): ReactElement {
const style = bubbleStyle(message.eventKind);
return (
<div className="flex gap-2">
<div className="mt-1.5 h-2 w-2 flex-none rounded-full bg-ocean-500" />
<div className="min-w-0 rounded-lg border border-line bg-surface px-3 py-2 shadow-soft">
<div className="flex gap-2" data-event-kind={message.eventKind ?? 'v1'}>
{style.glyph ? (
<span className={`mt-0.5 flex-none text-xs font-semibold ${style.dot}`}>{style.glyph}</span>
) : (
<div
className={`mt-1.5 h-2 w-2 flex-none rounded-full ${style.dot.replace('text-', 'bg-')}`}
/>
)}
<div
className={`min-w-0 rounded-lg border border-line bg-surface px-3 py-2 shadow-soft ${style.accent}`}>
<div className="flex flex-wrap items-center gap-x-2 gap-y-1">
<span className="text-xs font-semibold text-content-secondary">{message.from}</span>
{message.toolName ? (
<span className="rounded bg-surface-strong px-1.5 py-0.5 font-mono text-[10px] text-content-secondary">
{message.toolName}
</span>
) : null}
<span className="text-[10px] text-content-faint">{formatTime(message.timestamp)}</span>
</div>
<p
className={`mt-1 whitespace-pre-wrap break-words text-sm ${
message.encrypted ? 'text-content-muted' : 'text-content'
}`}>
className={`mt-1 max-h-64 overflow-y-auto whitespace-pre-wrap break-words ${
style.mono ? 'font-mono text-xs' : 'text-sm'
} ${message.encrypted ? 'text-content-muted' : style.tone}`}>
{message.body}
</p>
</div>
@@ -37,6 +37,9 @@ const props = (over: Partial<OrchestrationSidebarProps>): OrchestrationSidebarPr
steeringText: null,
selfIdentity: null,
identityLoading: false,
onPublishIdentity: vi.fn(),
publishingIdentity: false,
publishIdentityError: null,
attentionQueue: null,
attentionLoading: false,
onAttentionAction: vi.fn(),
@@ -38,6 +38,9 @@ export interface OrchestrationSidebarProps {
steeringText: string | null;
selfIdentity: SelfIdentity | null;
identityLoading: boolean;
onPublishIdentity: () => void;
publishingIdentity: boolean;
publishIdentityError: string | null;
attentionQueue: AttentionQueue | null;
attentionLoading: boolean;
onAttentionAction: (action: AttentionAction) => void;
@@ -72,6 +75,9 @@ export default function OrchestrationSidebar({
steeringText,
selfIdentity,
identityLoading,
onPublishIdentity,
publishingIdentity,
publishIdentityError,
attentionQueue,
attentionLoading,
onAttentionAction,
@@ -146,7 +152,13 @@ export default function OrchestrationSidebar({
) : null}
</div>
<SelfIdentityCard identity={selfIdentity} loading={identityLoading} />
<SelfIdentityCard
identity={selfIdentity}
loading={identityLoading}
onPublish={onPublishIdentity}
publishing={publishingIdentity}
publishError={publishIdentityError}
/>
<AttentionQueueView
queue={attentionQueue}
@@ -60,6 +60,66 @@ describe('SelfIdentityCard', () => {
).toBeInTheDocument();
});
const undiscoverable: SelfIdentity = {
agentId: 'addrWithNoCardPublishedYet',
handles: [{ username: 'openhuman', primary: true }],
primaryHandle: 'openhuman',
cardPublished: false,
keyPublished: false,
discoverable: false,
};
it('renders a "Make discoverable" button that fires onPublish while undiscoverable', () => {
const onPublish = vi.fn();
render(<SelfIdentityCard identity={undiscoverable} loading={false} onPublish={onPublish} />);
const btn = screen.getByTestId('tinyplace-self-identity-publish');
expect(btn).toHaveTextContent('tinyplaceOrchestration.identity.makeDiscoverable');
fireEvent.click(btn);
expect(onPublish).toHaveBeenCalledTimes(1);
});
it('disables the button and shows the publishing label while a publish is in flight', () => {
render(
<SelfIdentityCard identity={undiscoverable} loading={false} onPublish={vi.fn()} publishing />
);
const btn = screen.getByTestId('tinyplace-self-identity-publish');
expect(btn).toBeDisabled();
expect(btn).toHaveTextContent('tinyplaceOrchestration.identity.publishing');
});
it('surfaces a publish error under the button', () => {
render(
<SelfIdentityCard
identity={undiscoverable}
loading={false}
onPublish={vi.fn()}
publishError="boom"
/>
);
expect(screen.getByTestId('tinyplace-self-identity-publish-error')).toHaveTextContent(
'tinyplaceOrchestration.identity.publishFailed'
);
});
it('shows no "make discoverable" button once discoverable', () => {
render(<SelfIdentityCard identity={discoverable} loading={false} onPublish={vi.fn()} />);
expect(screen.queryByTestId('tinyplace-self-identity-publish')).toBeNull();
});
it('offers a "Republish keys" action while discoverable that fires onPublish', () => {
const onPublish = vi.fn();
render(<SelfIdentityCard identity={discoverable} loading={false} onPublish={onPublish} />);
const btn = screen.getByTestId('tinyplace-self-identity-republish');
expect(btn).toHaveTextContent('tinyplaceOrchestration.identity.republish');
fireEvent.click(btn);
expect(onPublish).toHaveBeenCalledTimes(1);
});
it('omits the publish button when no onPublish handler is provided', () => {
render(<SelfIdentityCard identity={undiscoverable} loading={false} />);
expect(screen.queryByTestId('tinyplace-self-identity-publish')).toBeNull();
});
it('copies the address to the clipboard on click', async () => {
render(<SelfIdentityCard identity={discoverable} loading={false} />);
fireEvent.click(screen.getByTestId('tinyplace-self-identity-copy'));
@@ -20,6 +20,16 @@ import type { SelfIdentity } from '../../lib/orchestration/orchestrationClient';
export interface SelfIdentityCardProps {
identity: SelfIdentity | null;
loading: boolean;
/**
* Publish (or refresh) this agent's directory card + Signal key so peers can
* DM it. Rendered only while undiscoverable. The parent owns the RPC + the
* identity refresh; the card just drives the button state.
*/
onPublish?: () => void;
/** True while {@link onPublish} is in flight — disables the button. */
publishing?: boolean;
/** Non-null when the last publish attempt failed — surfaces under the button. */
publishError?: string | null;
}
function shortAddress(address: string): string {
@@ -30,6 +40,9 @@ function shortAddress(address: string): string {
export default function SelfIdentityCard({
identity,
loading,
onPublish,
publishing,
publishError,
}: SelfIdentityCardProps): React.ReactElement {
const { t } = useT();
const [copied, setCopied] = useState(false);
@@ -124,10 +137,53 @@ export default function SelfIdentityCard({
</span>
</div>
{identity.discoverable && onPublish ? (
<div className="mt-2 flex flex-wrap items-center gap-2">
<button
type="button"
data-testid="tinyplace-self-identity-republish"
onClick={onPublish}
disabled={publishing}
className="inline-flex items-center rounded-md border border-line px-2 py-0.5 text-[10px] font-medium text-content-muted transition hover:bg-surface-hover disabled:opacity-50">
{publishing
? t('tinyplaceOrchestration.identity.publishing')
: t('tinyplaceOrchestration.identity.republish')}
</button>
{publishError ? (
<span
data-testid="tinyplace-self-identity-republish-error"
className="text-[10px] text-coral-600 dark:text-coral-300">
{t('tinyplaceOrchestration.identity.publishFailed')}
</span>
) : null}
</div>
) : null}
{!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>
<div className="mt-2 rounded-md bg-coral-50 px-2 py-1.5 dark:bg-coral-500/10">
<p className="text-[10px] text-coral-700 dark:text-coral-300">
{t('tinyplaceOrchestration.identity.undiscoverableHint')}
</p>
{onPublish ? (
<button
type="button"
data-testid="tinyplace-self-identity-publish"
onClick={onPublish}
disabled={publishing}
className="mt-1.5 inline-flex items-center rounded-md bg-coral-600 px-2 py-1 text-[11px] font-semibold text-white transition hover:bg-coral-700 disabled:opacity-50 dark:bg-coral-500 dark:hover:bg-coral-400">
{publishing
? t('tinyplaceOrchestration.identity.publishing')
: t('tinyplaceOrchestration.identity.makeDiscoverable')}
</button>
) : null}
{publishError ? (
<p
data-testid="tinyplace-self-identity-publish-error"
className="mt-1 text-[10px] text-coral-700 dark:text-coral-300">
{t('tinyplaceOrchestration.identity.publishFailed')}
</p>
) : null}
</div>
) : null}
</section>
);
@@ -31,6 +31,7 @@ vi.mock('../../lib/orchestration/orchestrationClient', async importOriginal => {
markRead: vi.fn(),
status: vi.fn(),
selfIdentity: vi.fn(),
publishIdentity: vi.fn(),
relayInfo: vi.fn(),
attention: vi.fn(),
},
@@ -69,6 +69,10 @@ export default function TinyPlaceOrchestrationTab() {
// the whole tab.
const [selfIdentity, setSelfIdentity] = useState<SelfIdentity | null>(null);
const [identityLoading, setIdentityLoading] = useState(true);
// "Make discoverable" remediation: publishes the directory card + Signal key
// when the identity card reports the agent is un-messageable.
const [publishingIdentity, setPublishingIdentity] = useState(false);
const [publishIdentityError, setPublishIdentityError] = useState<string | null>(null);
const [relayInfo, setRelayInfo] = useState<RelayInfo | null>(null);
// The aggregated "needs you" queue (approvals + blocked runs + unread). Read
// independently of chats so a failure leaves the zone empty, never the tab.
@@ -152,6 +156,28 @@ export default function TinyPlaceOrchestrationTab() {
setIdentityLoading(false);
}, []);
// Publish (or refresh) the directory card + Signal key, then adopt the fresh
// identity the RPC echoes back so the card flips to "discoverable" without a
// separate reload. A failure surfaces on the card, not the whole tab.
const publishIdentity = useCallback(async () => {
debug('[tinyplace-orchestration] publish identity entry');
setPublishingIdentity(true);
setPublishIdentityError(null);
try {
const identity = await orchestrationClient.publishIdentity();
if (!mountedRef.current) return;
debug('[tinyplace-orchestration] publish identity ok discoverable=%s', identity.discoverable);
setSelfIdentity(identity);
} catch (error) {
if (!mountedRef.current) return;
const message = error instanceof Error ? error.message : String(error);
debug('[tinyplace-orchestration] publish identity error %s', message);
setPublishIdentityError(message);
} finally {
if (mountedRef.current) setPublishingIdentity(false);
}
}, []);
const loadAttention = useCallback(async () => {
debug('[tinyplace-orchestration] attention load entry');
try {
@@ -348,6 +374,9 @@ export default function TinyPlaceOrchestrationTab() {
steeringText={steeringText}
selfIdentity={selfIdentity}
identityLoading={identityLoading}
onPublishIdentity={publishIdentity}
publishingIdentity={publishingIdentity}
publishIdentityError={publishIdentityError}
attentionQueue={attentionQueue}
attentionLoading={attentionLoading}
onAttentionAction={handleAttentionAction}
+5 -1
View File
@@ -299,7 +299,11 @@ const messages: TranslationMap = {
'tinyplaceOrchestration.identity.discoverable': 'قابل للاكتشاف',
'tinyplaceOrchestration.identity.undiscoverable': 'غير قابل للاكتشاف',
'tinyplaceOrchestration.identity.undiscoverableHint':
'سجّل @handle حتى يتمكن الآخرون من مراسلتك.',
'انشر بطاقة الدليل ومفتاح التشفير حتى يتمكن الآخرون من مراسلتك.',
'tinyplaceOrchestration.identity.makeDiscoverable': 'اجعله قابلاً للاكتشاف',
'tinyplaceOrchestration.identity.republish': 'إعادة نشر المفاتيح',
'tinyplaceOrchestration.identity.publishing': 'جارٍ النشر…',
'tinyplaceOrchestration.identity.publishFailed': 'فشل النشر — أعد المحاولة',
'tinyplaceOrchestration.identity.card': 'بطاقة الدليل',
'tinyplaceOrchestration.identity.key': 'مفتاح التشفير',
'tinyplaceOrchestration.identity.published': 'منشور',
+5 -1
View File
@@ -306,7 +306,11 @@ const messages: TranslationMap = {
'tinyplaceOrchestration.identity.discoverable': 'খুঁজে পাওয়া যায়',
'tinyplaceOrchestration.identity.undiscoverable': 'খুঁজে পাওয়া যায় না',
'tinyplaceOrchestration.identity.undiscoverableHint':
'একটি @handle নিবন্ধন করুন যাতে অন্য এজেন্টরা আপনাকে বার্তা পাঠাতে পারে।',
'আপনার ডিরেক্টরি কার্ড ও এনক্রিপশন কী প্রকাশ করুন যাতে অন্য এজেন্টরা আপনাকে বার্তা পাঠাতে পারে।',
'tinyplaceOrchestration.identity.makeDiscoverable': 'আবিষ্কারযোগ্য করুন',
'tinyplaceOrchestration.identity.republish': 'কী পুনঃপ্রকাশ করুন',
'tinyplaceOrchestration.identity.publishing': 'প্রকাশ করা হচ্ছে…',
'tinyplaceOrchestration.identity.publishFailed': 'প্রকাশ ব্যর্থ হয়েছে — আবার চেষ্টা করুন',
'tinyplaceOrchestration.identity.card': 'ডিরেক্টরি কার্ড',
'tinyplaceOrchestration.identity.key': 'এনক্রিপশন কী',
'tinyplaceOrchestration.identity.published': 'প্রকাশিত',
+6 -1
View File
@@ -313,7 +313,12 @@ const messages: TranslationMap = {
'tinyplaceOrchestration.identity.discoverable': 'Auffindbar',
'tinyplaceOrchestration.identity.undiscoverable': 'Nicht auffindbar',
'tinyplaceOrchestration.identity.undiscoverableHint':
'Registriere ein @handle, damit Peers dir schreiben können.',
'Veröffentliche deine Verzeichniskarte und deinen Verschlüsselungsschlüssel, damit Peers dir schreiben können.',
'tinyplaceOrchestration.identity.makeDiscoverable': 'Auffindbar machen',
'tinyplaceOrchestration.identity.republish': 'Schlüssel neu veröffentlichen',
'tinyplaceOrchestration.identity.publishing': 'Wird veröffentlicht…',
'tinyplaceOrchestration.identity.publishFailed':
'Veröffentlichung fehlgeschlagen — erneut versuchen',
'tinyplaceOrchestration.identity.card': 'Verzeichniskarte',
'tinyplaceOrchestration.identity.key': 'Verschlüsselungsschlüssel',
'tinyplaceOrchestration.identity.published': 'Veröffentlicht',
+5 -1
View File
@@ -4248,7 +4248,11 @@ const en: TranslationMap = {
'tinyplaceOrchestration.identity.discoverable': 'Discoverable',
'tinyplaceOrchestration.identity.undiscoverable': 'Not discoverable',
'tinyplaceOrchestration.identity.undiscoverableHint':
'Register a @handle so peers can message you.',
'Publish your directory card + encryption key so peers can message you.',
'tinyplaceOrchestration.identity.makeDiscoverable': 'Make discoverable',
'tinyplaceOrchestration.identity.republish': 'Republish keys',
'tinyplaceOrchestration.identity.publishing': 'Publishing…',
'tinyplaceOrchestration.identity.publishFailed': 'Publish failed — try again',
'tinyplaceOrchestration.identity.card': 'Directory card',
'tinyplaceOrchestration.identity.key': 'Encryption key',
'tinyplaceOrchestration.identity.published': 'Published',
+5 -1
View File
@@ -310,7 +310,11 @@ const messages: TranslationMap = {
'tinyplaceOrchestration.identity.discoverable': 'Detectable',
'tinyplaceOrchestration.identity.undiscoverable': 'No detectable',
'tinyplaceOrchestration.identity.undiscoverableHint':
'Registra un @handle para que otros agentes puedan escribirte.',
'Publica tu tarjeta de directorio y tu clave de cifrado para que otros agentes puedan escribirte.',
'tinyplaceOrchestration.identity.makeDiscoverable': 'Hacer detectable',
'tinyplaceOrchestration.identity.republish': 'Volver a publicar claves',
'tinyplaceOrchestration.identity.publishing': 'Publicando…',
'tinyplaceOrchestration.identity.publishFailed': 'Error al publicar — inténtalo de nuevo',
'tinyplaceOrchestration.identity.card': 'Tarjeta de directorio',
'tinyplaceOrchestration.identity.key': 'Clave de cifrado',
'tinyplaceOrchestration.identity.published': 'Publicado',
+5 -1
View File
@@ -310,7 +310,11 @@ const messages: TranslationMap = {
'tinyplaceOrchestration.identity.discoverable': 'Découvrable',
'tinyplaceOrchestration.identity.undiscoverable': 'Non découvrable',
'tinyplaceOrchestration.identity.undiscoverableHint':
'Enregistre un @handle pour que les pairs puissent te contacter.',
'Publie ta fiche dannuaire et ta clé de chiffrement pour que les pairs puissent te contacter.',
'tinyplaceOrchestration.identity.makeDiscoverable': 'Rendre découvrable',
'tinyplaceOrchestration.identity.republish': 'Republier les clés',
'tinyplaceOrchestration.identity.publishing': 'Publication…',
'tinyplaceOrchestration.identity.publishFailed': 'Échec de la publication — réessaie',
'tinyplaceOrchestration.identity.card': 'Fiche dannuaire',
'tinyplaceOrchestration.identity.key': 'Clé de chiffrement',
'tinyplaceOrchestration.identity.published': 'Publié',
+5 -1
View File
@@ -306,7 +306,11 @@ const messages: TranslationMap = {
'tinyplaceOrchestration.identity.discoverable': 'खोजने योग्य',
'tinyplaceOrchestration.identity.undiscoverable': 'खोजने योग्य नहीं',
'tinyplaceOrchestration.identity.undiscoverableHint':
'एक @handle पंजीकृत करें ताकि अन्य एजेंट आपको संदेश भेज सकें।',
'अपना डायरेक्टरी कार्ड और एन्क्रिप्शन कुंजी प्रकाशित करें ताकि अन्य एजेंट आपको संदेश भेज सकें।',
'tinyplaceOrchestration.identity.makeDiscoverable': 'खोजने योग्य बनाएँ',
'tinyplaceOrchestration.identity.republish': 'कुंजियाँ पुनः प्रकाशित करें',
'tinyplaceOrchestration.identity.publishing': 'प्रकाशित हो रहा है…',
'tinyplaceOrchestration.identity.publishFailed': 'प्रकाशन विफल — पुनः प्रयास करें',
'tinyplaceOrchestration.identity.card': 'डायरेक्टरी कार्ड',
'tinyplaceOrchestration.identity.key': 'एन्क्रिप्शन कुंजी',
'tinyplaceOrchestration.identity.published': 'प्रकाशित',
+5 -1
View File
@@ -307,7 +307,11 @@ const messages: TranslationMap = {
'tinyplaceOrchestration.identity.discoverable': 'Dapat ditemukan',
'tinyplaceOrchestration.identity.undiscoverable': 'Tidak dapat ditemukan',
'tinyplaceOrchestration.identity.undiscoverableHint':
'Daftarkan @handle agar rekan dapat mengirimi Anda pesan.',
'Terbitkan kartu direktori dan kunci enkripsi Anda agar rekan dapat mengirimi Anda pesan.',
'tinyplaceOrchestration.identity.makeDiscoverable': 'Jadikan dapat ditemukan',
'tinyplaceOrchestration.identity.republish': 'Terbitkan ulang kunci',
'tinyplaceOrchestration.identity.publishing': 'Menerbitkan…',
'tinyplaceOrchestration.identity.publishFailed': 'Gagal menerbitkan — coba lagi',
'tinyplaceOrchestration.identity.card': 'Kartu direktori',
'tinyplaceOrchestration.identity.key': 'Kunci enkripsi',
'tinyplaceOrchestration.identity.published': 'Diterbitkan',
+5 -1
View File
@@ -310,7 +310,11 @@ const messages: TranslationMap = {
'tinyplaceOrchestration.identity.discoverable': 'Rilevabile',
'tinyplaceOrchestration.identity.undiscoverable': 'Non rilevabile',
'tinyplaceOrchestration.identity.undiscoverableHint':
'Registra un @handle così altri agenti possono scriverti.',
'Pubblica la tua scheda directory e la tua chiave di crittografia così altri agenti possono scriverti.',
'tinyplaceOrchestration.identity.makeDiscoverable': 'Rendi rilevabile',
'tinyplaceOrchestration.identity.republish': 'Ripubblica chiavi',
'tinyplaceOrchestration.identity.publishing': 'Pubblicazione…',
'tinyplaceOrchestration.identity.publishFailed': 'Pubblicazione non riuscita — riprova',
'tinyplaceOrchestration.identity.card': 'Scheda directory',
'tinyplaceOrchestration.identity.key': 'Chiave di crittografia',
'tinyplaceOrchestration.identity.published': 'Pubblicato',
+5 -1
View File
@@ -303,7 +303,11 @@ const messages: TranslationMap = {
'tinyplaceOrchestration.identity.discoverable': '검색 가능',
'tinyplaceOrchestration.identity.undiscoverable': '검색 불가',
'tinyplaceOrchestration.identity.undiscoverableHint':
'다른 에이전트가 메시지를 보낼 수 있도록 @handle을 등록하세요.',
'다른 에이전트가 메시지를 보낼 수 있도록 디렉터리 카드와 암호화 키를 게시하세요.',
'tinyplaceOrchestration.identity.makeDiscoverable': '검색 가능하게 만들기',
'tinyplaceOrchestration.identity.republish': '키 다시 게시',
'tinyplaceOrchestration.identity.publishing': '게시 중…',
'tinyplaceOrchestration.identity.publishFailed': '게시 실패 — 다시 시도하세요',
'tinyplaceOrchestration.identity.card': '디렉터리 카드',
'tinyplaceOrchestration.identity.key': '암호화 키',
'tinyplaceOrchestration.identity.published': '게시됨',
+5 -1
View File
@@ -311,7 +311,11 @@ const messages: TranslationMap = {
'tinyplaceOrchestration.identity.discoverable': 'Wykrywalny',
'tinyplaceOrchestration.identity.undiscoverable': 'Niewykrywalny',
'tinyplaceOrchestration.identity.undiscoverableHint':
'Zarejestruj @handle, aby inni mogli do Ciebie pisać.',
'Opublikuj swoją kartę katalogu i klucz szyfrowania, aby inni mogli do Ciebie pisać.',
'tinyplaceOrchestration.identity.makeDiscoverable': 'Ustaw jako wykrywalny',
'tinyplaceOrchestration.identity.republish': 'Opublikuj ponownie klucze',
'tinyplaceOrchestration.identity.publishing': 'Publikowanie…',
'tinyplaceOrchestration.identity.publishFailed': 'Publikacja nie powiodła się — spróbuj ponownie',
'tinyplaceOrchestration.identity.card': 'Karta katalogu',
'tinyplaceOrchestration.identity.key': 'Klucz szyfrowania',
'tinyplaceOrchestration.identity.published': 'Opublikowano',
+5 -1
View File
@@ -309,7 +309,11 @@ const messages: TranslationMap = {
'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.',
'Publique seu cartão de diretório e sua chave de criptografia para que outros agentes possam lhe enviar mensagens.',
'tinyplaceOrchestration.identity.makeDiscoverable': 'Tornar descobrível',
'tinyplaceOrchestration.identity.republish': 'Republicar chaves',
'tinyplaceOrchestration.identity.publishing': 'Publicando…',
'tinyplaceOrchestration.identity.publishFailed': 'Falha ao publicar — tente novamente',
'tinyplaceOrchestration.identity.card': 'Cartão de diretório',
'tinyplaceOrchestration.identity.key': 'Chave de criptografia',
'tinyplaceOrchestration.identity.published': 'Publicado',
+5 -1
View File
@@ -311,7 +311,11 @@ const messages: TranslationMap = {
'tinyplaceOrchestration.identity.discoverable': 'Обнаруживаемый',
'tinyplaceOrchestration.identity.undiscoverable': 'Не обнаруживается',
'tinyplaceOrchestration.identity.undiscoverableHint':
'Зарегистрируйте @handle, чтобы другие агенты могли писать вам.',
'Опубликуйте карточку каталога и ключ шифрования, чтобы другие агенты могли писать вам.',
'tinyplaceOrchestration.identity.makeDiscoverable': 'Сделать обнаруживаемым',
'tinyplaceOrchestration.identity.republish': 'Опубликовать ключи заново',
'tinyplaceOrchestration.identity.publishing': 'Публикация…',
'tinyplaceOrchestration.identity.publishFailed': 'Не удалось опубликовать — попробуйте снова',
'tinyplaceOrchestration.identity.card': 'Карточка каталога',
'tinyplaceOrchestration.identity.key': 'Ключ шифрования',
'tinyplaceOrchestration.identity.published': 'Опубликовано',
+5 -1
View File
@@ -287,7 +287,11 @@ const messages: TranslationMap = {
'tinyplaceOrchestration.identity.discoverable': '可被发现',
'tinyplaceOrchestration.identity.undiscoverable': '不可被发现',
'tinyplaceOrchestration.identity.undiscoverableHint':
'注册一个 @handle,以便其他代理可以给你发消息。',
'发布你的目录卡片和加密密钥,以便其他代理可以给你发消息。',
'tinyplaceOrchestration.identity.makeDiscoverable': '设为可被发现',
'tinyplaceOrchestration.identity.republish': '重新发布密钥',
'tinyplaceOrchestration.identity.publishing': '正在发布…',
'tinyplaceOrchestration.identity.publishFailed': '发布失败 — 请重试',
'tinyplaceOrchestration.identity.card': '目录卡片',
'tinyplaceOrchestration.identity.key': '加密密钥',
'tinyplaceOrchestration.identity.published': '已发布',
@@ -50,6 +50,24 @@ export interface SessionSummary {
pinned: boolean;
}
/**
* Typed kind of a harness (v2) event, mirroring the SDK's
* `tinyplace.harness.session.v2` stream. Absent on legacy v1 rows (which carry
* only role + body). The core hides `status`/`lifecycle`/`unknown` from the
* thread, so those should not normally reach the renderer.
*/
export type HarnessEventKind =
| 'user_prompt'
| 'agent_message'
| 'agent_thinking'
| 'tool_call'
| 'tool_result'
| 'approval_request'
| 'status'
| 'lifecycle'
| 'error'
| 'unknown';
export interface OrchestrationMessage {
id: string;
agentId: string;
@@ -59,6 +77,12 @@ export interface OrchestrationMessage {
body: string;
timestamp: string;
seq: number;
/** Typed event kind for v2 harness streams; absent on v1 text rows. */
eventKind?: HarnessEventKind;
/** Raw harness tool name (e.g. "Bash") on tool_call / tool_result rows. */
toolName?: string;
/** Correlation id shared by a tool_call and its tool_result. */
callId?: string;
}
export interface OrchestrationSteering {
@@ -258,6 +282,15 @@ export const orchestrationClient = {
*/
selfIdentity: () => call<SelfIdentity>('openhuman.orchestration_self_identity', {}),
/**
* Make this agent discoverable: publish (or refresh) its directory card + Signal
* encryption key for the wallet's current identity, then return the updated
* {@link SelfIdentity}. No @handle registration and no payment — it repairs the
* common "has an identity but card/key aren't published" gap that makes every
* inbound DM 404. Powers the SelfIdentityCard's "Make discoverable" action.
*/
publishIdentity: () => call<SelfIdentity>('openhuman.orchestration_publish_identity', {}),
/** The relay endpoint + network label the core is talking to (RelayBadge). */
relayInfo: () => call<RelayInfo>('openhuman.orchestration_relay_info', {}),
};
@@ -16,6 +16,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { socketService } from '../../services/socketService';
import {
type HarnessEventKind,
orchestrationClient,
type OrchestrationMessage,
type OrchestrationMessageEvent,
@@ -37,6 +38,12 @@ export interface ChatMessage {
body: string;
timestamp: string;
encrypted: boolean;
/** Typed harness (v2) event kind; absent on legacy v1 text rows. */
eventKind?: HarnessEventKind;
/** Tool name on tool_call / tool_result rows. */
toolName?: string;
/** Correlation id linking a tool_call to its tool_result. */
callId?: string;
}
export interface ChatWindow {
@@ -84,6 +91,9 @@ function mapMessage(message: OrchestrationMessage): ChatMessage {
body: message.body,
timestamp: message.timestamp,
encrypted: false,
...(message.eventKind ? { eventKind: message.eventKind } : {}),
...(message.toolName ? { toolName: message.toolName } : {}),
...(message.callId ? { callId: message.callId } : {}),
};
}