Fix event hooks not persisting across server syncs (#799)

* Changes from fix/event-hook-persistence

* feat: Add explicit permission escape hatch for clearing eventHooks and improve error handling in UI
This commit is contained in:
gsxdsm
2026-02-22 00:36:08 -08:00
committed by GitHub
parent 629fd24d9f
commit 1d732916f1
7 changed files with 83 additions and 19 deletions

View File

@@ -9,6 +9,10 @@ import type { EventHook, EventHookTrigger } from '@automaker/types';
import { EVENT_HOOK_TRIGGER_LABELS } from '@automaker/types';
import { EventHookDialog } from './event-hook-dialog';
import { EventHistoryView } from './event-history-view';
import { toast } from 'sonner';
import { createLogger } from '@automaker/utils/logger';
const logger = createLogger('EventHooks');
export function EventHooksSection() {
const { eventHooks, setEventHooks } = useAppStore();
@@ -26,24 +30,39 @@ export function EventHooksSection() {
setDialogOpen(true);
};
const handleDeleteHook = (hookId: string) => {
setEventHooks(eventHooks.filter((h) => h.id !== hookId));
};
const handleToggleHook = (hookId: string, enabled: boolean) => {
setEventHooks(eventHooks.map((h) => (h.id === hookId ? { ...h, enabled } : h)));
};
const handleSaveHook = (hook: EventHook) => {
if (editingHook) {
// Update existing
setEventHooks(eventHooks.map((h) => (h.id === hook.id ? hook : h)));
} else {
// Add new
setEventHooks([...eventHooks, hook]);
const handleDeleteHook = async (hookId: string) => {
try {
await setEventHooks(eventHooks.filter((h) => h.id !== hookId));
} catch (error) {
logger.error('Failed to delete event hook:', error);
toast.error('Failed to delete event hook');
}
};
const handleToggleHook = async (hookId: string, enabled: boolean) => {
try {
await setEventHooks(eventHooks.map((h) => (h.id === hookId ? { ...h, enabled } : h)));
} catch (error) {
logger.error('Failed to toggle event hook:', error);
toast.error('Failed to update event hook');
}
};
const handleSaveHook = async (hook: EventHook) => {
try {
if (editingHook) {
// Update existing
await setEventHooks(eventHooks.map((h) => (h.id === hook.id ? hook : h)));
} else {
// Add new
await setEventHooks([...eventHooks, hook]);
}
setDialogOpen(false);
setEditingHook(null);
} catch (error) {
logger.error('Failed to save event hook:', error);
toast.error('Failed to save event hook');
}
setDialogOpen(false);
setEditingHook(null);
};
// Group hooks by trigger type for better organization

View File

@@ -363,6 +363,15 @@ export function mergeSettings(
merged.claudeCompatibleProviders = localSettings.claudeCompatibleProviders;
}
// Event hooks - preserve from localStorage if server is empty
if (
(!serverSettings.eventHooks || serverSettings.eventHooks.length === 0) &&
localSettings.eventHooks &&
localSettings.eventHooks.length > 0
) {
merged.eventHooks = localSettings.eventHooks;
}
// Preserve new settings fields from localStorage if server has defaults
// Use nullish coalescing to accept stored falsy values (e.g. false)
if (localSettings.enableAiCommitMessages != null && merged.enableAiCommitMessages == null) {

View File

@@ -594,6 +594,21 @@ function RootLayoutContent() {
logger.info(
'[FAST_HYDRATE] Background reconcile: cache updated (store untouched)'
);
// Selectively reconcile event hooks from server.
// Unlike projects/theme, eventHooks aren't rendered on the main view,
// so updating them won't cause a visible re-render flash.
const serverHooks = (finalSettings as GlobalSettings).eventHooks ?? [];
const currentHooks = useAppStore.getState().eventHooks;
if (
JSON.stringify(serverHooks) !== JSON.stringify(currentHooks) &&
serverHooks.length > 0
) {
logger.info(
`[FAST_HYDRATE] Reconciling eventHooks from server (server=${serverHooks.length}, store=${currentHooks.length})`
);
useAppStore.setState({ eventHooks: serverHooks });
}
} catch (e) {
logger.debug('[FAST_HYDRATE] Failed to update cache:', e);
}

View File

@@ -1415,7 +1415,15 @@ export const useAppStore = create<AppState & AppActions>()((set, get) => ({
},
// Event Hook actions
setEventHooks: (hooks) => set({ eventHooks: hooks }),
setEventHooks: async (hooks) => {
set({ eventHooks: hooks });
try {
const httpApi = getHttpApiClient();
await httpApi.settings.updateGlobal({ eventHooks: hooks });
} catch (error) {
logger.error('Failed to sync event hooks:', error);
}
},
// Claude-Compatible Provider actions (new system)
addClaudeCompatibleProvider: async (provider) => {

View File

@@ -634,7 +634,7 @@ export interface AppActions {
setPromptCustomization: (customization: PromptCustomization) => Promise<void>;
// Event Hook actions
setEventHooks: (hooks: EventHook[]) => void;
setEventHooks: (hooks: EventHook[]) => Promise<void>;
// Claude-Compatible Provider actions (new system)
addClaudeCompatibleProvider: (provider: ClaudeCompatibleProvider) => Promise<void>;