merge: resolve conflict in SessionManagerPage with main

Accept main's removal of local terminalTarget state, which was
replaced by global settings in refactor(terminal) commit.
This commit is contained in:
YoVinchen
2026-02-06 16:27:49 +08:00
25 changed files with 253 additions and 160 deletions
+10 -8
View File
@@ -20,7 +20,7 @@ import type { Provider } from "@/types";
import type { AppId } from "@/lib/api";
import { providersApi } from "@/lib/api/providers";
import { useDragSort } from "@/hooks/useDragSort";
import { useStreamCheck } from "@/hooks/useStreamCheck";
// import { useStreamCheck } from "@/hooks/useStreamCheck"; // 测试功能已隐藏
import { ProviderCard } from "@/components/providers/ProviderCard";
import { ProviderEmptyState } from "@/components/providers/ProviderEmptyState";
import {
@@ -93,8 +93,8 @@ export function ProviderList({
[appId, opencodeLiveIds],
);
// 流式健康检查
const { checkProvider, isChecking } = useStreamCheck(appId);
// 流式健康检查 - 功能已隐藏
// const { checkProvider, isChecking } = useStreamCheck(appId);
// 故障转移相关
const { data: isAutoFailoverEnabled } = useAutoFailoverEnabled(appId);
@@ -139,9 +139,10 @@ export function ProviderList({
[appId, addToQueue, removeFromQueue],
);
const handleTest = (provider: Provider) => {
checkProvider(provider.id, provider.name);
};
// handleTest 功能已隐藏 - 供应商请求格式复杂难以统一测试
// const handleTest = (provider: Provider) => {
// checkProvider(provider.id, provider.name);
// };
const [searchTerm, setSearchTerm] = useState("");
const [isSearchOpen, setIsSearchOpen] = useState(false);
@@ -229,8 +230,9 @@ export function ProviderList({
onConfigureUsage={onConfigureUsage}
onOpenWebsite={onOpenWebsite}
onOpenTerminal={onOpenTerminal}
onTest={appId !== "opencode" ? handleTest : undefined}
isTesting={isChecking(provider.id)}
// onTest 功能已隐藏 - 供应商请求格式复杂难以统一测试
// onTest={appId !== "opencode" ? handleTest : undefined}
isTesting={false} // isChecking(provider.id) - 测试功能已隐藏
isProxyRunning={isProxyRunning}
isProxyTakeover={isProxyTakeover}
// 故障转移相关:联动状态
@@ -746,7 +746,7 @@ export function ProviderForm({
return;
}
// OpenCode: validate provider key
// OpenCode: validate provider key and models
if (appId === "opencode") {
const keyPattern = /^[a-z0-9]+(-[a-z0-9]+)*$/;
if (!opencodeProviderKey.trim()) {
@@ -761,6 +761,11 @@ export function ProviderForm({
toast.error(t("opencode.providerKeyDuplicate"));
return;
}
// Validate that at least one model is configured
if (Object.keys(opencodeModels).length === 0) {
toast.error(t("opencode.modelsRequired"));
return;
}
}
// 非官方供应商必填校验:端点和 API Key
+1 -1
View File
@@ -70,7 +70,7 @@ export function SessionItem({
<div className="flex items-center gap-1 text-[11px] text-muted-foreground">
<Clock className="size-3" />
<span>
{lastActive ? formatRelativeTime(lastActive) : t("common.unknown")}
{lastActive ? formatRelativeTime(lastActive, t) : t("common.unknown")}
</span>
</div>
</button>
+30 -121
View File
@@ -10,9 +10,7 @@ import {
MessageSquare,
Clock,
FolderOpen,
Terminal,
X,
SquareTerminal,
} from "lucide-react";
import { useSessionMessagesQuery, useSessionsQuery } from "@/lib/query";
import { sessionsApi } from "@/lib/api";
@@ -24,7 +22,6 @@ import {
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
import { ScrollArea } from "@/components/ui/scroll-area";
@@ -37,13 +34,6 @@ import {
import { extractErrorMessage } from "@/utils/errorUtils";
import { isMac } from "@/lib/platform";
import { ProviderIcon } from "@/components/ProviderIcon";
import {
AlacrittyIcon,
GhosttyIcon,
ITermIcon,
KittyIcon,
WezTermIcon,
} from "@/components/icons/TerminalIcons";
import { SessionItem } from "./SessionItem";
import { SessionMessageItem } from "./SessionMessageItem";
import { SessionTocDialog, SessionTocSidebar } from "./SessionToc";
@@ -56,16 +46,6 @@ import {
getSessionKey,
} from "./utils";
const TERMINAL_TARGET_KEY = "session_manager_terminal_target";
type TerminalTarget =
| "terminal"
| "iterm"
| "ghostty"
| "kitty"
| "wezterm"
| "alacritty";
type ProviderFilter = "all" | "codex" | "claude";
export function SessionManagerPage() {
@@ -85,26 +65,6 @@ export function SessionManagerPage() {
const [search, setSearch] = useState("");
const [providerFilter, setProviderFilter] = useState<ProviderFilter>("all");
const [selectedKey, setSelectedKey] = useState<string | null>(null);
const [terminalTarget, setTerminalTarget] =
useState<TerminalTarget>("terminal");
const [isLoaded, setIsLoaded] = useState(false);
useEffect(() => {
const storedTarget = window.localStorage.getItem(
TERMINAL_TARGET_KEY,
) as TerminalTarget | null;
if (storedTarget) {
setTerminalTarget(storedTarget);
}
setIsLoaded(true);
}, []);
useEffect(() => {
if (isLoaded) {
window.localStorage.setItem(TERMINAL_TARGET_KEY, terminalTarget);
}
}, [terminalTarget, isLoaded]);
// 使用 FlexSearch 全文搜索
const { search: searchSessions } = useSessionSearch({
@@ -205,7 +165,6 @@ export function SessionManagerPage() {
try {
await sessionsApi.launchTerminal({
target: terminalTarget,
command: selectedSession.resumeCommand,
cwd: selectedSession.projectDir ?? undefined,
});
@@ -288,7 +247,9 @@ export function SessionManagerPage() {
<Search className="size-3.5" />
</Button>
</TooltipTrigger>
<TooltipContent></TooltipContent>
<TooltipContent>
{t("sessionManager.searchSessions")}
</TooltipContent>
</Tooltip>
<Select
@@ -482,7 +443,7 @@ export function SessionManagerPage() {
{selectedSession.projectDir}
</p>
<p className="text-muted-foreground mt-1">
{t("sessionManager.clickToCopyPath")}
</p>
</TooltipContent>
</Tooltip>
@@ -493,84 +454,32 @@ export function SessionManagerPage() {
{/* 右侧:操作按钮组 */}
<div className="flex items-center gap-2 shrink-0">
{isMac() && (
<>
<Select
value={terminalTarget}
onValueChange={(value) =>
setTerminalTarget(value as TerminalTarget)
}
>
<SelectTrigger className="h-8 min-w-[110px] w-auto text-xs px-2.5">
<Terminal className="size-3 mr-1.5" />
<SelectValue />
</SelectTrigger>
<SelectContent align="end">
<SelectItem value="terminal">
<div className="flex items-center gap-2">
<SquareTerminal className="size-3.5" />
<span>Terminal</span>
</div>
</SelectItem>
<SelectItem value="iterm">
<div className="flex items-center gap-2">
<ITermIcon className="size-3.5" />
<span>iTerm2 (Untested)</span>
</div>
</SelectItem>
<SelectItem value="ghostty">
<div className="flex items-center gap-2">
<GhosttyIcon className="size-3.5" />
<span>Ghostty</span>
</div>
</SelectItem>
<SelectItem value="kitty">
<div className="flex items-center gap-2">
<KittyIcon className="size-3.5" />
<span>Kitty</span>
</div>
</SelectItem>
<SelectItem value="wezterm">
<div className="flex items-center gap-2">
<WezTermIcon className="size-3.5" />
<span>WezTerm (Untested)</span>
</div>
</SelectItem>
<SelectItem value="alacritty">
<div className="flex items-center gap-2">
<AlacrittyIcon className="size-3.5" />
<span>Alacritty (Untested)</span>
</div>
</SelectItem>
</SelectContent>
</Select>
<Tooltip>
<TooltipTrigger asChild>
<Button
size="sm"
className="gap-1.5"
onClick={() => void handleResume()}
disabled={!selectedSession.resumeCommand}
>
<Play className="size-3.5" />
<span className="hidden sm:inline">
{t("sessionManager.resume", {
defaultValue: "恢复会话",
})}
</span>
</Button>
</TooltipTrigger>
<TooltipContent>
{selectedSession.resumeCommand
? t("sessionManager.resumeTooltip", {
defaultValue: "在终端中恢复此会话",
})
: t("sessionManager.noResumeCommand", {
defaultValue: "此会话无法恢复",
})}
</TooltipContent>
</Tooltip>
</>
<Tooltip>
<TooltipTrigger asChild>
<Button
size="sm"
className="gap-1.5"
onClick={() => void handleResume()}
disabled={!selectedSession.resumeCommand}
>
<Play className="size-3.5" />
<span className="hidden sm:inline">
{t("sessionManager.resume", {
defaultValue: "恢复会话",
})}
</span>
</Button>
</TooltipTrigger>
<TooltipContent>
{selectedSession.resumeCommand
? t("sessionManager.resumeTooltip", {
defaultValue: "在终端中恢复此会话",
})
: t("sessionManager.noResumeCommand", {
defaultValue: "此会话无法恢复",
})}
</TooltipContent>
</Tooltip>
)}
</div>
</div>
@@ -59,7 +59,7 @@ export function SessionMessageItem({
</Tooltip>
<div className="flex items-center justify-between text-xs mb-1.5 pr-6">
<span className={cn("font-semibold", getRoleTone(message.role))}>
{getRoleLabel(message.role)}
{getRoleLabel(message.role, t)}
</span>
{message.ts && (
<span className="text-muted-foreground">
+6 -3
View File
@@ -1,4 +1,5 @@
import { List, X } from "lucide-react";
import { useTranslation } from "react-i18next";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { ScrollArea } from "@/components/ui/scroll-area";
@@ -26,6 +27,7 @@ export function SessionTocSidebar({
items,
onItemClick,
}: SessionTocSidebarProps) {
const { t } = useTranslation();
if (items.length <= 2) return null;
return (
@@ -33,7 +35,7 @@ export function SessionTocSidebar({
<div className="p-3 border-b">
<div className="flex items-center gap-1.5 text-xs font-medium text-muted-foreground">
<List className="size-3.5" />
<span></span>
<span>{t("sessionManager.tocTitle")}</span>
</div>
</div>
<ScrollArea className="h-[calc(100%-40px)]">
@@ -74,6 +76,7 @@ export function SessionTocDialog({
open,
onOpenChange,
}: SessionTocDialogProps) {
const { t } = useTranslation();
if (items.length <= 2) return null;
return (
@@ -95,11 +98,11 @@ export function SessionTocDialog({
<DialogHeader className="px-4 py-3 relative border-b">
<DialogTitle className="flex items-center gap-2 text-base font-semibold">
<List className="size-4 text-primary" />
{t("sessionManager.tocTitle")}
</DialogTitle>
<DialogClose
className="absolute right-3 top-1/2 -translate-y-1/2 rounded-full p-1.5 hover:bg-muted transition-colors focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2"
aria-label="关闭"
aria-label={t("common.close")}
>
<X className="size-4 text-muted-foreground" />
</DialogClose>
+12 -9
View File
@@ -17,7 +17,10 @@ export const formatTimestamp = (value?: number) => {
return new Date(value).toLocaleString();
};
export const formatRelativeTime = (value?: number) => {
export const formatRelativeTime = (
value: number | undefined,
t: (key: string, options?: Record<string, unknown>) => string,
) => {
if (!value) return "";
const now = Date.now();
const diff = now - value;
@@ -25,10 +28,10 @@ export const formatRelativeTime = (value?: number) => {
const hours = Math.floor(diff / 3600000);
const days = Math.floor(diff / 86400000);
if (minutes < 1) return "刚刚";
if (minutes < 60) return `${minutes} 分钟前`;
if (hours < 24) return `${hours} 小时前`;
if (days < 7) return `${days} 天前`;
if (minutes < 1) return t("sessionManager.justNow");
if (minutes < 60) return t("sessionManager.minutesAgo", { count: minutes });
if (hours < 24) return t("sessionManager.hoursAgo", { count: hours });
if (days < 7) return t("sessionManager.daysAgo", { count: days });
return new Date(value).toLocaleDateString();
};
@@ -57,12 +60,12 @@ export const getRoleTone = (role: string) => {
return "text-muted-foreground";
};
export const getRoleLabel = (role: string) => {
export const getRoleLabel = (role: string, t: (key: string) => string) => {
const normalized = role.toLowerCase();
if (normalized === "assistant") return "AI";
if (normalized === "user") return "用户";
if (normalized === "system") return "系统";
if (normalized === "tool") return "工具";
if (normalized === "user") return t("sessionManager.roleUser");
if (normalized === "system") return t("sessionManager.roleSystem");
if (normalized === "tool") return t("sessionManager.roleTool");
return role;
};
@@ -15,6 +15,7 @@ const MACOS_TERMINALS = [
{ value: "alacritty", labelKey: "settings.terminal.options.macos.alacritty" },
{ value: "kitty", labelKey: "settings.terminal.options.macos.kitty" },
{ value: "ghostty", labelKey: "settings.terminal.options.macos.ghostty" },
{ value: "wezterm", labelKey: "settings.terminal.options.macos.wezterm" },
] as const;
const WINDOWS_TERMINALS = [
+49
View File
@@ -5,6 +5,7 @@ import React, {
useMemo,
useState,
} from "react";
import { invoke } from "@tauri-apps/api/core";
type Theme = "light" | "dark" | "system";
@@ -94,6 +95,54 @@ export function ThemeProvider({
return () => mediaQuery.removeEventListener("change", handleChange);
}, [theme]);
// Sync native window theme (Windows/macOS title bar)
useEffect(() => {
if (typeof window === "undefined") {
return;
}
let isCancelled = false;
const updateNativeTheme = async (nativeTheme: string) => {
if (isCancelled) return;
try {
await invoke("set_window_theme", { theme: nativeTheme });
} catch (e) {
// Ignore errors (e.g., when not running in Tauri)
console.debug("Failed to set native window theme:", e);
}
};
// Determine current effective theme
if (theme === "system") {
const isDark =
window.matchMedia &&
window.matchMedia("(prefers-color-scheme: dark)").matches;
updateNativeTheme(isDark ? "dark" : "light");
} else {
updateNativeTheme(theme);
}
// Listen to system theme changes for native window when in "system" mode
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
const handleChange = () => {
if (theme === "system" && !isCancelled) {
updateNativeTheme(mediaQuery.matches ? "dark" : "light");
}
};
if (theme === "system") {
mediaQuery.addEventListener("change", handleChange);
}
return () => {
isCancelled = true;
if (theme === "system") {
mediaQuery.removeEventListener("change", handleChange);
}
};
}, [theme]);
const value = useMemo<ThemeContextValue>(
() => ({
theme,