fix(ui): bulk update cache invalidation and model dropdown display (#633)

Fix two related issues with bulk model updates in Kanban view:

1. Bulk update now properly invalidates React Query cache
   - Changed handleBulkUpdate and bulk verify handler to call loadFeatures()
   - This ensures UI immediately reflects bulk changes

2. Custom provider models (GLM, MiniMax, etc.) now display correctly
   - Added fallback lookup in PhaseModelSelector by model ID
   - Updated mass-edit-dialog to track providerId after selection
This commit is contained in:
Stefan de Vogelaere
2026-01-20 23:01:06 +01:00
committed by GitHub
parent 47a6033b43
commit 4f584f9a89
3 changed files with 48 additions and 11 deletions

View File

@@ -636,10 +636,8 @@ export function BoardView() {
const result = await api.features.bulkUpdate(currentProject.path, featureIds, finalUpdates); const result = await api.features.bulkUpdate(currentProject.path, featureIds, finalUpdates);
if (result.success) { if (result.success) {
// Update local state // Invalidate React Query cache to refetch features with server-updated values
featureIds.forEach((featureId) => { loadFeatures();
updateFeature(featureId, finalUpdates);
});
toast.success(`Updated ${result.updatedCount} features`); toast.success(`Updated ${result.updatedCount} features`);
exitSelectionMode(); exitSelectionMode();
} else { } else {
@@ -655,7 +653,7 @@ export function BoardView() {
[ [
currentProject, currentProject,
selectedFeatureIds, selectedFeatureIds,
updateFeature, loadFeatures,
exitSelectionMode, exitSelectionMode,
getPrimaryWorktreeBranch, getPrimaryWorktreeBranch,
addAndSelectWorktree, addAndSelectWorktree,
@@ -783,10 +781,8 @@ export function BoardView() {
const result = await api.features.bulkUpdate(currentProject.path, featureIds, updates); const result = await api.features.bulkUpdate(currentProject.path, featureIds, updates);
if (result.success) { if (result.success) {
// Update local state for all features // Invalidate React Query cache to refetch features with server-updated values
featureIds.forEach((featureId) => { loadFeatures();
updateFeature(featureId, updates);
});
toast.success(`Verified ${result.updatedCount} features`); toast.success(`Verified ${result.updatedCount} features`);
exitSelectionMode(); exitSelectionMode();
} else { } else {
@@ -798,7 +794,7 @@ export function BoardView() {
logger.error('Bulk verify failed:', error); logger.error('Bulk verify failed:', error);
toast.error('Failed to verify features'); toast.error('Failed to verify features');
} }
}, [currentProject, selectedFeatureIds, updateFeature, exitSelectionMode]); }, [currentProject, selectedFeatureIds, loadFeatures, exitSelectionMode]);
// Handler for addressing PR comments - creates a feature and starts it automatically // Handler for addressing PR comments - creates a feature and starts it automatically
const handleAddressPRComments = useCallback( const handleAddressPRComments = useCallback(

View File

@@ -128,6 +128,7 @@ export function MassEditDialog({
// Field values // Field values
const [model, setModel] = useState<ModelAlias>('claude-sonnet'); const [model, setModel] = useState<ModelAlias>('claude-sonnet');
const [thinkingLevel, setThinkingLevel] = useState<ThinkingLevel>('none'); const [thinkingLevel, setThinkingLevel] = useState<ThinkingLevel>('none');
const [providerId, setProviderId] = useState<string | undefined>(undefined);
const [planningMode, setPlanningMode] = useState<PlanningMode>('skip'); const [planningMode, setPlanningMode] = useState<PlanningMode>('skip');
const [requirePlanApproval, setRequirePlanApproval] = useState(false); const [requirePlanApproval, setRequirePlanApproval] = useState(false);
const [priority, setPriority] = useState(2); const [priority, setPriority] = useState(2);
@@ -162,6 +163,7 @@ export function MassEditDialog({
}); });
setModel(getInitialValue(selectedFeatures, 'model', 'claude-sonnet') as ModelAlias); setModel(getInitialValue(selectedFeatures, 'model', 'claude-sonnet') as ModelAlias);
setThinkingLevel(getInitialValue(selectedFeatures, 'thinkingLevel', 'none') as ThinkingLevel); setThinkingLevel(getInitialValue(selectedFeatures, 'thinkingLevel', 'none') as ThinkingLevel);
setProviderId(undefined); // Features don't store providerId, but we track it after selection
setPlanningMode(getInitialValue(selectedFeatures, 'planningMode', 'skip') as PlanningMode); setPlanningMode(getInitialValue(selectedFeatures, 'planningMode', 'skip') as PlanningMode);
setRequirePlanApproval(getInitialValue(selectedFeatures, 'requirePlanApproval', false)); setRequirePlanApproval(getInitialValue(selectedFeatures, 'requirePlanApproval', false));
setPriority(getInitialValue(selectedFeatures, 'priority', 2)); setPriority(getInitialValue(selectedFeatures, 'priority', 2));
@@ -226,10 +228,11 @@ export function MassEditDialog({
Select a specific model configuration Select a specific model configuration
</p> </p>
<PhaseModelSelector <PhaseModelSelector
value={{ model, thinkingLevel }} value={{ model, thinkingLevel, providerId }}
onChange={(entry: PhaseModelEntry) => { onChange={(entry: PhaseModelEntry) => {
setModel(entry.model as ModelAlias); setModel(entry.model as ModelAlias);
setThinkingLevel(entry.thinkingLevel || 'none'); setThinkingLevel(entry.thinkingLevel || 'none');
setProviderId(entry.providerId);
// Auto-enable model and thinking level for apply state // Auto-enable model and thinking level for apply state
setApplyState((prev) => ({ setApplyState((prev) => ({
...prev, ...prev,

View File

@@ -415,6 +415,44 @@ export function PhaseModelSelector({
} }
} }
// Fallback: Check ClaudeCompatibleProvider models by model ID only (when providerId is not set)
// This handles cases where features store model ID but not providerId
for (const provider of enabledProviders) {
const providerModel = provider.models?.find((m) => m.id === selectedModel);
if (providerModel) {
// Count providers of same type to determine if we need provider name suffix
const sameTypeCount = enabledProviders.filter(
(p) => p.providerType === provider.providerType
).length;
const suffix = sameTypeCount > 1 ? ` (${provider.name})` : '';
// Add thinking level to label if not 'none'
const thinkingLabel =
selectedThinkingLevel !== 'none'
? ` (${THINKING_LEVEL_LABELS[selectedThinkingLevel]} Thinking)`
: '';
// Get icon based on provider type
const getIconForProviderType = () => {
switch (provider.providerType) {
case 'glm':
return GlmIcon;
case 'minimax':
return MiniMaxIcon;
case 'openrouter':
return OpenRouterIcon;
default:
return getProviderIconForModel(providerModel.id) || OpenRouterIcon;
}
};
return {
id: selectedModel,
label: `${providerModel.displayName}${suffix}${thinkingLabel}`,
description: provider.name,
provider: 'claude-compatible' as const,
icon: getIconForProviderType(),
};
}
}
return null; return null;
}, [ }, [
selectedModel, selectedModel,