Fix orphaned features when deleting worktrees (#820)

* Changes from fix/orphaned-features

* fix: Handle feature migration failures and improve UI accessibility

* feat: Add event emission for worktree deletion and feature migration

* fix: Handle OpenCode model errors and prevent duplicate model IDs

* feat: Add summary dialog and async verify with loading state

* fix: Add type attributes to buttons and improve OpenCode model selection

* fix: Add null checks for onVerify callback and opencode model selection
This commit is contained in:
gsxdsm
2026-02-28 15:42:10 -08:00
committed by GitHub
parent 1c0e460dd1
commit 63b0a4fb38
29 changed files with 838 additions and 85 deletions

View File

@@ -311,6 +311,7 @@ const initialState: AppState = {
keyboardShortcuts: DEFAULT_KEYBOARD_SHORTCUTS,
muteDoneSound: false,
disableSplashScreen: false,
defaultSortNewestCardOnTop: false,
serverLogLevel: 'info',
enableRequestLogging: true,
showQueryDevtools: true,
@@ -333,6 +334,7 @@ const initialState: AppState = {
opencodeDefaultModel: DEFAULT_OPENCODE_MODEL,
dynamicOpencodeModels: [],
enabledDynamicModelIds: [],
knownDynamicModelIds: [],
cachedOpencodeProviders: [],
opencodeModelsLoading: false,
opencodeModelsError: null,
@@ -1233,6 +1235,9 @@ export const useAppStore = create<AppState & AppActions>()((set, get) => ({
// Splash Screen actions
setDisableSplashScreen: (disabled) => set({ disableSplashScreen: disabled }),
// Board Card Sorting (global default) actions
setDefaultSortNewestCardOnTop: (enabled) => set({ defaultSortNewestCardOnTop: enabled }),
// Server Log Level actions
setServerLogLevel: (level) => set({ serverLogLevel: level }),
setEnableRequestLogging: (enabled) => set({ enableRequestLogging: enabled }),
@@ -1355,21 +1360,52 @@ export const useAppStore = create<AppState & AppActions>()((set, get) => ({
// OpenCode CLI Settings actions
setEnabledOpencodeModels: (models) => set({ enabledOpencodeModels: models }),
setOpencodeDefaultModel: (model) => set({ opencodeDefaultModel: model }),
toggleOpencodeModel: (model, enabled) =>
setOpencodeDefaultModel: async (model) => {
set({ opencodeDefaultModel: model });
try {
const httpApi = getHttpApiClient();
await httpApi.settings.updateGlobal({ opencodeDefaultModel: model });
} catch (error) {
logger.error('Failed to sync opencodeDefaultModel:', error);
}
},
toggleOpencodeModel: async (model, enabled) => {
set((state) => ({
enabledOpencodeModels: enabled
? [...state.enabledOpencodeModels, model]
? [...new Set([...state.enabledOpencodeModels, model])]
: state.enabledOpencodeModels.filter((m) => m !== model),
})),
}));
try {
const httpApi = getHttpApiClient();
await httpApi.settings.updateGlobal({ enabledOpencodeModels: get().enabledOpencodeModels });
} catch (error) {
logger.error('Failed to sync enabledOpencodeModels:', error);
}
},
setDynamicOpencodeModels: (models) => set({ dynamicOpencodeModels: models }),
setEnabledDynamicModelIds: (ids) => set({ enabledDynamicModelIds: ids }),
toggleDynamicModel: (modelId, enabled) =>
setEnabledDynamicModelIds: async (ids) => {
const deduped = Array.from(new Set(ids));
set({ enabledDynamicModelIds: deduped });
try {
const httpApi = getHttpApiClient();
await httpApi.settings.updateGlobal({ enabledDynamicModelIds: deduped });
} catch (error) {
logger.error('Failed to sync enabledDynamicModelIds:', error);
}
},
toggleDynamicModel: async (modelId, enabled) => {
set((state) => ({
enabledDynamicModelIds: enabled
? [...state.enabledDynamicModelIds, modelId]
? [...new Set([...state.enabledDynamicModelIds, modelId])]
: state.enabledDynamicModelIds.filter((id) => id !== modelId),
})),
}));
try {
const httpApi = getHttpApiClient();
await httpApi.settings.updateGlobal({ enabledDynamicModelIds: get().enabledDynamicModelIds });
} catch (error) {
logger.error('Failed to sync enabledDynamicModelIds:', error);
}
},
setCachedOpencodeProviders: (providers) => set({ cachedOpencodeProviders: providers }),
// Gemini CLI Settings actions
@@ -2877,13 +2913,43 @@ export const useAppStore = create<AppState & AppActions>()((set, get) => ({
(m) => !m.id.startsWith(OPENCODE_BEDROCK_MODEL_PREFIX)
);
// Auto-enable only models that are genuinely new (never seen before).
// Models that existed previously and were explicitly deselected by the user
// should NOT be re-enabled on subsequent fetches.
const currentEnabledIds = get().enabledDynamicModelIds;
const currentKnownIds = get().knownDynamicModelIds;
const allFetchedIds = filteredModels.map((m) => m.id);
// Only auto-enable models that have NEVER been seen before (not in knownDynamicModelIds)
const trulyNewModelIds = allFetchedIds.filter((id) => !currentKnownIds.includes(id));
const updatedEnabledIds =
trulyNewModelIds.length > 0
? [...new Set([...currentEnabledIds, ...trulyNewModelIds])]
: currentEnabledIds;
// Track all discovered model IDs (union of known + newly fetched)
const updatedKnownIds = [...new Set([...currentKnownIds, ...allFetchedIds])];
set({
dynamicOpencodeModels: filteredModels,
enabledDynamicModelIds: updatedEnabledIds,
knownDynamicModelIds: updatedKnownIds,
cachedOpencodeProviders: data.providers ?? [],
opencodeModelsLoading: false,
opencodeModelsLastFetched: now,
opencodeModelsError: null,
});
// Persist newly enabled model IDs and known model IDs to server settings
if (trulyNewModelIds.length > 0) {
try {
const httpApi = getHttpApiClient();
await httpApi.settings.updateGlobal({
enabledDynamicModelIds: updatedEnabledIds,
knownDynamicModelIds: updatedKnownIds,
});
} catch (syncError) {
logger.error('Failed to sync enabledDynamicModelIds after auto-enable:', syncError);
}
}
} else {
set({
opencodeModelsLoading: false,

View File

@@ -168,6 +168,9 @@ export interface AppState {
// Splash Screen Settings
disableSplashScreen: boolean; // When true, skip showing the splash screen overlay on startup
// Board Card Sorting (global default)
defaultSortNewestCardOnTop: boolean; // Global default: sort latest card on top in board columns and list view
// Server Log Level Settings
serverLogLevel: ServerLogLevel; // Log level for the API server (error, warn, info, debug)
enableRequestLogging: boolean; // Enable HTTP request logging (Morgan)
@@ -215,6 +218,7 @@ export interface AppState {
// from `opencode models` CLI and depend on current provider authentication state
dynamicOpencodeModels: ModelDefinition[]; // Dynamically discovered models from OpenCode CLI
enabledDynamicModelIds: string[]; // Which dynamic models are enabled
knownDynamicModelIds: string[]; // All dynamic model IDs ever seen (used to avoid re-enabling explicitly deselected models)
cachedOpencodeProviders: Array<{
id: string;
name: string;
@@ -574,6 +578,9 @@ export interface AppActions {
// Splash Screen actions
setDisableSplashScreen: (disabled: boolean) => void;
// Board Card Sorting (global default) actions
setDefaultSortNewestCardOnTop: (enabled: boolean) => void;
// Server Log Level actions
setServerLogLevel: (level: ServerLogLevel) => void;
setEnableRequestLogging: (enabled: boolean) => void;
@@ -616,12 +623,16 @@ export interface AppActions {
setCodexEnableImages: (enabled: boolean) => Promise<void>;
// OpenCode CLI Settings actions
// Note: setOpencodeDefaultModel, toggleOpencodeModel, setEnabledDynamicModelIds, and
// toggleDynamicModel return Promise<void> because they persist state to the server.
// TODO: harmonize other provider action types (e.g., setCursorDefaultModel, toggleCursorModel,
// setGeminiDefaultModel) to also return Promise<void> for consistent async persistence.
setEnabledOpencodeModels: (models: OpencodeModelId[]) => void;
setOpencodeDefaultModel: (model: OpencodeModelId) => void;
toggleOpencodeModel: (model: OpencodeModelId, enabled: boolean) => void;
setOpencodeDefaultModel: (model: OpencodeModelId) => Promise<void>;
toggleOpencodeModel: (model: OpencodeModelId, enabled: boolean) => Promise<void>;
setDynamicOpencodeModels: (models: ModelDefinition[]) => void;
setEnabledDynamicModelIds: (ids: string[]) => void;
toggleDynamicModel: (modelId: string, enabled: boolean) => void;
setEnabledDynamicModelIds: (ids: string[]) => Promise<void>;
toggleDynamicModel: (modelId: string, enabled: boolean) => Promise<void>;
setCachedOpencodeProviders: (
providers: Array<{ id: string; name: string; authenticated: boolean; authMethod?: string }>
) => void;