feat: Show timeout indicator for repository selector (Vibe Kanban) (#1510)

* Done. I've added a 5-second delay to the `list_git_repos` endpoint at `crates/server/src/routes/filesystem.rs:46`. This will let you see:

- "Searching for repositories..." for the first 2 seconds
- "Still searching... (2s)", "Still searching... (3s)", etc. after that
- The "This is taking longer than usual" message after 3 seconds

Remember to remove this delay (marked with `TODO`) once you're done testing!

* Done, the delay has been removed.

* All checks pass. The i18n script now passes with no new violations introduced.

* feat: Add timeout indicator to repository selector

- Shows progressive feedback when repo search takes longer than expected
- Displays "no repos found" message when search completes empty
- Added translations for all 6 locales (EN, ES, JA, KO, ZH-Hans, ZH-Hant)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Alex Netsch
2026-01-08 13:29:31 +00:00
committed by GitHub
parent 0753a4ff94
commit b63c90186e
7 changed files with 100 additions and 3 deletions

View File

@@ -1,4 +1,5 @@
import { useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
@@ -38,6 +39,7 @@ const RepoPickerDialogImpl = NiceModal.create<RepoPickerDialogProps>(
title = 'Select Repository',
description = 'Choose or create a git repository',
}) => {
const { t } = useTranslation('projects');
const modal = useModal();
const [stage, setStage] = useState<Stage>('options');
const [error, setError] = useState('');
@@ -47,6 +49,8 @@ const RepoPickerDialogImpl = NiceModal.create<RepoPickerDialogProps>(
const [allRepos, setAllRepos] = useState<DirectoryEntry[]>([]);
const [reposLoading, setReposLoading] = useState(false);
const [showMoreRepos, setShowMoreRepos] = useState(false);
const [loadingDuration, setLoadingDuration] = useState(0);
const [hasSearched, setHasSearched] = useState(false);
// Stage: new
const [repoName, setRepoName] = useState('');
@@ -60,12 +64,15 @@ const RepoPickerDialogImpl = NiceModal.create<RepoPickerDialogProps>(
setShowMoreRepos(false);
setRepoName('');
setParentPath('');
setLoadingDuration(0);
setHasSearched(false);
}
}, [modal.visible]);
const loadRecentRepos = useCallback(async () => {
setReposLoading(true);
setError('');
setLoadingDuration(0);
try {
const repos = await fileSystemApi.listGitRepos();
setAllRepos(repos);
@@ -74,14 +81,33 @@ const RepoPickerDialogImpl = NiceModal.create<RepoPickerDialogProps>(
console.error('Failed to load repos:', err);
} finally {
setReposLoading(false);
setHasSearched(true);
}
}, []);
useEffect(() => {
if (stage === 'existing' && allRepos.length === 0 && !reposLoading) {
if (
stage === 'existing' &&
allRepos.length === 0 &&
!reposLoading &&
!hasSearched
) {
loadRecentRepos();
}
}, [stage, allRepos.length, reposLoading, loadRecentRepos]);
}, [stage, allRepos.length, reposLoading, hasSearched, loadRecentRepos]);
// Track loading duration to show timeout message
useEffect(() => {
if (!reposLoading) {
return;
}
const interval = setInterval(() => {
setLoadingDuration((prev) => prev + 1);
}, 1000);
return () => clearInterval(interval);
}, [reposLoading]);
const registerAndReturn = async (path: string) => {
setIsWorking(true);
@@ -220,9 +246,18 @@ const RepoPickerDialogImpl = NiceModal.create<RepoPickerDialogProps>(
<div className="flex items-center gap-3">
<div className="animate-spin h-5 w-5 border-2 border-muted-foreground border-t-transparent rounded-full" />
<div className="text-sm text-muted-foreground">
Loading repositories...
{loadingDuration < 2
? t('repoSearch.searching')
: t('repoSearch.stillSearching', {
seconds: loadingDuration,
})}
</div>
</div>
{loadingDuration >= 3 && (
<div className="text-xs text-muted-foreground mt-2 ml-8">
{t('repoSearch.takingLonger')}
</div>
)}
</div>
)}
@@ -269,6 +304,26 @@ const RepoPickerDialogImpl = NiceModal.create<RepoPickerDialogProps>(
</div>
)}
{/* No repos found state */}
{!reposLoading &&
hasSearched &&
allRepos.length === 0 &&
!error && (
<div className="p-4 border rounded-lg bg-card">
<div className="flex items-start gap-3">
<Folder className="h-5 w-5 mt-0.5 flex-shrink-0 text-muted-foreground" />
<div>
<div className="text-sm text-muted-foreground">
{t('repoSearch.noReposFound')}
</div>
<div className="text-xs text-muted-foreground mt-1">
{t('repoSearch.browseHint')}
</div>
</div>
</div>
</div>
)}
<div
className="p-4 border border-dashed cursor-pointer hover:shadow-md transition-shadow rounded-lg bg-card"
onClick={() => !isWorking && handleBrowseForRepo()}

