Add Hermes operations management layer
This commit is contained in:
@@ -4,6 +4,8 @@ import {
|
||||
ArrowLeftIcon,
|
||||
CheckIcon,
|
||||
CircleDotIcon,
|
||||
ExternalLinkIcon,
|
||||
MessageSquareIcon,
|
||||
MonitorUpIcon,
|
||||
RadioTowerIcon,
|
||||
SparklesIcon,
|
||||
@@ -80,7 +82,7 @@ export function PodView({
|
||||
const [sharing, setSharing] = useState(false);
|
||||
const [playingBeat, setPlayingBeat] = useState(false);
|
||||
const [note, setNote] = useState<string | null>(null);
|
||||
const { active, respond } = useInterventions(room);
|
||||
const { active, hermes, voiceCue, actionUrl, respond } = useInterventions(room);
|
||||
|
||||
const audioRef = useRef<HTMLDivElement>(null);
|
||||
const beatRef = useRef<BeatHandle | null>(null);
|
||||
@@ -181,6 +183,15 @@ export function PodView({
|
||||
}
|
||||
}
|
||||
|
||||
async function answerIntervention(status: 'accepted' | 'dismissed', accepted: boolean) {
|
||||
setNote(null);
|
||||
try {
|
||||
await respond(status, accepted);
|
||||
} catch (e) {
|
||||
setNote(`Action failed: ${(e as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
const podmanPresent = participants.some((p) => p.name.toLowerCase() === 'podman');
|
||||
|
||||
return (
|
||||
@@ -299,6 +310,24 @@ export function PodView({
|
||||
{active.suggestedAction.kind.replaceAll('_', ' ')}
|
||||
</Badge>
|
||||
</div>
|
||||
{hermes?.interventionId === active.id && (
|
||||
<div className="rounded-lg border border-dashed p-3">
|
||||
<div className="mb-1 flex items-center gap-2 text-xs font-medium text-muted-foreground">
|
||||
<MessageSquareIcon className="size-3.5" />
|
||||
Hermes message
|
||||
</div>
|
||||
<p className="text-sm leading-6">{hermes.text}</p>
|
||||
</div>
|
||||
)}
|
||||
{voiceCue && (
|
||||
<div className="rounded-lg border border-dashed p-3">
|
||||
<div className="mb-1 flex items-center gap-2 text-xs font-medium text-muted-foreground">
|
||||
<Volume2Icon className="size-3.5" />
|
||||
Voice cue
|
||||
</div>
|
||||
<p className="text-sm leading-6">{voiceCue}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<Empty className="min-h-72 border-0 p-0">
|
||||
@@ -314,14 +343,28 @@ export function PodView({
|
||||
</EmptyHeader>
|
||||
</Empty>
|
||||
)}
|
||||
{actionUrl && (
|
||||
<a
|
||||
href={actionUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="mt-4 flex items-center justify-between gap-3 rounded-lg border bg-muted/30 px-3 py-2 text-sm font-medium hover:bg-muted"
|
||||
>
|
||||
Sync PR artifact opened
|
||||
<ExternalLinkIcon className="size-4" />
|
||||
</a>
|
||||
)}
|
||||
</CardContent>
|
||||
{active && (
|
||||
<CardFooter className="justify-end gap-2">
|
||||
<Button variant="outline" onClick={() => void respond('dismissed', false)}>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => void answerIntervention('dismissed', false)}
|
||||
>
|
||||
<XIcon data-icon="inline-start" />
|
||||
Dismiss
|
||||
</Button>
|
||||
<Button onClick={() => void respond('accepted', true)}>
|
||||
<Button onClick={() => void answerIntervention('accepted', true)}>
|
||||
<CheckIcon data-icon="inline-start" />
|
||||
Accept
|
||||
</Button>
|
||||
|
||||
@@ -46,6 +46,20 @@ export async function postOutcome(outcome: InterventionOutcome): Promise<void> {
|
||||
if (!res.ok) throw new Error(`outcome post failed: ${res.status}`);
|
||||
}
|
||||
|
||||
export async function createSyncPr(input: {
|
||||
headBranch?: string;
|
||||
file?: string;
|
||||
summary?: string;
|
||||
}): Promise<{ url: string; number: number }> {
|
||||
return json(
|
||||
await fetch(`${BACKEND_URL}/api/sync-pr`, {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify(input),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// --- Pods CRUD ---
|
||||
|
||||
export async function listPods(): Promise<Pod[]> {
|
||||
|
||||
@@ -1,18 +1,26 @@
|
||||
import { useEffect, useState, useCallback } from 'react';
|
||||
import { RoomEvent, type Room } from 'livekit-client';
|
||||
import type { DataMessage, Intervention, InterventionStatus } from '@podman/shared';
|
||||
import type { DataMessage, HermesMessage, Intervention, InterventionStatus } from '@podman/shared';
|
||||
import { DATA_TOPIC } from '@podman/shared';
|
||||
import { postOutcome } from '../lib/api';
|
||||
import { createSyncPr, postOutcome } from '../lib/api';
|
||||
|
||||
export function useInterventions(room: Room | null) {
|
||||
const [active, setActive] = useState<Intervention | null>(null);
|
||||
const [hermes, setHermes] = useState<HermesMessage | null>(null);
|
||||
const [voiceCue, setVoiceCue] = useState<string | null>(null);
|
||||
const [actionUrl, setActionUrl] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!room) return;
|
||||
const onData = (payload: Uint8Array, _p: unknown, _k: unknown, topic?: string) => {
|
||||
if (topic !== DATA_TOPIC) return;
|
||||
const msg = JSON.parse(new TextDecoder().decode(payload)) as DataMessage;
|
||||
if (msg.type === 'COLLISION') setActive(msg.intervention);
|
||||
if (msg.type === 'COLLISION') {
|
||||
setActive(msg.intervention);
|
||||
setActionUrl(null);
|
||||
}
|
||||
if (msg.type === 'HERMES_MESSAGE') setHermes(msg.message);
|
||||
if (msg.type === 'VOICE_CUE') setVoiceCue(msg.text);
|
||||
};
|
||||
room.on(RoomEvent.DataReceived, onData);
|
||||
return () => {
|
||||
@@ -23,6 +31,13 @@ export function useInterventions(room: Room | null) {
|
||||
const respond = useCallback(
|
||||
async (status: InterventionStatus, accepted: boolean) => {
|
||||
if (!active) return;
|
||||
if (accepted && active.suggestedAction.kind === 'open_sync_pr') {
|
||||
const pr = await createSyncPr({
|
||||
file: String(active.suggestedAction.params?.file ?? ''),
|
||||
summary: String(active.suggestedAction.params?.summary ?? active.message),
|
||||
});
|
||||
setActionUrl(pr.url);
|
||||
}
|
||||
await postOutcome({
|
||||
interventionId: active.id,
|
||||
collisionId: active.collisionId,
|
||||
@@ -37,5 +52,5 @@ export function useInterventions(room: Room | null) {
|
||||
[active],
|
||||
);
|
||||
|
||||
return { active, respond };
|
||||
return { active, hermes, voiceCue, actionUrl, respond };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user