mirror of
https://github.com/aljazceru/claude-code-viewer.git
synced 2025-12-20 06:44: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:
@@ -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,
|
||||
},
|
||||
})) {
|
||||
|
||||
Reference in New Issue
Block a user