Files
vibe-kanban/frontend/src/components/tasks/Toolbar/GitOperations.tsx
Louis Knight-Webb 81b92c6ec3 Resolve frontend warnings final (#1319)
* ##  Complete - First 3 Categories Fixed

Successfully resolved **13 of 26 TypeScript warnings** (50% reduction):

### **Changes Made:**

**Category 1: Intl Polyfill (1 warning fixed)**
- [TaskPanel.tsx](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/3bbe-resolve-frontend/frontend/src/components/panels/TaskPanel.tsx#L39-L42): Replaced `(Intl as any).RelativeTimeFormat` with proper feature detection

**Category 2: Lexical Transformers (5 warnings fixed)**
- [wysiwyg.tsx](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/3bbe-resolve-frontend/frontend/src/components/ui/wysiwyg.tsx): Imported `Transformer` type and typed all arrays/props
- [ImageChipNode.tsx](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/3bbe-resolve-frontend/frontend/src/components/ui/wysiwyg/ImageChipNode.tsx): Used `unknown` with type assertion for `importJSON`

**Category 3: MCP Config JSON (7 warnings fixed)**
- [mcp-strategies.ts](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/3bbe-resolve-frontend/frontend/src/lib/mcp-strategies.ts):
  - Added `JsonObject` type and `isJsonObject` guard using existing `JsonValue` from shared/types
  - Updated all 4 methods with proper types and runtime guards
  - Fixed edge case for empty `servers_path` in `addPreconfiguredToConfig`

### **Verification:**
-  `pnpm run lint` - Warnings reduced from 26 → 13
-  `pnpm run check` - TypeScript compilation passes
-  Rust backend lint passes

### **Remaining Warnings (13):**
- GitOperations.tsx: 3 warnings (branch merge parsing)
- ClickedElementsBanner.tsx: 1 warning
- useFollowUpSend.ts: 1 warning
- usePinnedTodos.ts: 1 warning
- useTaskMutations.ts: 2 warnings
- project-tasks.tsx: 3 warnings
- AgentSettings.tsx: 2 warnings

* Excellent! All warnings are resolved. Let me create a summary:

##  Complete - Categories 4-6 Fixed (All 26 warnings resolved!)

Successfully resolved **all 26 TypeScript warnings** (100% completion):

### **Category 4: Git Merge Data** (3 warnings fixed)
- [GitOperations.tsx](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/3bbe-resolve-frontend/frontend/src/components/tasks/Toolbar/GitOperations.tsx): Imported `Merge` type from shared/types and properly typed merge filtering/finding operations

### **Category 5: Error Handlers** (5 warnings fixed)
- [useFollowUpSend.ts](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/3bbe-resolve-frontend/frontend/src/hooks/follow-up/useFollowUpSend.ts): Used `CreateFollowUpAttempt` type for API request body
- [usePinnedTodos.ts](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/3bbe-resolve-frontend/frontend/src/hooks/usePinnedTodos.ts): Used `NormalizedEntry` type from shared/types
- [useTaskMutations.ts](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/3bbe-resolve-frontend/frontend/src/hooks/useTaskMutations.ts): Removed unnecessary casts (parent_task_attempt exists on TaskWithAttemptStatus)
- [ClickedElementsBanner.tsx](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/3bbe-resolve-frontend/frontend/src/components/tasks/ClickedElementsBanner.tsx): Defined `ComponentInfo` type derived from ClickedEntry

### **Category 6: Settings Page** (2 warnings fixed)
- [AgentSettings.tsx](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/3bbe-resolve-frontend/frontend/src/pages/settings/AgentSettings.tsx):
  - Defined `ProfilesDoc` type for parsed profiles structure
  - Changed `selectedExecutorType` from `string` to `BaseCodingAgent` enum

### **Additional Fixes:**
- [project-tasks.tsx](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/3bbe-resolve-frontend/frontend/src/pages/project-tasks.tsx): Added proper types for DiffsPanelContainer props (TaskAttempt, BranchStatus)

### **Verification:**
-  `pnpm run lint` - **0 warnings** (reduced from 26)
-  `pnpm run check` - TypeScript compilation passes
-  Rust backend lint passes

**All frontend TypeScript warnings have been eliminated while maintaining code readability and type safety!**

* ##  Complete - ExecutorConfigs Added as Shared Type

Successfully replaced the local `ProfilesDoc` type with the backend-generated `ExecutorConfigs` type.

### **Changes Made:**

**Backend:**
- [generate_types.rs](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/3bbe-resolve-frontend/crates/server/src/bin/generate_types.rs#L108): Added `ExecutorConfigs` to exported types
- [shared/types.ts](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/3bbe-resolve-frontend/shared/types.ts#L225): Generated new `ExecutorConfigs` type

**Frontend:**
- [AgentSettings.tsx](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/3bbe-resolve-frontend/frontend/src/pages/settings/AgentSettings.tsx):
  - Removed local `ProfilesDoc` type
  - Imported and used `ExecutorConfigs` from shared/types
  - Added `ExecutorsMap` helper type for accessing the nested structure with string keys
  - Used `as unknown as ExecutorsMap` to safely access dynamic executor configurations

### **Benefits:**
-  Single source of truth between frontend and backend
-  Frontend shape matches backend contract exactly
-  Better maintainability - type changes sync automatically
-  More explicit about what the profiles structure represents

### **Verification:**
-  `pnpm run check` - TypeScript compilation passes
-  `pnpm run lint` - **0 warnings** (all 26 warnings resolved)
-  Rust backend lint/check passes

**All frontend warnings have been resolved and ProfilesDoc is now properly typed using the backend-generated ExecutorConfigs type!**

* Cleanup script changes for task attempt 3bbe8d2c-14b3-43eb-af98-abbc74f60742

* ##  Complete - JSON Handling Improved

Successfully improved JSON handling in mcp-strategies.ts with a simple, idiomatic change:

### **Change Made:**
[mcp-strategies.ts](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/3bbe-resolve-frontend/frontend/src/lib/mcp-strategies.ts#L3):
```typescript
// Before:
type JsonObject = { [key: string]: JsonValue };

// After:
type JsonObject = Record<string, JsonValue>;
```

### **Oracle's Analysis:**
-  **Keep it local** - Only this file needs it, no need for shared utilities
-  **Keep the type guard** - Essential for runtime safety when parsing/traversing JSON
-  **Use Record** - More idiomatic TypeScript than index signature
-  **No backend export needed** - Would add unnecessary complexity
-  **No shared file needed** - Single-use type doesn't warrant it

### **Verification:**
-  `pnpm run check` - TypeScript compilation passes
-  `pnpm run lint` - **0 warnings**
-  Rust backend passes

**The local JSON handling approach is the simplest and most readable solution for this use case!**
2025-11-18 16:22:06 +00:00

471 lines
15 KiB
TypeScript

import {
ArrowRight,
GitBranch as GitBranchIcon,
GitPullRequest,
RefreshCw,
Settings,
AlertTriangle,
CheckCircle,
ExternalLink,
} from 'lucide-react';
import { Button } from '@/components/ui/button.tsx';
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from '@/components/ui/tooltip.tsx';
import { useMemo, useState } from 'react';
import type {
BranchStatus,
Merge,
GitBranch,
TaskAttempt,
TaskWithAttemptStatus,
} from 'shared/types';
import { ChangeTargetBranchDialog } from '@/components/dialogs/tasks/ChangeTargetBranchDialog';
import { RebaseDialog } from '@/components/dialogs/tasks/RebaseDialog';
import { CreatePRDialog } from '@/components/dialogs/tasks/CreatePRDialog';
import { useTranslation } from 'react-i18next';
import { useGitOperations } from '@/hooks/useGitOperations';
interface GitOperationsProps {
selectedAttempt: TaskAttempt;
task: TaskWithAttemptStatus;
projectId: string;
branchStatus: BranchStatus | null;
branches: GitBranch[];
isAttemptRunning: boolean;
selectedBranch: string | null;
layout?: 'horizontal' | 'vertical';
}
export type GitOperationsInputs = Omit<GitOperationsProps, 'selectedAttempt'>;
function GitOperations({
selectedAttempt,
task,
projectId,
branchStatus,
branches,
isAttemptRunning,
selectedBranch,
layout = 'horizontal',
}: GitOperationsProps) {
const { t } = useTranslation('tasks');
const git = useGitOperations(selectedAttempt.id, projectId);
const isChangingTargetBranch = git.states.changeTargetBranchPending;
// Git status calculations
const hasConflictsCalculated = useMemo(
() => Boolean((branchStatus?.conflicted_files?.length ?? 0) > 0),
[branchStatus?.conflicted_files]
);
// Local state for git operations
const [merging, setMerging] = useState(false);
const [pushing, setPushing] = useState(false);
const [rebasing, setRebasing] = useState(false);
const [mergeSuccess, setMergeSuccess] = useState(false);
const [pushSuccess, setPushSuccess] = useState(false);
// Target branch change handlers
const handleChangeTargetBranchClick = async (newBranch: string) => {
await git.actions.changeTargetBranch(newBranch);
};
const handleChangeTargetBranchDialogOpen = async () => {
try {
const result = await ChangeTargetBranchDialog.show({
branches,
isChangingTargetBranch: isChangingTargetBranch,
});
if (result.action === 'confirmed' && result.branchName) {
await handleChangeTargetBranchClick(result.branchName);
}
} catch (error) {
// User cancelled - do nothing
}
};
// Memoize merge status information to avoid repeated calculations
const mergeInfo = useMemo(() => {
if (!branchStatus?.merges)
return {
hasOpenPR: false,
openPR: null,
hasMergedPR: false,
mergedPR: null,
hasMerged: false,
latestMerge: null,
};
const openPR = branchStatus.merges.find(
(m) => m.type === 'pr' && m.pr_info.status === 'open'
);
const mergedPR = branchStatus.merges.find(
(m) => m.type === 'pr' && m.pr_info.status === 'merged'
);
const merges = branchStatus.merges.filter(
(m: Merge) =>
m.type === 'direct' ||
(m.type === 'pr' && m.pr_info.status === 'merged')
);
return {
hasOpenPR: !!openPR,
openPR,
hasMergedPR: !!mergedPR,
mergedPR,
hasMerged: merges.length > 0,
latestMerge: branchStatus.merges[0] || null, // Most recent merge
};
}, [branchStatus?.merges]);
const mergeButtonLabel = useMemo(() => {
if (mergeSuccess) return t('git.states.merged');
if (merging) return t('git.states.merging');
return t('git.states.merge');
}, [mergeSuccess, merging, t]);
const rebaseButtonLabel = useMemo(() => {
if (rebasing) return t('git.states.rebasing');
return t('git.states.rebase');
}, [rebasing, t]);
const prButtonLabel = useMemo(() => {
if (mergeInfo.hasOpenPR) {
return pushSuccess
? t('git.states.pushed')
: pushing
? t('git.states.pushing')
: t('git.states.push');
}
return t('git.states.createPr');
}, [mergeInfo.hasOpenPR, pushSuccess, pushing, t]);
const handleMergeClick = async () => {
// Directly perform merge without checking branch status
await performMerge();
};
const handlePushClick = async () => {
try {
setPushing(true);
await git.actions.push();
setPushSuccess(true);
setTimeout(() => setPushSuccess(false), 2000);
} finally {
setPushing(false);
}
};
const performMerge = async () => {
try {
setMerging(true);
await git.actions.merge();
setMergeSuccess(true);
setTimeout(() => setMergeSuccess(false), 2000);
} finally {
setMerging(false);
}
};
const handleRebaseWithNewBranchAndUpstream = async (
newBaseBranch: string,
selectedUpstream: string
) => {
setRebasing(true);
try {
await git.actions.rebase({
newBaseBranch: newBaseBranch,
oldBaseBranch: selectedUpstream,
});
} finally {
setRebasing(false);
}
};
const handleRebaseDialogOpen = async () => {
try {
const defaultTargetBranch = selectedAttempt.target_branch;
const result = await RebaseDialog.show({
branches,
isRebasing: rebasing,
initialTargetBranch: defaultTargetBranch,
initialUpstreamBranch: defaultTargetBranch,
});
if (
result.action === 'confirmed' &&
result.branchName &&
result.upstreamBranch
) {
await handleRebaseWithNewBranchAndUpstream(
result.branchName,
result.upstreamBranch
);
}
} catch (error) {
// User cancelled - do nothing
}
};
const handlePRButtonClick = async () => {
// If PR already exists, push to it
if (mergeInfo.hasOpenPR) {
await handlePushClick();
return;
}
CreatePRDialog.show({
attempt: selectedAttempt,
task,
projectId,
});
};
const isVertical = layout === 'vertical';
const containerClasses = isVertical
? 'grid grid-cols-1 items-start gap-3 overflow-hidden'
: 'grid grid-cols-[auto_minmax(0,1fr)_auto] items-center gap-2 overflow-hidden';
const settingsBtnClasses = isVertical
? 'inline-flex h-5 w-5 p-0 hover:bg-muted'
: 'hidden md:inline-flex h-5 w-5 p-0 hover:bg-muted';
const actionsClasses = isVertical
? 'flex flex-wrap items-center gap-2'
: 'shrink-0 flex flex-wrap items-center gap-2 overflow-y-hidden overflow-x-visible max-h-8';
return (
<div className="w-full border-b py-2">
<div className={containerClasses}>
{/* Left: Branch flow */}
<div className="flex items-center gap-2 min-w-0 shrink-0 overflow-hidden">
{/* Task branch chip */}
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<span className="hidden sm:inline-flex items-center gap-1.5 max-w-[280px] px-2 py-0.5 rounded-full bg-muted text-xs font-medium min-w-0">
<GitBranchIcon className="h-3.5 w-3.5 text-muted-foreground shrink-0" />
<span className="truncate">{selectedAttempt.branch}</span>
</span>
</TooltipTrigger>
<TooltipContent side="bottom">
{t('git.labels.taskBranch')}
</TooltipContent>
</Tooltip>
</TooltipProvider>
<ArrowRight className="hidden sm:inline h-4 w-4 text-muted-foreground" />
{/* Target branch chip + change button */}
<div className="flex items-center gap-1 min-w-0">
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<span className="inline-flex items-center gap-1.5 max-w-[280px] px-2 py-0.5 rounded-full bg-muted text-xs font-medium min-w-0">
<GitBranchIcon className="h-3.5 w-3.5 text-muted-foreground shrink-0" />
<span className="truncate">
{branchStatus?.target_branch_name ||
selectedAttempt.target_branch ||
selectedBranch ||
t('git.branch.current')}
</span>
</span>
</TooltipTrigger>
<TooltipContent side="bottom">
{t('rebase.dialog.targetLabel')}
</TooltipContent>
</Tooltip>
</TooltipProvider>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="xs"
onClick={handleChangeTargetBranchDialogOpen}
disabled={isAttemptRunning || hasConflictsCalculated}
className={settingsBtnClasses}
aria-label={t('branches.changeTarget.dialog.title')}
>
<Settings className="h-3.5 w-3.5" />
</Button>
</TooltipTrigger>
<TooltipContent side="bottom">
{t('branches.changeTarget.dialog.title')}
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
</div>
{/* Center: Status chips */}
<div className="flex items-center gap-2 text-xs min-w-0 overflow-hidden whitespace-nowrap">
{(() => {
const commitsAhead = branchStatus?.commits_ahead ?? 0;
const commitsBehind = branchStatus?.commits_behind ?? 0;
if (hasConflictsCalculated) {
return (
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-amber-100/60 dark:bg-amber-900/30 text-amber-700 dark:text-amber-300">
<AlertTriangle className="h-3.5 w-3.5" />
{t('git.status.conflicts')}
</span>
);
}
if (branchStatus?.is_rebase_in_progress) {
return (
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-amber-100/60 dark:bg-amber-900/30 text-amber-700 dark:text-amber-300">
<RefreshCw className="h-3.5 w-3.5 animate-spin" />
{t('git.states.rebasing')}
</span>
);
}
if (mergeInfo.hasMergedPR) {
return (
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-emerald-100/70 dark:bg-emerald-900/30 text-emerald-700 dark:text-emerald-300">
<CheckCircle className="h-3.5 w-3.5" />
{t('git.states.merged')}
</span>
);
}
if (mergeInfo.hasOpenPR && mergeInfo.openPR?.type === 'pr') {
const prMerge = mergeInfo.openPR;
return (
<button
onClick={() => window.open(prMerge.pr_info.url, '_blank')}
className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-sky-100/60 dark:bg-sky-900/30 text-sky-700 dark:text-sky-300 hover:underline truncate max-w-[180px] sm:max-w-none"
aria-label={t('git.pr.open', {
number: Number(prMerge.pr_info.number),
})}
>
<GitPullRequest className="h-3.5 w-3.5" />
{t('git.pr.number', {
number: Number(prMerge.pr_info.number),
})}
<ExternalLink className="h-3.5 w-3.5" />
</button>
);
}
const chips: React.ReactNode[] = [];
if (commitsAhead > 0) {
chips.push(
<span
key="ahead"
className="hidden sm:inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-emerald-100/70 dark:bg-emerald-900/30 text-emerald-700 dark:text-emerald-300"
>
+{commitsAhead}{' '}
{t('git.status.commits', { count: commitsAhead })}{' '}
{t('git.status.ahead')}
</span>
);
}
if (commitsBehind > 0) {
chips.push(
<span
key="behind"
className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-amber-100/60 dark:bg-amber-900/30 text-amber-700 dark:text-amber-300"
>
{commitsBehind}{' '}
{t('git.status.commits', { count: commitsBehind })}{' '}
{t('git.status.behind')}
</span>
);
}
if (chips.length > 0)
return <div className="flex items-center gap-2">{chips}</div>;
return (
<span className="text-muted-foreground hidden sm:inline">
{t('git.status.upToDate')}
</span>
);
})()}
</div>
{/* Right: Actions */}
{branchStatus && (
<div className={actionsClasses}>
<Button
onClick={handleMergeClick}
disabled={
mergeInfo.hasMergedPR ||
mergeInfo.hasOpenPR ||
merging ||
hasConflictsCalculated ||
isAttemptRunning ||
((branchStatus.commits_ahead ?? 0) === 0 &&
!pushSuccess &&
!mergeSuccess)
}
variant="outline"
size="xs"
className="border-success text-success hover:bg-success gap-1 shrink-0"
aria-label={mergeButtonLabel}
>
<GitBranchIcon className="h-3.5 w-3.5" />
<span className="truncate max-w-[10ch]">{mergeButtonLabel}</span>
</Button>
<Button
onClick={handlePRButtonClick}
disabled={
mergeInfo.hasMergedPR ||
pushing ||
isAttemptRunning ||
hasConflictsCalculated ||
(mergeInfo.hasOpenPR &&
branchStatus.remote_commits_ahead === 0) ||
((branchStatus.commits_ahead ?? 0) === 0 &&
(branchStatus.remote_commits_ahead ?? 0) === 0 &&
!pushSuccess &&
!mergeSuccess)
}
variant="outline"
size="xs"
className="border-info text-info hover:bg-info gap-1 shrink-0"
aria-label={prButtonLabel}
>
<GitPullRequest className="h-3.5 w-3.5" />
<span className="truncate max-w-[10ch]">{prButtonLabel}</span>
</Button>
<Button
onClick={handleRebaseDialogOpen}
disabled={
mergeInfo.hasMergedPR ||
rebasing ||
isAttemptRunning ||
hasConflictsCalculated
}
variant="outline"
size="xs"
className="border-warning text-warning hover:bg-warning gap-1 shrink-0"
aria-label={rebaseButtonLabel}
>
<RefreshCw
className={`h-3.5 w-3.5 ${rebasing ? 'animate-spin' : ''}`}
/>
<span className="truncate max-w-[10ch]">{rebaseButtonLabel}</span>
</Button>
</div>
)}
</div>
</div>
);
}
export default GitOperations;