refactor(workspace): remove welcome page and render conversation panel directly
This commit is contained in:
@@ -1389,7 +1389,6 @@ export function ConversationDetailPanel() {
|
||||
|
||||
const canTile = isTileMode && tabs.length > 1
|
||||
|
||||
// Empty state: no tabs at all — show full-screen welcome
|
||||
if (hasNoTabs) {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -1,199 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useMemo } from "react"
|
||||
import { useTranslations } from "next-intl"
|
||||
import { toast } from "sonner"
|
||||
import { cloneRepository } from "@/lib/api"
|
||||
import { isDesktop, openFileDialog } from "@/lib/platform"
|
||||
import { useAppWorkspace } from "@/contexts/app-workspace-context"
|
||||
import { useGitCredential } from "@/contexts/git-credential-context"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogFooter,
|
||||
} from "@/components/ui/dialog"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { FolderOpen, Loader2 } from "lucide-react"
|
||||
import { resolveCloneError } from "@/components/workspace-empty/error-utils"
|
||||
import { DirectoryBrowserDialog } from "@/components/shared/directory-browser-dialog"
|
||||
|
||||
interface CloneDialogProps {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
}
|
||||
|
||||
export function CloneDialog({ open: isOpen, onOpenChange }: CloneDialogProps) {
|
||||
const t = useTranslations("WelcomePage")
|
||||
const { withCredentialRetry } = useGitCredential()
|
||||
const { openFolder } = useAppWorkspace()
|
||||
const [url, setUrl] = useState("")
|
||||
const [targetDir, setTargetDir] = useState("")
|
||||
const [cloning, setCloning] = useState(false)
|
||||
const [browserOpen, setBrowserOpen] = useState(false)
|
||||
const [error, setError] = useState<{
|
||||
message: string
|
||||
detail: string | null
|
||||
} | null>(null)
|
||||
|
||||
const repoName = useMemo(
|
||||
() =>
|
||||
url
|
||||
.replace(/\.git$/, "")
|
||||
.split("/")
|
||||
.pop() ?? "repo",
|
||||
[url]
|
||||
)
|
||||
|
||||
const handleBrowse = async () => {
|
||||
if (isDesktop()) {
|
||||
const selected = await openFileDialog({
|
||||
directory: true,
|
||||
multiple: false,
|
||||
})
|
||||
if (selected) {
|
||||
setTargetDir(Array.isArray(selected) ? selected[0] : selected)
|
||||
}
|
||||
} else {
|
||||
setBrowserOpen(true)
|
||||
}
|
||||
}
|
||||
|
||||
const handleClone = async () => {
|
||||
if (!url || !targetDir) return
|
||||
|
||||
const fullPath = `${targetDir}/${repoName}`
|
||||
|
||||
setCloning(true)
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
await withCredentialRetry(
|
||||
(creds) => cloneRepository(url, fullPath, creds),
|
||||
{ remoteUrl: url }
|
||||
)
|
||||
await openFolder(fullPath)
|
||||
onOpenChange(false)
|
||||
resetForm()
|
||||
} catch (err) {
|
||||
const resolvedError = resolveCloneError(err)
|
||||
setError({
|
||||
message: t(resolvedError.key),
|
||||
detail: resolvedError.detail ?? null,
|
||||
})
|
||||
toast.error(t("toasts.cloneFailed"), {
|
||||
description: resolvedError.detail ?? t(resolvedError.key),
|
||||
})
|
||||
} finally {
|
||||
setCloning(false)
|
||||
}
|
||||
}
|
||||
|
||||
const resetForm = () => {
|
||||
setUrl("")
|
||||
setTargetDir("")
|
||||
setError(null)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog
|
||||
open={isOpen}
|
||||
onOpenChange={(v) => {
|
||||
onOpenChange(v)
|
||||
if (!v) resetForm()
|
||||
}}
|
||||
>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t("cloneDialog.title")}</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4 py-2">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="repo-url">{t("cloneDialog.repositoryUrl")}</Label>
|
||||
<Input
|
||||
id="repo-url"
|
||||
placeholder={t("cloneDialog.repositoryUrlPlaceholder")}
|
||||
value={url}
|
||||
onChange={(e) => setUrl(e.target.value)}
|
||||
disabled={cloning}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="target-dir">{t("cloneDialog.directory")}</Label>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
id="target-dir"
|
||||
placeholder={t("cloneDialog.directoryPlaceholder")}
|
||||
value={targetDir}
|
||||
onChange={(e) => setTargetDir(e.target.value)}
|
||||
disabled={cloning}
|
||||
className="flex-1"
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={handleBrowse}
|
||||
disabled={cloning}
|
||||
title={t("cloneDialog.browseDirectory")}
|
||||
aria-label={t("cloneDialog.browseDirectory")}
|
||||
type="button"
|
||||
>
|
||||
<FolderOpen className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
{targetDir && url && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("cloneDialog.clonePath", {
|
||||
path: `${targetDir}/${repoName}`,
|
||||
})}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm text-destructive">{error.message}</p>
|
||||
{error.detail && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{error.detail}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => onOpenChange(false)}
|
||||
disabled={cloning}
|
||||
type="button"
|
||||
>
|
||||
{t("cloneDialog.cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleClone}
|
||||
disabled={!url || !targetDir || cloning}
|
||||
type="button"
|
||||
>
|
||||
{cloning && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
|
||||
{t("cloneDialog.clone")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<DirectoryBrowserDialog
|
||||
open={browserOpen}
|
||||
onOpenChange={setBrowserOpen}
|
||||
onSelect={(path) => setTargetDir(path)}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,150 +0,0 @@
|
||||
import { extractAppCommandError, toErrorMessage } from "@/lib/app-error"
|
||||
|
||||
export type WelcomeErrorKey =
|
||||
| "errors.unknown"
|
||||
| "errors.invalidInput"
|
||||
| "errors.notFound"
|
||||
| "errors.alreadyExists"
|
||||
| "errors.dependencyMissing"
|
||||
| "errors.databaseError"
|
||||
| "errors.ioError"
|
||||
| "errors.externalCommandFailed"
|
||||
| "errors.windowOperationFailed"
|
||||
| "errors.gitNotInstalled"
|
||||
| "errors.targetDirectoryNotEmpty"
|
||||
| "errors.repositoryNotFound"
|
||||
| "errors.networkUnavailable"
|
||||
| "errors.authenticationFailed"
|
||||
| "errors.permissionDenied"
|
||||
|
||||
export interface WelcomeErrorResult {
|
||||
key: WelcomeErrorKey
|
||||
detail?: string
|
||||
}
|
||||
|
||||
export function normalizeErrorMessage(error: unknown): string {
|
||||
return toErrorMessage(error)
|
||||
}
|
||||
|
||||
function stripClonePrefix(message: string): string {
|
||||
return message.replace(/^clone failed:\s*/i, "").trim()
|
||||
}
|
||||
|
||||
function mapCommonCodeToKey(code: string): WelcomeErrorKey {
|
||||
switch (code) {
|
||||
case "invalid_input":
|
||||
case "configuration_missing":
|
||||
case "configuration_invalid":
|
||||
return "errors.invalidInput"
|
||||
case "not_found":
|
||||
return "errors.notFound"
|
||||
case "already_exists":
|
||||
return "errors.alreadyExists"
|
||||
case "permission_denied":
|
||||
return "errors.permissionDenied"
|
||||
case "dependency_missing":
|
||||
return "errors.dependencyMissing"
|
||||
case "network_error":
|
||||
return "errors.networkUnavailable"
|
||||
case "authentication_failed":
|
||||
return "errors.authenticationFailed"
|
||||
case "database_error":
|
||||
return "errors.databaseError"
|
||||
case "io_error":
|
||||
return "errors.ioError"
|
||||
case "external_command_failed":
|
||||
return "errors.externalCommandFailed"
|
||||
case "window_operation_failed":
|
||||
return "errors.windowOperationFailed"
|
||||
case "task_execution_failed":
|
||||
return "errors.unknown"
|
||||
default:
|
||||
return "errors.unknown"
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveWelcomeError(error: unknown): WelcomeErrorResult {
|
||||
const appError = extractAppCommandError(error)
|
||||
if (appError) {
|
||||
const key = mapCommonCodeToKey(appError.code)
|
||||
const detail =
|
||||
key === "errors.unknown" ? appError.detail || appError.message : undefined
|
||||
|
||||
return detail ? { key, detail } : { key }
|
||||
}
|
||||
|
||||
return {
|
||||
key: "errors.unknown",
|
||||
detail: normalizeErrorMessage(error),
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveCloneError(error: unknown): WelcomeErrorResult {
|
||||
const appError = extractAppCommandError(error)
|
||||
if (appError) {
|
||||
switch (appError.code) {
|
||||
case "dependency_missing":
|
||||
return { key: "errors.gitNotInstalled" }
|
||||
case "already_exists":
|
||||
return { key: "errors.targetDirectoryNotEmpty" }
|
||||
case "not_found":
|
||||
return { key: "errors.repositoryNotFound" }
|
||||
case "network_error":
|
||||
return { key: "errors.networkUnavailable" }
|
||||
case "authentication_failed":
|
||||
return { key: "errors.authenticationFailed" }
|
||||
case "permission_denied":
|
||||
return { key: "errors.permissionDenied" }
|
||||
default: {
|
||||
const key = mapCommonCodeToKey(appError.code)
|
||||
const detail =
|
||||
key === "errors.unknown"
|
||||
? appError.detail || appError.message
|
||||
: undefined
|
||||
return detail ? { key, detail } : { key }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const rawMessage = normalizeErrorMessage(error)
|
||||
const message = stripClonePrefix(rawMessage)
|
||||
const normalized = message.toLowerCase()
|
||||
|
||||
if (normalized.includes("git is not installed")) {
|
||||
return { key: "errors.gitNotInstalled" }
|
||||
}
|
||||
|
||||
if (normalized.includes("already exists and is not an empty directory")) {
|
||||
return { key: "errors.targetDirectoryNotEmpty" }
|
||||
}
|
||||
|
||||
if (normalized.includes("repository not found")) {
|
||||
return { key: "errors.repositoryNotFound" }
|
||||
}
|
||||
|
||||
if (
|
||||
normalized.includes("could not resolve host") ||
|
||||
normalized.includes("network is unreachable") ||
|
||||
normalized.includes("connection timed out") ||
|
||||
normalized.includes("failed to connect")
|
||||
) {
|
||||
return { key: "errors.networkUnavailable" }
|
||||
}
|
||||
|
||||
if (
|
||||
normalized.includes("authentication failed") ||
|
||||
normalized.includes("could not read username") ||
|
||||
normalized.includes("permission denied (publickey)")
|
||||
) {
|
||||
return { key: "errors.authenticationFailed" }
|
||||
}
|
||||
|
||||
if (normalized.includes("permission denied")) {
|
||||
return { key: "errors.permissionDenied" }
|
||||
}
|
||||
|
||||
return {
|
||||
key: "errors.unknown",
|
||||
detail: message || rawMessage || undefined,
|
||||
}
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { FolderOpen, GitBranch, Rocket } from "lucide-react"
|
||||
import { useTranslations } from "next-intl"
|
||||
import { toast } from "sonner"
|
||||
import { useAppWorkspace } from "@/contexts/app-workspace-context"
|
||||
import { openProjectBootWindow } from "@/lib/api"
|
||||
import { isDesktop, openFileDialog } from "@/lib/platform"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { CloneDialog } from "./clone-dialog"
|
||||
import { resolveWelcomeError } from "@/components/workspace-empty/error-utils"
|
||||
import { DirectoryBrowserDialog } from "@/components/shared/directory-browser-dialog"
|
||||
|
||||
export function FolderActions() {
|
||||
const t = useTranslations("WelcomePage")
|
||||
const { openFolder } = useAppWorkspace()
|
||||
const [cloneOpen, setCloneOpen] = useState(false)
|
||||
const [browserOpen, setBrowserOpen] = useState(false)
|
||||
|
||||
const handleOpen = async () => {
|
||||
if (isDesktop()) {
|
||||
const result = await openFileDialog({
|
||||
directory: true,
|
||||
multiple: false,
|
||||
})
|
||||
if (!result) return
|
||||
const selected = Array.isArray(result) ? result[0] : result
|
||||
try {
|
||||
await openFolder(selected)
|
||||
} catch (err) {
|
||||
console.error("[FolderActions] failed to open folder:", err)
|
||||
const resolvedError = resolveWelcomeError(err)
|
||||
toast.error(t("toasts.openFolderFailed"), {
|
||||
description: resolvedError.detail ?? t(resolvedError.key),
|
||||
})
|
||||
}
|
||||
} else {
|
||||
setBrowserOpen(true)
|
||||
}
|
||||
}
|
||||
|
||||
const handleBrowserSelect = async (path: string) => {
|
||||
try {
|
||||
await openFolder(path)
|
||||
} catch (err) {
|
||||
console.error("[FolderActions] failed to open folder:", err)
|
||||
const resolvedError = resolveWelcomeError(err)
|
||||
toast.error(t("toasts.openFolderFailed"), {
|
||||
description: resolvedError.detail ?? t(resolvedError.key),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="w-full flex flex-col gap-1 px-3">
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="justify-start gap-2 h-9"
|
||||
onClick={handleOpen}
|
||||
type="button"
|
||||
>
|
||||
<FolderOpen className="h-4 w-4" />
|
||||
{t("openFolder")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="justify-start gap-2 h-9"
|
||||
onClick={() => setCloneOpen(true)}
|
||||
type="button"
|
||||
>
|
||||
<GitBranch className="h-4 w-4" />
|
||||
{t("cloneRepository")}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="justify-start gap-2 h-9"
|
||||
onClick={async () => {
|
||||
try {
|
||||
await openProjectBootWindow("welcome")
|
||||
} catch (err) {
|
||||
console.error("[FolderActions] failed to open project boot:", err)
|
||||
toast.error(t("toasts.openProjectBootFailed"))
|
||||
}
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<Rocket className="h-4 w-4" />
|
||||
{t("projectBoot")}
|
||||
</Button>
|
||||
|
||||
<CloneDialog open={cloneOpen} onOpenChange={setCloneOpen} />
|
||||
</div>
|
||||
|
||||
<DirectoryBrowserDialog
|
||||
open={browserOpen}
|
||||
onOpenChange={setBrowserOpen}
|
||||
onSelect={handleBrowserSelect}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,136 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import { useMemo, useState } from "react"
|
||||
import { Search, X, FolderOpen } from "lucide-react"
|
||||
import { formatDistanceToNow } from "date-fns"
|
||||
import { enUS, zhCN, zhTW } from "date-fns/locale"
|
||||
import { useLocale, useTranslations } from "next-intl"
|
||||
import { toast } from "sonner"
|
||||
import { useAppWorkspace } from "@/contexts/app-workspace-context"
|
||||
import { removeFolderFromHistory } from "@/lib/api"
|
||||
import type { FolderHistoryEntry } from "@/lib/types"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { resolveWelcomeError } from "@/components/workspace-empty/error-utils"
|
||||
|
||||
interface FolderListProps {
|
||||
history: FolderHistoryEntry[]
|
||||
loading: boolean
|
||||
onRefresh: () => void
|
||||
}
|
||||
|
||||
export function FolderList({ history, loading, onRefresh }: FolderListProps) {
|
||||
const t = useTranslations("WelcomePage")
|
||||
const locale = useLocale()
|
||||
const { openFolder } = useAppWorkspace()
|
||||
const [search, setSearch] = useState("")
|
||||
const dateFnsLocale =
|
||||
locale === "zh-CN" ? zhCN : locale === "zh-TW" ? zhTW : enUS
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
if (!search) return history
|
||||
const lowerCaseSearch = search.toLowerCase()
|
||||
|
||||
return history.filter(
|
||||
(h) =>
|
||||
h.name.toLowerCase().includes(lowerCaseSearch) ||
|
||||
h.path.toLowerCase().includes(lowerCaseSearch)
|
||||
)
|
||||
}, [history, search])
|
||||
|
||||
const handleOpen = async (path: string) => {
|
||||
try {
|
||||
await openFolder(path)
|
||||
} catch (err) {
|
||||
console.error("Failed to open folder:", err)
|
||||
const resolvedError = resolveWelcomeError(err)
|
||||
toast.error(t("toasts.openFolderFailed"), {
|
||||
description: resolvedError.detail ?? t(resolvedError.key),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleRemove = async (e: React.MouseEvent, path: string) => {
|
||||
e.stopPropagation()
|
||||
try {
|
||||
await removeFolderFromHistory(path)
|
||||
onRefresh()
|
||||
} catch (err) {
|
||||
console.error("Failed to remove folder:", err)
|
||||
const resolvedError = resolveWelcomeError(err)
|
||||
toast.error(t("toasts.removeFromHistoryFailed"), {
|
||||
description: resolvedError.detail ?? t(resolvedError.key),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex-1 flex flex-col min-w-0 p-8">
|
||||
<div className="relative mb-4">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder={t("searchPlaceholder")}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
className="pl-9 h-9"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center h-32 text-sm text-muted-foreground">
|
||||
{t("loading")}
|
||||
</div>
|
||||
) : filtered.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center h-32 text-sm text-muted-foreground gap-2">
|
||||
<FolderOpen className="h-8 w-8" />
|
||||
<span>{t("emptyFolders")}</span>
|
||||
</div>
|
||||
) : (
|
||||
filtered.map((entry) => (
|
||||
<div
|
||||
key={entry.id}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className="w-full text-left p-3 rounded-lg hover:bg-accent/50 transition-colors group flex items-start gap-3 cursor-pointer"
|
||||
onClick={() => handleOpen(entry.path)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.preventDefault()
|
||||
handleOpen(entry.path)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium truncate">
|
||||
{entry.name}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground truncate mt-0.5">
|
||||
{entry.path}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 mt-0.5">
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{formatDistanceToNow(new Date(entry.last_opened_at), {
|
||||
addSuffix: true,
|
||||
locale: dateFnsLocale,
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
className="opacity-0 group-hover:opacity-100 transition-opacity p-1 hover:bg-destructive/10 rounded shrink-0 mt-0.5"
|
||||
onClick={(e) => handleRemove(e, entry.path)}
|
||||
title={t("removeFromHistory")}
|
||||
aria-label={t("removeFromHistory")}
|
||||
type="button"
|
||||
>
|
||||
<X className="h-3.5 w-3.5 text-muted-foreground hover:text-destructive" />
|
||||
</button>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import { useEffect, useState } from "react"
|
||||
import { AppIcon } from "@/components/app-icon"
|
||||
import { useTranslations } from "next-intl"
|
||||
import { getCurrentAppVersion } from "@/lib/updater"
|
||||
|
||||
export function SoftwareInfo() {
|
||||
const t = useTranslations("WelcomePage")
|
||||
const [version, setVersion] = useState<string>("")
|
||||
|
||||
useEffect(() => {
|
||||
getCurrentAppVersion()
|
||||
.then(setVersion)
|
||||
.catch((err) => {
|
||||
console.error("[Welcome] get app version failed:", err)
|
||||
})
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="w-full flex items-center gap-4 px-6 py-8">
|
||||
<AppIcon className="size-12" />
|
||||
<div className="flex flex-col">
|
||||
<span className="text-base">Codeg</span>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{version
|
||||
? t("softwareVersion", { version })
|
||||
: t("softwareVersion", { version: "..." })}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import { useEffect, useState, useCallback } from "react"
|
||||
import { loadFolderHistory } from "@/lib/api"
|
||||
import type { FolderHistoryEntry } from "@/lib/types"
|
||||
import { FolderActions } from "./folder-actions"
|
||||
import { FolderList } from "./folder-list"
|
||||
import { SoftwareInfo } from "./software-info"
|
||||
|
||||
export function WorkspaceEmpty() {
|
||||
const [history, setHistory] = useState<FolderHistoryEntry[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const result = await loadFolderHistory()
|
||||
setHistory(result)
|
||||
} catch (err) {
|
||||
console.error("[WorkspaceEmpty] loadFolderHistory failed:", err)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
void refresh()
|
||||
}, [refresh])
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full overflow-hidden">
|
||||
<aside className="flex w-72 shrink-0 flex-col border-r">
|
||||
<SoftwareInfo />
|
||||
<FolderActions />
|
||||
</aside>
|
||||
<main className="flex-1 min-w-0">
|
||||
<FolderList history={history} loading={loading} onRefresh={refresh} />
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user