Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
105 lines
3.7 KiB
TypeScript
105 lines
3.7 KiB
TypeScript
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
import { localStorageAdapter } from '../../infrastructure/persistence/localStorageAdapter';
|
|
import { STORAGE_KEY_AI_PERMISSION_GRANTS } from '../../infrastructure/config/storageKeys';
|
|
import {
|
|
sanitizePermissionGrants,
|
|
setActivePermissionGrants,
|
|
type PermissionGrantRule,
|
|
} from '../../infrastructure/ai/harness/permissionGrants';
|
|
import { getAIBridge } from './aiStateSnapshots';
|
|
import { AI_STATE_CHANGED_EVENT, emitAIStateChanged } from './aiStateEvents';
|
|
|
|
function readPermissionGrants(): PermissionGrantRule[] {
|
|
return sanitizePermissionGrants(localStorageAdapter.read<unknown>(STORAGE_KEY_AI_PERMISSION_GRANTS));
|
|
}
|
|
|
|
function syncPermissionGrantsToMainProcess(rules: PermissionGrantRule[]): void {
|
|
void getAIBridge()?.aiMcpSyncPermissionGrants?.(rules)?.catch(() => {});
|
|
}
|
|
|
|
function writePermissionGrants(rules: PermissionGrantRule[]): void {
|
|
localStorageAdapter.write(STORAGE_KEY_AI_PERMISSION_GRANTS, rules);
|
|
setActivePermissionGrants(rules);
|
|
syncPermissionGrantsToMainProcess(rules);
|
|
emitAIStateChanged(STORAGE_KEY_AI_PERMISSION_GRANTS);
|
|
}
|
|
|
|
export function useAIPermissionGrantsState() {
|
|
const [rules, setRulesRaw] = useState<PermissionGrantRule[]>(() => {
|
|
const initial = readPermissionGrants();
|
|
setActivePermissionGrants(initial);
|
|
return initial;
|
|
});
|
|
|
|
const setRules = useCallback((value: PermissionGrantRule[] | ((prev: PermissionGrantRule[]) => PermissionGrantRule[])) => {
|
|
setRulesRaw((prev) => {
|
|
const next = typeof value === 'function' ? value(prev) : value;
|
|
writePermissionGrants(next);
|
|
return next;
|
|
});
|
|
}, []);
|
|
|
|
const addGrant = useCallback((rule: PermissionGrantRule) => {
|
|
setRules((prev) => [...prev, rule]);
|
|
}, [setRules]);
|
|
|
|
const updateGrant = useCallback((
|
|
id: string,
|
|
updates: Partial<Omit<PermissionGrantRule, 'id' | 'createdAt'>>,
|
|
) => {
|
|
setRules((prev) => prev.map((rule) => (
|
|
rule.id === id ? { ...rule, ...updates } : rule
|
|
)));
|
|
}, [setRules]);
|
|
|
|
const removeGrant = useCallback((id: string) => {
|
|
setRules((prev) => prev.filter((rule) => rule.id !== id));
|
|
}, [setRules]);
|
|
|
|
const importGrants = useCallback((raw: unknown, mode: 'merge' | 'replace' = 'replace') => {
|
|
const imported = sanitizePermissionGrants(raw);
|
|
setRules((prev) => (mode === 'merge' ? [...prev, ...imported] : imported));
|
|
}, [setRules]);
|
|
|
|
const exportGrants = useCallback((): PermissionGrantRule[] => {
|
|
return readPermissionGrants();
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
const syncFromStorageKey = (key: string | null) => {
|
|
if (key !== STORAGE_KEY_AI_PERMISSION_GRANTS) return;
|
|
const next = readPermissionGrants();
|
|
setActivePermissionGrants(next);
|
|
syncPermissionGrantsToMainProcess(next);
|
|
setRulesRaw(next);
|
|
};
|
|
|
|
const handleStorage = (event: StorageEvent) => syncFromStorageKey(event.key);
|
|
const handleLocalStateChanged = (event: Event) => {
|
|
syncFromStorageKey((event as CustomEvent<{ key?: string }>).detail?.key ?? null);
|
|
};
|
|
|
|
window.addEventListener('storage', handleStorage);
|
|
window.addEventListener(AI_STATE_CHANGED_EVENT, handleLocalStateChanged);
|
|
return () => {
|
|
window.removeEventListener('storage', handleStorage);
|
|
window.removeEventListener(AI_STATE_CHANGED_EVENT, handleLocalStateChanged);
|
|
};
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
const bridge = getAIBridge();
|
|
bridge?.aiMcpSyncPermissionGrants?.(rules);
|
|
}, [rules]);
|
|
|
|
return useMemo(() => ({
|
|
permissionGrants: rules,
|
|
setPermissionGrants: setRules,
|
|
addGrant,
|
|
updateGrant,
|
|
removeGrant,
|
|
importGrants,
|
|
exportGrants,
|
|
}), [rules, setRules, addGrant, updateGrant, removeGrant, importGrants, exportGrants]);
|
|
}
|