mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-30 23:14:37 +00:00
feat(orchestration): receive typed v2 harness-session stream (#4652)
This commit is contained in:
@@ -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}
|
||||
|
||||
@@ -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': 'منشور',
|
||||
|
||||
@@ -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': 'প্রকাশিত',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 d’annuaire 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 d’annuaire',
|
||||
'tinyplaceOrchestration.identity.key': 'Clé de chiffrement',
|
||||
'tinyplaceOrchestration.identity.published': 'Publié',
|
||||
|
||||
@@ -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': 'प्रकाशित',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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': '게시됨',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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': 'Опубликовано',
|
||||
|
||||
@@ -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 } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user