View File

@@ -4,6 +4,13 @@
"createProject": "Create Project",
"linkToOrganization": "Link to Remote Project",
"loading": "Loading projects...",
"repoSearch": {
"searching": "Searching for repositories...",
"stillSearching": "Still searching... ({{seconds}}s)",
"takingLonger": "This is taking longer than usual. You can browse manually below.",
"noReposFound": "No repositories found in common locations.",
"browseHint": "Use the option below to browse for a repository."
},
"errors": {
"fetchFailed": "Failed to fetch projects",
"deleteFailed": "Failed to delete project"

View File

@@ -4,6 +4,13 @@
"createProject": "Crear Proyecto",
"linkToOrganization": "Vincular a Proyecto Remoto",
"loading": "Cargando proyectos...",
"repoSearch": {
"searching": "Buscando repositorios...",
"stillSearching": "Aún buscando... ({{seconds}}s)",
"takingLonger": "Esto está tardando más de lo habitual. Puedes buscar manualmente abajo.",
"noReposFound": "No se encontraron repositorios en ubicaciones comunes.",
"browseHint": "Usa la opción de abajo para buscar un repositorio."
},
"errors": {
"fetchFailed": "Error al cargar proyectos",
"deleteFailed": "Error al eliminar el proyecto"

View File

@@ -4,6 +4,13 @@
"createProject": "プロジェクトを作成",
"linkToOrganization": "リモートプロジェクトにリンク",
"loading": "プロジェクトを読み込み中...",
"repoSearch": {
"searching": "リポジトリを検索中...",
"stillSearching": "検索中... ({{seconds}}秒)",
"takingLonger": "通常より時間がかかっています。下から手動で選択できます。",
"noReposFound": "一般的な場所にリポジトリが見つかりませんでした。",
"browseHint": "下のオプションを使用してリポジトリを参照してください。"
},
"errors": {
"fetchFailed": "プロジェクトの取得に失敗しました",
"deleteFailed": "プロジェクトの削除に失敗しました"

View File

@@ -4,6 +4,13 @@
"createProject": "프로젝트 생성",
"linkToOrganization": "원격 프로젝트에 연결",
"loading": "프로젝트 로딩 중...",
"repoSearch": {
"searching": "저장소 검색 중...",
"stillSearching": "검색 중... ({{seconds}}초)",
"takingLonger": "평소보다 시간이 오래 걸리고 있습니다. 아래에서 수동으로 찾아볼 수 있습니다.",
"noReposFound": "일반적인 위치에서 저장소를 찾을 수 없습니다.",
"browseHint": "아래 옵션을 사용하여 저장소를 찾아보세요."
},
"errors": {
"fetchFailed": "프로젝트를 불러오지 못했습니다",
"deleteFailed": "프로젝트 삭제에 실패했습니다"

View File

@@ -4,6 +4,13 @@
"createProject": "创建项目",
"linkToOrganization": "链接到远程项目",
"loading": "加载项目中...",
"repoSearch": {
"searching": "正在搜索仓库...",
"stillSearching": "仍在搜索... ({{seconds}}秒)",
"takingLonger": "搜索时间比平时长。您可以在下方手动浏览。",
"noReposFound": "在常见位置未找到仓库。",
"browseHint": "使用下方选项浏览仓库。"
},
"errors": {
"fetchFailed": "获取项目失败",
"deleteFailed": "删除项目失败"

View File

@@ -4,6 +4,13 @@
"createProject": "建立專案",
"linkToOrganization": "連結到遠端專案",
"loading": "載入專案中...",
"repoSearch": {
"searching": "正在搜尋儲存庫...",
"stillSearching": "仍在搜尋... ({{seconds}}秒)",
"takingLonger": "搜尋時間比平時長。您可以在下方手動瀏覽。",
"noReposFound": "在常見位置未找到儲存庫。",
"browseHint": "使用下方選項瀏覽儲存庫。"
},
"errors": {
"fetchFailed": "取得專案失敗",
"deleteFailed": "刪除專案失敗"