mirror of
https://github.com/aljazceru/claude-code-viewer.git
synced 2025-12-18 22:04:19 +01:00
feat: UI許可システムの実装
- permissionModeを設定可能に変更(bypassPermissions, default, acceptEdits, plan) - defaultモード時のUI許可ダイアログを実装 - canUseTool callbackによるプログラマティックな許可処理 - SSEを使用したリアルタイム通信 - 許可/拒否の選択UI - PermissionDialog機能 - 大きなダイアログサイズ(max-w-4xl, max-h-[80vh]) - パラメータの折りたたみ表示 - 各パラメータのコピーボタン - 長いテキストのスクロール対応 - レスポンシブデザイン対応
This commit is contained in:
@@ -12,7 +12,9 @@ import {
|
||||
import Link from "next/link";
|
||||
import type { FC } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { PermissionDialog } from "@/components/PermissionDialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { usePermissionRequests } from "@/hooks/usePermissionRequests";
|
||||
import { useTaskNotifications } from "@/hooks/useTaskNotifications";
|
||||
import { Badge } from "../../../../../../components/ui/badge";
|
||||
import { honoClient } from "../../../../../../lib/api/client";
|
||||
@@ -50,6 +52,8 @@ export const SessionPageContent: FC<{
|
||||
});
|
||||
|
||||
const { isRunningTask, isPausedTask } = useAliveTask(sessionId);
|
||||
const { currentPermissionRequest, isDialogOpen, onPermissionResponse } =
|
||||
usePermissionRequests();
|
||||
|
||||
// Set up task completion notifications
|
||||
useTaskNotifications(isRunningTask);
|
||||
@@ -226,6 +230,13 @@ export const SessionPageContent: FC<{
|
||||
isOpen={isDiffModalOpen}
|
||||
onOpenChange={setIsDiffModalOpen}
|
||||
/>
|
||||
|
||||
{/* Permission Dialog */}
|
||||
<PermissionDialog
|
||||
permissionRequest={currentPermissionRequest}
|
||||
isOpen={isDialogOpen}
|
||||
onResponse={onPermissionResponse}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
195
src/components/PermissionDialog.tsx
Normal file
195
src/components/PermissionDialog.tsx
Normal file
@@ -0,0 +1,195 @@
|
||||
"use client";
|
||||
|
||||
import { ChevronDown, ChevronRight, Copy } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from "@/components/ui/collapsible";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import type {
|
||||
PermissionRequest,
|
||||
PermissionResponse,
|
||||
} from "@/types/permissions";
|
||||
|
||||
interface PermissionDialogProps {
|
||||
permissionRequest: PermissionRequest | null;
|
||||
isOpen: boolean;
|
||||
onResponse: (response: PermissionResponse) => void;
|
||||
}
|
||||
|
||||
export const PermissionDialog = ({
|
||||
permissionRequest,
|
||||
isOpen,
|
||||
onResponse,
|
||||
}: PermissionDialogProps) => {
|
||||
const [isResponding, setIsResponding] = useState(false);
|
||||
const [isParametersExpanded, setIsParametersExpanded] = useState(false);
|
||||
|
||||
if (!permissionRequest) return null;
|
||||
|
||||
const handleResponse = async (decision: "allow" | "deny") => {
|
||||
setIsResponding(true);
|
||||
|
||||
const response: PermissionResponse = {
|
||||
permissionRequestId: permissionRequest.id,
|
||||
decision,
|
||||
};
|
||||
|
||||
try {
|
||||
await onResponse(response);
|
||||
} finally {
|
||||
setIsResponding(false);
|
||||
}
|
||||
};
|
||||
|
||||
const formatValue = (value: unknown): string => {
|
||||
if (value === null) return "null";
|
||||
if (value === undefined) return "undefined";
|
||||
if (typeof value === "boolean") return value.toString();
|
||||
if (typeof value === "number") return value.toString();
|
||||
if (typeof value === "string") return value;
|
||||
return JSON.stringify(value, null, 2);
|
||||
};
|
||||
|
||||
const copyToClipboard = async (text: string) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
} catch (error) {
|
||||
console.error("Failed to copy to clipboard:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const renderParameterValue = (key: string, value: unknown) => {
|
||||
const formattedValue = formatValue(value);
|
||||
const isLong = formattedValue.length > 100;
|
||||
|
||||
return (
|
||||
<div key={key} className="border rounded-lg p-3 space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-medium text-sm">{key}</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => copyToClipboard(formattedValue)}
|
||||
className="h-6 w-6 p-0"
|
||||
>
|
||||
<Copy className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
<div
|
||||
className={`text-xs font-mono bg-muted p-2 rounded ${isLong ? "max-h-32 overflow-y-auto" : ""}`}
|
||||
>
|
||||
<pre className="whitespace-pre-wrap break-words">
|
||||
{formattedValue}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const parameterEntries = Object.entries(permissionRequest.toolInput);
|
||||
const hasParameters = parameterEntries.length > 0;
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={() => !isResponding}>
|
||||
<DialogContent className="max-w-4xl max-h-[80vh] overflow-hidden flex flex-col">
|
||||
<DialogHeader className="flex-shrink-0">
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<span className="text-orange-600">⚠️</span>
|
||||
Claude Code Permission Request
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
Claude Code wants to execute the following tool and needs your
|
||||
permission.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex-1 overflow-y-auto space-y-4 pr-2">
|
||||
{/* Tool Information */}
|
||||
<div className="rounded-lg border p-4 space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="secondary" className="text-sm">
|
||||
{permissionRequest.toolName}
|
||||
</Badge>
|
||||
</div>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{new Date(permissionRequest.timestamp).toLocaleTimeString()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Parameters Section */}
|
||||
{hasParameters && (
|
||||
<div className="rounded-lg border">
|
||||
<Collapsible
|
||||
open={isParametersExpanded}
|
||||
onOpenChange={setIsParametersExpanded}
|
||||
>
|
||||
<CollapsibleTrigger asChild>
|
||||
<div className="flex items-center justify-between p-4 hover:bg-muted/50 cursor-pointer">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium">Tool Parameters</span>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{parameterEntries.length} parameter
|
||||
{parameterEntries.length !== 1 ? "s" : ""}
|
||||
</Badge>
|
||||
</div>
|
||||
{isParametersExpanded ? (
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
) : (
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
)}
|
||||
</div>
|
||||
</CollapsibleTrigger>
|
||||
|
||||
<CollapsibleContent className="px-4 pb-4">
|
||||
<div className="space-y-3">
|
||||
{parameterEntries.map(([key, value]) =>
|
||||
renderParameterValue(key, value),
|
||||
)}
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!hasParameters && (
|
||||
<div className="rounded-lg border p-4 text-center text-muted-foreground">
|
||||
This tool has no parameters.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex-shrink-0 flex gap-3 justify-end pt-4 border-t">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => handleResponse("deny")}
|
||||
disabled={isResponding}
|
||||
className="min-w-20"
|
||||
>
|
||||
Deny
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => handleResponse("allow")}
|
||||
disabled={isResponding}
|
||||
className="min-w-20"
|
||||
>
|
||||
Allow
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -32,6 +32,7 @@ export const SettingsControls: FC<SettingsControlsProps> = ({
|
||||
}: SettingsControlsProps) => {
|
||||
const checkboxId = useId();
|
||||
const enterKeyBehaviorId = useId();
|
||||
const permissionModeId = useId();
|
||||
const { config, updateConfig } = useConfig();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
@@ -74,6 +75,19 @@ export const SettingsControls: FC<SettingsControlsProps> = ({
|
||||
await onConfigChanged();
|
||||
};
|
||||
|
||||
const handlePermissionModeChange = async (value: string) => {
|
||||
const newConfig = {
|
||||
...config,
|
||||
permissionMode: value as
|
||||
| "acceptEdits"
|
||||
| "bypassPermissions"
|
||||
| "default"
|
||||
| "plan",
|
||||
};
|
||||
updateConfig(newConfig);
|
||||
await onConfigChanged();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`space-y-4 ${className}`}>
|
||||
<div className="flex items-center space-x-2">
|
||||
@@ -148,6 +162,41 @@ export const SettingsControls: FC<SettingsControlsProps> = ({
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
{showLabels && (
|
||||
<label
|
||||
htmlFor={permissionModeId}
|
||||
className="text-sm font-medium leading-none"
|
||||
>
|
||||
Permission Mode
|
||||
</label>
|
||||
)}
|
||||
<Select
|
||||
value={config?.permissionMode || "default"}
|
||||
onValueChange={handlePermissionModeChange}
|
||||
>
|
||||
<SelectTrigger id={permissionModeId} className="w-full">
|
||||
<SelectValue placeholder="Select permission mode" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="default">Default (Ask permission)</SelectItem>
|
||||
<SelectItem value="acceptEdits">
|
||||
Accept Edits (Auto-approve file edits)
|
||||
</SelectItem>
|
||||
<SelectItem value="bypassPermissions">
|
||||
Bypass Permissions (No prompts)
|
||||
</SelectItem>
|
||||
<SelectItem value="plan">Plan Mode (Planning only)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{showDescriptions && (
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
Control how Claude Code handles permission requests for file
|
||||
operations
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
54
src/hooks/usePermissionRequests.ts
Normal file
54
src/hooks/usePermissionRequests.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useState } from "react";
|
||||
import { useServerEventListener } from "@/lib/sse/hook/useServerEventListener";
|
||||
import type {
|
||||
PermissionRequest,
|
||||
PermissionResponse,
|
||||
} from "@/types/permissions";
|
||||
|
||||
export const usePermissionRequests = () => {
|
||||
const [currentPermissionRequest, setCurrentPermissionRequest] =
|
||||
useState<PermissionRequest | null>(null);
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||
|
||||
// Listen for permission requests from the server
|
||||
useServerEventListener("permission_requested", (data) => {
|
||||
if (data.permissionRequest) {
|
||||
setCurrentPermissionRequest(data.permissionRequest);
|
||||
setIsDialogOpen(true);
|
||||
}
|
||||
});
|
||||
|
||||
const handlePermissionResponse = useCallback(
|
||||
async (response: PermissionResponse) => {
|
||||
try {
|
||||
const apiResponse = await fetch("/api/tasks/permission-response", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(response),
|
||||
});
|
||||
|
||||
if (!apiResponse.ok) {
|
||||
throw new Error("Failed to send permission response");
|
||||
}
|
||||
|
||||
// Close the dialog
|
||||
setIsDialogOpen(false);
|
||||
setCurrentPermissionRequest(null);
|
||||
} catch (error) {
|
||||
console.error("Error sending permission response:", error);
|
||||
// TODO: Show error toast to user
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return {
|
||||
currentPermissionRequest,
|
||||
isDialogOpen,
|
||||
onPermissionResponse: handlePermissionResponse,
|
||||
};
|
||||
};
|
||||
@@ -7,6 +7,10 @@ export const configSchema = z.object({
|
||||
.enum(["shift-enter-send", "enter-send"])
|
||||
.optional()
|
||||
.default("shift-enter-send"),
|
||||
permissionMode: z
|
||||
.enum(["acceptEdits", "bypassPermissions", "default", "plan"])
|
||||
.optional()
|
||||
.default("default"),
|
||||
});
|
||||
|
||||
export type Config = z.infer<typeof configSchema>;
|
||||
|
||||
@@ -5,7 +5,7 @@ import { zValidator } from "@hono/zod-validator";
|
||||
import { setCookie } from "hono/cookie";
|
||||
import { streamSSE } from "hono/streaming";
|
||||
import { z } from "zod";
|
||||
import { configSchema } from "../config/config";
|
||||
import { type Config, configSchema } from "../config/config";
|
||||
import { ClaudeCodeTaskController } from "../service/claude-code/ClaudeCodeTaskController";
|
||||
import type { SerializableAliveTask } from "../service/claude-code/types";
|
||||
import { adaptInternalEventToSSE } from "../service/events/adaptInternalEventToSSE";
|
||||
@@ -26,7 +26,16 @@ import type { HonoAppType } from "./app";
|
||||
import { configMiddleware } from "./middleware/config.middleware";
|
||||
|
||||
export const routes = (app: HonoAppType) => {
|
||||
const taskController = new ClaudeCodeTaskController();
|
||||
let taskController: ClaudeCodeTaskController | null = null;
|
||||
const getTaskController = (config: Config) => {
|
||||
if (!taskController) {
|
||||
taskController = new ClaudeCodeTaskController(config);
|
||||
} else {
|
||||
taskController.updateConfig(config);
|
||||
}
|
||||
return taskController;
|
||||
};
|
||||
|
||||
const fileWatcher = getFileWatcher();
|
||||
const eventBus = getEventBus();
|
||||
|
||||
@@ -312,7 +321,9 @@ export const routes = (app: HonoAppType) => {
|
||||
return c.json({ error: "Project path not found" }, 400);
|
||||
}
|
||||
|
||||
const task = await taskController.startOrContinueTask(
|
||||
const task = await getTaskController(
|
||||
c.get("config"),
|
||||
).startOrContinueTask(
|
||||
{
|
||||
projectId,
|
||||
cwd: project.meta.projectPath,
|
||||
@@ -345,7 +356,9 @@ export const routes = (app: HonoAppType) => {
|
||||
return c.json({ error: "Project path not found" }, 400);
|
||||
}
|
||||
|
||||
const task = await taskController.startOrContinueTask(
|
||||
const task = await getTaskController(
|
||||
c.get("config"),
|
||||
).startOrContinueTask(
|
||||
{
|
||||
projectId,
|
||||
sessionId,
|
||||
@@ -364,7 +377,7 @@ export const routes = (app: HonoAppType) => {
|
||||
|
||||
.get("/tasks/alive", async (c) => {
|
||||
return c.json({
|
||||
aliveTasks: taskController.aliveTasks.map(
|
||||
aliveTasks: getTaskController(c.get("config")).aliveTasks.map(
|
||||
(task): SerializableAliveTask => ({
|
||||
id: task.id,
|
||||
status: task.status,
|
||||
@@ -380,11 +393,29 @@ export const routes = (app: HonoAppType) => {
|
||||
zValidator("json", z.object({ sessionId: z.string() })),
|
||||
async (c) => {
|
||||
const { sessionId } = c.req.valid("json");
|
||||
taskController.abortTask(sessionId);
|
||||
getTaskController(c.get("config")).abortTask(sessionId);
|
||||
return c.json({ message: "Task aborted" });
|
||||
},
|
||||
)
|
||||
|
||||
.post(
|
||||
"/tasks/permission-response",
|
||||
zValidator(
|
||||
"json",
|
||||
z.object({
|
||||
permissionRequestId: z.string(),
|
||||
decision: z.enum(["allow", "deny"]),
|
||||
}),
|
||||
),
|
||||
async (c) => {
|
||||
const permissionResponse = c.req.valid("json");
|
||||
getTaskController(c.get("config")).respondToPermissionRequest(
|
||||
permissionResponse,
|
||||
);
|
||||
return c.json({ message: "Permission response received" });
|
||||
},
|
||||
)
|
||||
|
||||
.get("/sse", async (c) => {
|
||||
return streamSSE(
|
||||
c,
|
||||
|
||||
@@ -2,12 +2,15 @@ import { execSync } from "node:child_process";
|
||||
import { query } from "@anthropic-ai/claude-code";
|
||||
import prexit from "prexit";
|
||||
import { ulid } from "ulid";
|
||||
import type { Config } from "../../config/config";
|
||||
import { getEventBus, type IEventBus } from "../events/EventBus";
|
||||
import { createMessageGenerator } from "./createMessageGenerator";
|
||||
import type {
|
||||
AliveClaudeCodeTask,
|
||||
ClaudeCodeTask,
|
||||
PendingClaudeCodeTask,
|
||||
PermissionRequest,
|
||||
PermissionResponse,
|
||||
RunningClaudeCodeTask,
|
||||
} from "./types";
|
||||
|
||||
@@ -15,12 +18,16 @@ export class ClaudeCodeTaskController {
|
||||
private pathToClaudeCodeExecutable: string;
|
||||
private tasks: ClaudeCodeTask[] = [];
|
||||
private eventBus: IEventBus;
|
||||
private config: Config;
|
||||
private pendingPermissionRequests: Map<string, PermissionRequest> = new Map();
|
||||
private permissionResponses: Map<string, PermissionResponse> = new Map();
|
||||
|
||||
constructor() {
|
||||
constructor(config: Config) {
|
||||
this.pathToClaudeCodeExecutable = execSync("which claude", {})
|
||||
.toString()
|
||||
.trim();
|
||||
this.eventBus = getEventBus();
|
||||
this.config = config;
|
||||
|
||||
prexit(() => {
|
||||
this.aliveTasks.forEach((task) => {
|
||||
@@ -29,6 +36,124 @@ export class ClaudeCodeTaskController {
|
||||
});
|
||||
}
|
||||
|
||||
public updateConfig(config: Config) {
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
public respondToPermissionRequest(response: PermissionResponse) {
|
||||
this.permissionResponses.set(response.permissionRequestId, response);
|
||||
this.pendingPermissionRequests.delete(response.permissionRequestId);
|
||||
}
|
||||
|
||||
private createCanUseToolCallback(taskId: string, sessionId?: string) {
|
||||
return async (
|
||||
toolName: string,
|
||||
toolInput: Record<string, unknown>,
|
||||
_options: { signal: AbortSignal },
|
||||
) => {
|
||||
// If not in default mode, use the configured permission mode behavior
|
||||
if (this.config.permissionMode !== "default") {
|
||||
// Convert Claude Code permission modes to canUseTool behaviors
|
||||
if (
|
||||
this.config.permissionMode === "bypassPermissions" ||
|
||||
this.config.permissionMode === "acceptEdits"
|
||||
) {
|
||||
return {
|
||||
behavior: "allow" as const,
|
||||
updatedInput: toolInput,
|
||||
};
|
||||
} else {
|
||||
// plan mode should deny actual tool execution
|
||||
return {
|
||||
behavior: "deny" as const,
|
||||
message: "Tool execution is disabled in plan mode",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Create permission request
|
||||
const permissionRequest: PermissionRequest = {
|
||||
id: ulid(),
|
||||
taskId,
|
||||
sessionId,
|
||||
toolName,
|
||||
toolInput,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
|
||||
// Store the request
|
||||
this.pendingPermissionRequests.set(
|
||||
permissionRequest.id,
|
||||
permissionRequest,
|
||||
);
|
||||
|
||||
// Emit event to notify UI
|
||||
this.eventBus.emit("permissionRequested", {
|
||||
permissionRequest,
|
||||
});
|
||||
|
||||
// Wait for user response with timeout
|
||||
const response = await this.waitForPermissionResponse(
|
||||
permissionRequest.id,
|
||||
60000,
|
||||
); // 60 second timeout
|
||||
|
||||
if (response) {
|
||||
if (response.decision === "allow") {
|
||||
return {
|
||||
behavior: "allow" as const,
|
||||
updatedInput: toolInput,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
behavior: "deny" as const,
|
||||
message: "Permission denied by user",
|
||||
};
|
||||
}
|
||||
} else {
|
||||
// Timeout - default to deny for security
|
||||
this.pendingPermissionRequests.delete(permissionRequest.id);
|
||||
return {
|
||||
behavior: "deny" as const,
|
||||
message: "Permission request timed out",
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private async waitForPermissionResponse(
|
||||
permissionRequestId: string,
|
||||
timeoutMs: number,
|
||||
): Promise<PermissionResponse | null> {
|
||||
return new Promise((resolve) => {
|
||||
const checkResponse = () => {
|
||||
const response = this.permissionResponses.get(permissionRequestId);
|
||||
if (response) {
|
||||
this.permissionResponses.delete(permissionRequestId);
|
||||
resolve(response);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if request was cancelled/deleted
|
||||
if (!this.pendingPermissionRequests.has(permissionRequestId)) {
|
||||
resolve(null);
|
||||
return;
|
||||
}
|
||||
|
||||
// Continue polling
|
||||
setTimeout(checkResponse, 100);
|
||||
};
|
||||
|
||||
// Set timeout
|
||||
setTimeout(() => {
|
||||
resolve(null);
|
||||
}, timeoutMs);
|
||||
|
||||
// Start polling
|
||||
checkResponse();
|
||||
});
|
||||
}
|
||||
|
||||
public get aliveTasks() {
|
||||
return this.tasks.filter(
|
||||
(task) => task.status === "running" || task.status === "paused",
|
||||
@@ -114,7 +239,11 @@ export class ClaudeCodeTaskController {
|
||||
resume: task.baseSessionId,
|
||||
cwd: task.cwd,
|
||||
pathToClaudeCodeExecutable: this.pathToClaudeCodeExecutable,
|
||||
permissionMode: "bypassPermissions",
|
||||
permissionMode: this.config.permissionMode,
|
||||
canUseTool: this.createCanUseToolCallback(
|
||||
task.id,
|
||||
task.baseSessionId,
|
||||
),
|
||||
abortController: abortController,
|
||||
},
|
||||
})) {
|
||||
|
||||
@@ -58,3 +58,17 @@ export type SerializableAliveTask = Pick<
|
||||
AliveClaudeCodeTask,
|
||||
"id" | "status" | "sessionId" | "userMessageId"
|
||||
>;
|
||||
|
||||
export type PermissionRequest = {
|
||||
id: string;
|
||||
taskId: string;
|
||||
sessionId?: string;
|
||||
toolName: string;
|
||||
toolInput: Record<string, unknown>;
|
||||
timestamp: number;
|
||||
};
|
||||
|
||||
export type PermissionResponse = {
|
||||
permissionRequestId: string;
|
||||
decision: "allow" | "deny";
|
||||
};
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import type { AliveClaudeCodeTask } from "../claude-code/types";
|
||||
import type {
|
||||
AliveClaudeCodeTask,
|
||||
PermissionRequest,
|
||||
} from "../claude-code/types";
|
||||
|
||||
export type InternalEventDeclaration = {
|
||||
// biome-ignore lint/complexity/noBannedTypes: correct type
|
||||
@@ -16,4 +19,8 @@ export type InternalEventDeclaration = {
|
||||
taskChanged: {
|
||||
aliveTasks: AliveClaudeCodeTask[];
|
||||
};
|
||||
|
||||
permissionRequested: {
|
||||
permissionRequest: PermissionRequest;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -30,6 +30,7 @@ export const adaptInternalEventToSSE = (
|
||||
abortController.abort();
|
||||
|
||||
eventBus.off("heartbeat", heartbeat);
|
||||
eventBus.off("permissionRequested", permissionRequested);
|
||||
cleanUp?.();
|
||||
};
|
||||
|
||||
@@ -45,7 +46,16 @@ export const adaptInternalEventToSSE = (
|
||||
});
|
||||
};
|
||||
|
||||
const permissionRequested = (
|
||||
event: InternalEventDeclaration["permissionRequested"],
|
||||
) => {
|
||||
stream.writeSSE("permission_requested", {
|
||||
permissionRequest: event.permissionRequest,
|
||||
});
|
||||
};
|
||||
|
||||
eventBus.on("heartbeat", heartbeat);
|
||||
eventBus.on("permissionRequested", permissionRequested);
|
||||
|
||||
stream.writeSSE("connect", {
|
||||
timestamp: new Date().toISOString(),
|
||||
|
||||
13
src/types/permissions.ts
Normal file
13
src/types/permissions.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
export type PermissionRequest = {
|
||||
id: string;
|
||||
taskId: string;
|
||||
sessionId?: string;
|
||||
toolName: string;
|
||||
toolInput: Record<string, unknown>;
|
||||
timestamp: number;
|
||||
};
|
||||
|
||||
export type PermissionResponse = {
|
||||
permissionRequestId: string;
|
||||
decision: "allow" | "deny";
|
||||
};
|
||||
@@ -1,4 +1,7 @@
|
||||
import type { AliveClaudeCodeTask } from "../server/service/claude-code/types";
|
||||
import type {
|
||||
AliveClaudeCodeTask,
|
||||
PermissionRequest,
|
||||
} from "../server/service/claude-code/types";
|
||||
|
||||
export type SSEEventDeclaration = {
|
||||
// biome-ignore lint/complexity/noBannedTypes: correct type
|
||||
@@ -19,6 +22,10 @@ export type SSEEventDeclaration = {
|
||||
taskChanged: {
|
||||
aliveTasks: AliveClaudeCodeTask[];
|
||||
};
|
||||
|
||||
permission_requested: {
|
||||
permissionRequest: PermissionRequest;
|
||||
};
|
||||
};
|
||||
|
||||
export type SSEEventMap = {
|
||||
|
||||
@@ -28,6 +28,12 @@
|
||||
"exactOptionalPropertyTypes": false,
|
||||
"allowJs": true
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
"next-env.d.ts",
|
||||
"./.next/types/**/*.ts"
|
||||
],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user