fix(workspace-state): stop resync loop on non-git folders and allow retry for degraded watcher

Gate git refresh on .git presence so file churn in non-git workspaces no longer produces endless resync_hint events, and silently log tree/git refresh errors during watch flushing instead of flagging requires_resync, which turned transient failures into self-reinforcing loops.

Degrade gracefully when the filesystem watcher fails to attach (e.g. permission denied, inotify quota): keep the initial snapshot, surface a degraded flag, and expose a store-level restart that the banner uses to retry attachment after the root cause is fixed.

Propagate is_git_repo through the snapshot so the git log and changes tabs render a dedicated "Not a Git repository" empty state instead of raw git stderr with a useless retry button.

Stop polling get_git_branch from the title bar once it returns null and re-arm on visibility change.

Add translations for the new banner, empty-state, and retry keys across all ten locales.
This commit is contained in:
xintaofei
2026-04-18 17:18:11 +08:00
parent c5c2bdd331
commit 7ef8d84d44
19 changed files with 380 additions and 74 deletions

View File

@@ -0,0 +1,59 @@
"use client"
import { useState } from "react"
import { RefreshCw, TriangleAlert } from "lucide-react"
import { useTranslations } from "next-intl"
import { Button } from "@/components/ui/button"
interface WorkspaceDegradedBannerProps {
onRetry?: () => void | Promise<void>
}
export function WorkspaceDegradedBanner({
onRetry,
}: WorkspaceDegradedBannerProps) {
const t = useTranslations("Folder.workspaceStatus")
const [retrying, setRetrying] = useState(false)
const handleRetry = async () => {
if (!onRetry || retrying) return
setRetrying(true)
try {
await onRetry()
} finally {
setRetrying(false)
}
}
return (
<div
className="flex items-start gap-2 px-2 py-1.5 text-[11px] text-amber-700 dark:text-amber-400 bg-amber-100/60 dark:bg-amber-900/20 border-b border-amber-300/50 dark:border-amber-800/50"
role="status"
aria-live="polite"
title={t("degradedHint")}
>
<TriangleAlert className="size-3.5 mt-0.5 shrink-0" aria-hidden />
<div className="leading-snug flex-1">
<span className="font-medium">{t("degradedTitle")}</span>
<span className="ml-1 text-muted-foreground">{t("degradedHint")}</span>
</div>
{onRetry && (
<Button
variant="ghost"
size="xs"
className="h-5 px-1.5 -my-0.5 shrink-0 text-amber-700 dark:text-amber-400 hover:bg-amber-200/60 dark:hover:bg-amber-900/40"
onClick={() => {
void handleRetry()
}}
disabled={retrying}
>
<RefreshCw
className={`size-3 ${retrying ? "animate-spin" : ""}`}
aria-hidden
/>
<span className="ml-1">{retrying ? t("retrying") : t("retry")}</span>
</Button>
)}
</div>
)
}