feat(drainage): execute frontend actions
This commit is contained in:
@@ -31,6 +31,8 @@ import {
|
||||
type UIEnvelopePayload,
|
||||
type UIRegistry
|
||||
} from "@/features/agent/ui-envelope";
|
||||
import type { FrontendActionRequest } from "@/features/agent/frontend-action";
|
||||
import { FrontendActionExecutor } from "@/features/agent/frontend-action/executor";
|
||||
import {
|
||||
appendStreamRenderToken,
|
||||
applySessionStreamEvent,
|
||||
@@ -53,9 +55,10 @@ type AgentRuntimeAvailability = "connecting" | "connected" | "unavailable" | "fa
|
||||
|
||||
type UseWorkbenchAgentOptions = {
|
||||
onUiEnvelope: (payload: UIEnvelopePayload, sessionId: string) => Promise<void> | void;
|
||||
onFrontendAction: (request: FrontendActionRequest, signal: AbortSignal) => Promise<unknown>;
|
||||
};
|
||||
|
||||
export function useWorkbenchAgent({ onUiEnvelope }: UseWorkbenchAgentOptions) {
|
||||
export function useWorkbenchAgent({ onUiEnvelope, onFrontendAction }: UseWorkbenchAgentOptions) {
|
||||
const collapseTimerRef = useRef<number | null>(null);
|
||||
const mobileCollapseTimerRef = useRef<number | null>(null);
|
||||
const sessionStreamAbortRef = useRef<AbortController | null>(null);
|
||||
@@ -64,6 +67,9 @@ export function useWorkbenchAgent({ onUiEnvelope }: UseWorkbenchAgentOptions) {
|
||||
const clientRef = useRef(createAgentApiClient());
|
||||
const sessionIdRef = useRef<string | null>(null);
|
||||
const registryRef = useRef<UIRegistry | null>(null);
|
||||
const frontendActionEnabledRef = useRef(false);
|
||||
const onFrontendActionRef = useRef(onFrontendAction);
|
||||
const processedEnvelopeIdsRef = useRef(new Set<string>());
|
||||
const [panelOpen, setPanelOpen] = useState(true);
|
||||
const [panelCollapsing, setPanelCollapsing] = useState(false);
|
||||
const [mobileOpen, setMobileOpen] = useState(false);
|
||||
@@ -88,6 +94,10 @@ export function useWorkbenchAgent({ onUiEnvelope }: UseWorkbenchAgentOptions) {
|
||||
registryRef.current = registry;
|
||||
}, [registry]);
|
||||
|
||||
useEffect(() => {
|
||||
onFrontendActionRef.current = onFrontendAction;
|
||||
}, [onFrontendAction]);
|
||||
|
||||
const applySessionId = useCallback((nextSessionId: string | undefined) => {
|
||||
if (!nextSessionId || sessionIdRef.current === nextSessionId) {
|
||||
return;
|
||||
@@ -105,6 +115,30 @@ export function useWorkbenchAgent({ onUiEnvelope }: UseWorkbenchAgentOptions) {
|
||||
return clientRef.current.resolveRenderRef(renderRef, nextSessionId);
|
||||
}, []);
|
||||
|
||||
const frontendActionExecutor = useMemo(
|
||||
() => new FrontendActionExecutor(
|
||||
(request, signal) => onFrontendActionRef.current(request, signal),
|
||||
(nextSessionId, actionId, result) => clientRef.current.submitFrontendActionResult(nextSessionId, actionId, result)
|
||||
),
|
||||
[]
|
||||
);
|
||||
const handleFrontendAction = useCallback(
|
||||
(value: unknown) => frontendActionExecutor.handle(value, sessionIdRef.current),
|
||||
[frontendActionExecutor]
|
||||
);
|
||||
|
||||
const claimEnvelope = useCallback((payload: UIEnvelopePayload, activeSessionId: string) => {
|
||||
if (payload.session_id !== activeSessionId) return false;
|
||||
const key = `${activeSessionId}:${payload.envelope_id}`;
|
||||
if (processedEnvelopeIdsRef.current.has(key)) return false;
|
||||
processedEnvelopeIdsRef.current.add(key);
|
||||
if (processedEnvelopeIdsRef.current.size > 256) {
|
||||
const oldest = processedEnvelopeIdsRef.current.values().next().value;
|
||||
if (oldest) processedEnvelopeIdsRef.current.delete(oldest);
|
||||
}
|
||||
return true;
|
||||
}, []);
|
||||
|
||||
const handleDataPart = useCallback(
|
||||
(part: AgentDataPart) => {
|
||||
const eventSessionId = getDataString(part.data, "session_id");
|
||||
@@ -130,6 +164,7 @@ export function useWorkbenchAgent({ onUiEnvelope }: UseWorkbenchAgentOptions) {
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (part.type === "data-frontend_action") { void handleFrontendAction(part.data); return; }
|
||||
|
||||
if (part.type === "data-session_title") {
|
||||
const title = getDataString(part.data, "title");
|
||||
@@ -153,6 +188,7 @@ export function useWorkbenchAgent({ onUiEnvelope }: UseWorkbenchAgentOptions) {
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!claimEnvelope(payload, activeSessionId)) return;
|
||||
|
||||
void Promise.resolve(onUiEnvelope(payload, activeSessionId)).catch((error) => {
|
||||
showMapNotice({
|
||||
@@ -162,7 +198,7 @@ export function useWorkbenchAgent({ onUiEnvelope }: UseWorkbenchAgentOptions) {
|
||||
});
|
||||
});
|
||||
},
|
||||
[applySessionId, onUiEnvelope]
|
||||
[applySessionId, claimEnvelope, handleFrontendAction, onUiEnvelope]
|
||||
);
|
||||
|
||||
const transport = useMemo(
|
||||
@@ -178,7 +214,8 @@ export function useWorkbenchAgent({ onUiEnvelope }: UseWorkbenchAgentOptions) {
|
||||
trigger,
|
||||
messageId,
|
||||
session_id: readBodyString(body, "session_id") ?? sessionIdRef.current ?? undefined,
|
||||
approval_mode: readBodyString(body, "approval_mode") ?? "request"
|
||||
approval_mode: readBodyString(body, "approval_mode") ?? "request",
|
||||
capabilities: frontendActionEnabledRef.current ? ["frontend-action@1"] : []
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -253,9 +290,10 @@ export function useWorkbenchAgent({ onUiEnvelope }: UseWorkbenchAgentOptions) {
|
||||
clearCollapseTimer();
|
||||
clearMobileCollapseTimer();
|
||||
stopSessionStreamSubscription();
|
||||
frontendActionExecutor.cancelAll();
|
||||
chatRef.current.stop();
|
||||
};
|
||||
}, [clearCollapseTimer, clearMobileCollapseTimer, stopSessionStreamSubscription]);
|
||||
}, [clearCollapseTimer, clearMobileCollapseTimer, frontendActionExecutor, stopSessionStreamSubscription]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!backendConnection) {
|
||||
@@ -263,6 +301,7 @@ export function useWorkbenchAgent({ onUiEnvelope }: UseWorkbenchAgentOptions) {
|
||||
}
|
||||
|
||||
registryRef.current = backendConnection.registry;
|
||||
frontendActionEnabledRef.current = Boolean(backendConnection.frontendActionRegistry);
|
||||
setRegistry(backendConnection.registry);
|
||||
setModelOptions(backendConnection.models);
|
||||
setSelectedModel((current) => current || backendConnection.defaultModel || backendConnection.models[0]?.id || "");
|
||||
@@ -396,6 +435,7 @@ export function useWorkbenchAgent({ onUiEnvelope }: UseWorkbenchAgentOptions) {
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!claimEnvelope(payload, activeSessionId)) return;
|
||||
void Promise.resolve(onUiEnvelope(payload, activeSessionId)).catch((error) => {
|
||||
showMapNotice({
|
||||
tone: "error",
|
||||
@@ -403,6 +443,8 @@ export function useWorkbenchAgent({ onUiEnvelope }: UseWorkbenchAgentOptions) {
|
||||
message: error instanceof Error ? error.message : "工作台处理 Agent 展示结果失败。"
|
||||
});
|
||||
});
|
||||
} else if (event.type === "frontend_action") {
|
||||
void handleFrontendAction(event.data);
|
||||
} else if (event.type === "progress") {
|
||||
const title = getDataString(event.data, "title");
|
||||
if (title) {
|
||||
@@ -422,7 +464,7 @@ export function useWorkbenchAgent({ onUiEnvelope }: UseWorkbenchAgentOptions) {
|
||||
|
||||
chatRef.current.setMessages((current) => applySessionStreamEvent(current, event));
|
||||
},
|
||||
[onUiEnvelope, refreshSessionHistory]
|
||||
[claimEnvelope, handleFrontendAction, onUiEnvelope, refreshSessionHistory]
|
||||
);
|
||||
|
||||
const startSessionStreamSubscription = useCallback(
|
||||
|
||||
Reference in New Issue
Block a user