diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 981f062..5dcfac3 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -151,6 +151,16 @@ "timeout": 5000 } ] + }, + { + "matcher": ".*", + "hooks": [ + { + "type": "command", + "command": "powershell -NoProfile -File hooks/approval-permission.ps1", + "timeout": 130000 + } + ] } ], "Notification": [ @@ -174,6 +184,15 @@ "timeout": 10000 } ] + }, + { + "hooks": [ + { + "type": "command", + "command": "powershell -NoProfile -File hooks/approval-plan.ps1", + "timeout": 130000 + } + ] } ] } diff --git a/frontend/src/components/HooksApprovalModal.vue b/frontend/src/components/HooksApprovalModal.vue index 26d24d7..6f6eaa4 100644 --- a/frontend/src/components/HooksApprovalModal.vue +++ b/frontend/src/components/HooksApprovalModal.vue @@ -67,7 +67,7 @@ watch(totalPending, (val) => { v-for="perm in group.permissions" :key="perm.requestId" :request="perm" - @respond="respondPermission" + @respond="(id, decision, reason) => respondPermission(id, decision, reason)" /> +import { ref, computed } from 'vue' import type { HooksApprovalPermissionRequest } from '@/types/hooks-approval' const props = defineProps<{ @@ -6,16 +7,130 @@ const props = defineProps<{ }>() const emit = defineEmits<{ - respond: [requestId: string, decision: 'allow' | 'deny'] + respond: [requestId: string, decision: string, reason?: string] }>() -function formatInput(input: unknown): string { - if (!input) return '' - if (typeof input === 'string') return input +// ── Mode detection ── +type CardMode = 'permission' | 'plan' | 'question' + +const mode = computed(() => { + if (props.request.tool_name === 'ExitPlanMode') return 'plan' + if (props.request.tool_name === 'AskUserQuestion') return 'question' + return 'permission' +}) + +const input = computed(() => { + if (!props.request.tool_input) return null + return props.request.tool_input as Record +}) + +// ── Plan mode ── +const planText = computed(() => { + if (mode.value !== 'plan' || !input.value) return '' + return (input.value.plan as string) || '' +}) + +const showPlanEditor = ref(false) +const planEditText = ref('') + +function handlePlanEdit() { + if (!showPlanEditor.value) { + showPlanEditor.value = true + return + } + emit('respond', props.request.requestId, 'deny', planEditText.value || 'Continue with modifications.') +} + +// ── Question mode ── +interface QuestionOption { + label: string + description?: string +} + +interface QuestionItem { + question: string + header?: string + options?: QuestionOption[] + multiSelect?: boolean +} + +const questions = computed(() => { + if (mode.value !== 'question' || !input.value) return [] + const qs = input.value.questions + if (Array.isArray(qs)) return qs as QuestionItem[] + return [] +}) + +// Track selected options per question (keyed by question text) +const selectedAnswers = ref>>({}) +const customAnswers = ref>({}) +const showCustom = ref>({}) + +function toggleOption(questionText: string, label: string, multiSelect?: boolean) { + if (!selectedAnswers.value[questionText]) { + selectedAnswers.value[questionText] = new Set() + } + const set = selectedAnswers.value[questionText] + if (multiSelect) { + if (set.has(label)) set.delete(label) + else set.add(label) + } else { + if (set.has(label)) set.clear() + else { set.clear(); set.add(label) } + } + // Trigger reactivity + selectedAnswers.value = { ...selectedAnswers.value } +} + +function toggleCustom(questionText: string) { + showCustom.value = { ...showCustom.value, [questionText]: !showCustom.value[questionText] } +} + +function submitAnswers() { + // Build answers object: { "question text": "selected label" } + const answers: Record = {} + for (const q of questions.value) { + const selected = selectedAnswers.value[q.question] + const custom = customAnswers.value[q.question] + if (custom?.trim()) { + answers[q.question] = custom.trim() + } else if (selected?.size) { + answers[q.question] = Array.from(selected).join(', ') + } + } + // Deny the tool (so Claude gets the answer as the deny message) + // The answer goes as a structured JSON string + const answerText = JSON.stringify({ answers }) + emit('respond', props.request.requestId, 'deny', answerText) +} + +function allowQuestion() { + // Let AskUserQuestion run normally (shows in terminal) + emit('respond', props.request.requestId, 'allow') +} + +// ── Permission mode ── +const showFreeResponse = ref(false) +const freeText = ref('') + +function handleFreeResponse() { + if (!showFreeResponse.value) { + showFreeResponse.value = true + return + } + if (freeText.value.trim()) { + emit('respond', props.request.requestId, 'deny', freeText.value.trim()) + } +} + +// ── Common ── +function formatInput(inp: unknown): string { + if (!inp) return '' + if (typeof inp === 'string') return inp try { - return JSON.stringify(input, null, 2) + return JSON.stringify(inp, null, 2) } catch { - return String(input) + return String(inp) } } @@ -28,8 +143,123 @@ function elapsed(): string {