feat: add permission request UI
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
import {
|
||||
abortAgentChat,
|
||||
forkAgentChat,
|
||||
replyAgentPermission,
|
||||
type StreamEvent,
|
||||
resumeAgentChatStream,
|
||||
streamAgentChat,
|
||||
} from "./chatStream";
|
||||
@@ -162,12 +164,7 @@ describe("streamAgentChat", () => {
|
||||
]),
|
||||
});
|
||||
|
||||
const events: Array<{
|
||||
type: string;
|
||||
sessionId?: string;
|
||||
tool?: string;
|
||||
params?: Record<string, unknown>;
|
||||
}> = [];
|
||||
const events: StreamEvent[] = [];
|
||||
|
||||
await streamAgentChat({
|
||||
message: "hi",
|
||||
@@ -182,6 +179,43 @@ describe("streamAgentChat", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("parses permission request and response events", async () => {
|
||||
apiFetch.mockResolvedValue({
|
||||
ok: true,
|
||||
body: makeStream([
|
||||
'event: permission_request\ndata: {"session_id":"s1","request_id":"perm-1","permission":"bash","patterns":["rm *"],"metadata":{"command":"rm tmp.txt"},"always":["rm *"],"created_at":123}\n\n',
|
||||
'event: permission_response\ndata: {"session_id":"s1","request_id":"perm-1","reply":"reject"}\n\n',
|
||||
]),
|
||||
});
|
||||
|
||||
const events: StreamEvent[] = [];
|
||||
|
||||
await streamAgentChat({
|
||||
message: "hi",
|
||||
onEvent: (event) => events.push(event),
|
||||
});
|
||||
|
||||
expect(events).toEqual([
|
||||
{
|
||||
type: "permission_request",
|
||||
sessionId: "s1",
|
||||
requestId: "perm-1",
|
||||
permission: "bash",
|
||||
patterns: ["rm *"],
|
||||
metadata: { command: "rm tmp.txt" },
|
||||
always: ["rm *"],
|
||||
tool: undefined,
|
||||
createdAt: 123,
|
||||
},
|
||||
{
|
||||
type: "permission_response",
|
||||
sessionId: "s1",
|
||||
requestId: "perm-1",
|
||||
reply: "reject",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("emits error when response is not ok", async () => {
|
||||
apiFetch.mockResolvedValue({
|
||||
ok: false,
|
||||
@@ -255,6 +289,29 @@ describe("streamAgentChat", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("calls permission reply endpoint", async () => {
|
||||
apiFetch.mockResolvedValue({
|
||||
ok: true,
|
||||
status: 202,
|
||||
text: async () => "",
|
||||
});
|
||||
|
||||
await replyAgentPermission("s1", "perm-1", "once");
|
||||
|
||||
expect(apiFetch).toHaveBeenCalledWith(
|
||||
expect.stringContaining("/api/v1/agent/chat/permission/perm-1/reply"),
|
||||
expect.objectContaining({
|
||||
method: "POST",
|
||||
projectHeaderMode: "include",
|
||||
skipAuthRedirect: true,
|
||||
body: JSON.stringify({
|
||||
session_id: "s1",
|
||||
reply: "once",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("calls fork endpoint and returns new session id", async () => {
|
||||
apiFetch.mockResolvedValue({
|
||||
ok: true,
|
||||
|
||||
+89
-2
@@ -5,6 +5,8 @@ export type AgentModel =
|
||||
| "deepseek/deepseek-v4-flash"
|
||||
| "deepseek/deepseek-v4-pro";
|
||||
|
||||
export type PermissionReply = "once" | "always" | "reject";
|
||||
|
||||
export type StreamEvent =
|
||||
| {
|
||||
type: "state";
|
||||
@@ -41,6 +43,26 @@ export type StreamEvent =
|
||||
sessionId: string;
|
||||
tool: string;
|
||||
params: Record<string, unknown>;
|
||||
}
|
||||
| {
|
||||
type: "permission_request";
|
||||
sessionId: string;
|
||||
requestId: string;
|
||||
permission: string;
|
||||
patterns: string[];
|
||||
metadata: Record<string, unknown>;
|
||||
always: string[];
|
||||
tool?: {
|
||||
messageID: string;
|
||||
callID: string;
|
||||
};
|
||||
createdAt: number;
|
||||
}
|
||||
| {
|
||||
type: "permission_response";
|
||||
sessionId: string;
|
||||
requestId: string;
|
||||
reply: PermissionReply;
|
||||
};
|
||||
|
||||
type StreamOptions = {
|
||||
@@ -111,7 +133,7 @@ const emitParsedStreamEvent = (
|
||||
content?: string;
|
||||
message?: string;
|
||||
detail?: string;
|
||||
tool?: string;
|
||||
tool?: unknown;
|
||||
params?: Record<string, unknown>;
|
||||
arguments?: unknown;
|
||||
id?: string;
|
||||
@@ -126,6 +148,13 @@ const emitParsedStreamEvent = (
|
||||
elapsed_ms?: number;
|
||||
duration_ms?: number;
|
||||
total_duration_ms?: number;
|
||||
request_id?: string;
|
||||
permission?: string;
|
||||
patterns?: unknown;
|
||||
metadata?: unknown;
|
||||
always?: unknown;
|
||||
created_at?: number;
|
||||
reply?: PermissionReply;
|
||||
};
|
||||
if (event === "state") {
|
||||
onEvent({
|
||||
@@ -179,9 +208,39 @@ const emitParsedStreamEvent = (
|
||||
onEvent({
|
||||
type: "tool_call",
|
||||
sessionId: parsed.session_id ?? "",
|
||||
tool: parsed.tool ?? "",
|
||||
tool: typeof parsed.tool === "string" ? parsed.tool : "",
|
||||
params: resolveToolParams(parsed.params, parsed.arguments),
|
||||
});
|
||||
} else if (event === "permission_request") {
|
||||
onEvent({
|
||||
type: "permission_request",
|
||||
sessionId: parsed.session_id ?? "",
|
||||
requestId: parsed.request_id ?? "",
|
||||
permission: parsed.permission ?? "",
|
||||
patterns: Array.isArray(parsed.patterns)
|
||||
? parsed.patterns.filter((item): item is string => typeof item === "string")
|
||||
: [],
|
||||
metadata: isObjectRecord(parsed.metadata) ? parsed.metadata : {},
|
||||
always: Array.isArray(parsed.always)
|
||||
? parsed.always.filter((item): item is string => typeof item === "string")
|
||||
: [],
|
||||
tool: isObjectRecord(parsed.tool) &&
|
||||
typeof parsed.tool.messageID === "string" &&
|
||||
typeof parsed.tool.callID === "string"
|
||||
? {
|
||||
messageID: parsed.tool.messageID,
|
||||
callID: parsed.tool.callID,
|
||||
}
|
||||
: undefined,
|
||||
createdAt: parsed.created_at ?? Date.now(),
|
||||
});
|
||||
} else if (event === "permission_response") {
|
||||
onEvent({
|
||||
type: "permission_response",
|
||||
sessionId: parsed.session_id ?? "",
|
||||
requestId: parsed.request_id ?? "",
|
||||
reply: parsed.reply ?? "reject",
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
onEvent({
|
||||
@@ -349,6 +408,34 @@ export const abortAgentChat = async (sessionId?: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const replyAgentPermission = async (
|
||||
sessionId: string,
|
||||
requestId: string,
|
||||
reply: PermissionReply,
|
||||
) => {
|
||||
const response = await apiFetch(
|
||||
`${config.AGENT_URL}/api/v1/agent/chat/permission/${encodeURIComponent(requestId)}/reply`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
session_id: sessionId,
|
||||
reply,
|
||||
}),
|
||||
projectHeaderMode: "include",
|
||||
userHeaderMode: "include",
|
||||
skipAuthRedirect: true,
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
const detail = await response.text();
|
||||
throw new Error(detail || `permission reply failed: ${response.status}`);
|
||||
}
|
||||
};
|
||||
|
||||
export const forkAgentChat = async (sessionId: string | undefined, keepMessageCount: number) => {
|
||||
const response = await apiFetch(`${config.AGENT_URL}/api/v1/agent/chat/fork`, {
|
||||
method: "POST",
|
||||
|
||||
Reference in New Issue
Block a user