chore: init main branch

This commit is contained in:
954801926@qq.com committed 2026-07-29 23:36:41 +08:00
commit 549c8b5b13
231 files changed
+27286

No files matched your search

+89
View File
@@ -0,0 +1,89 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { invoke } from "@tauri-apps/api/core";
import { useTranslation } from "../i18n";
import type { Agent, Config } from "../types";
interface UseConfigReturn {
config: Config | null;
dirty: boolean;
error: string | null;
loading: boolean;
refresh: () => Promise<void>;
save: () => Promise<void>;
updateConfig: (updater: (config: Config) => Config) => void;
}
export function useConfig(agent: Agent): UseConfigReturn {
const { t } = useTranslation();
const [config, setConfig] = useState<Config | null>(null);
const configRef = useRef(config);
const [dirty, setDirty] = useState(false);
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
useEffect(() => {
configRef.current = config;
}, [config]);
const refresh = useCallback(async () => {
setLoading(true);
setError(null);
try {
const loadedConfig = await invoke<Config>("load_agent_config_command", {
agent,
});
setConfig(loadedConfig);
setDirty(false);
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
setError(message);
console.error("useConfig refresh failed:", message);
} finally {
setLoading(false);
}
}, [agent]);
useEffect(() => {
refresh();
}, [refresh]);
const updateConfig = useCallback((updater: (config: Config) => Config) => {
// Compute next state synchronously from the ref (always current) and set
// the ref BEFORE setConfig. Previously the ref was updated inside the
// setConfig updater, which React 18 defers — so save() reading the ref
// immediately after would get a stale value.
const prev = configRef.current;
if (prev) {
const next = updater(prev);
configRef.current = next;
setConfig(next);
}
setDirty(true);
}, []);
const save = useCallback(async () => {
const current = configRef.current;
if (!current) return;
setLoading(true);
setError(null);
try {
await invoke("save_agent_config_command", { agent, config: current });
await refresh();
} catch (err) {
const raw = err instanceof Error ? err.message : String(err);
setError(raw);
} finally {
setLoading(false);
}
}, [refresh, t, agent]);
return {
config,
dirty,
error,
loading,
refresh,
save,
updateConfig,
};
}
+54
View File
@@ -0,0 +1,54 @@
import { useCallback, useEffect, useState } from "react";
import { invoke } from "@tauri-apps/api/core";
import type { SummaryResult } from "../types/dashboard";
export type DashboardRange = "today" | "7d" | "30d" | "all";
const RANGE_STORAGE_KEY = "kimi-switch-dashboard-range";
export function useDashboard() {
const [range, setRange] = useState<DashboardRange>(() => {
try {
const stored = localStorage.getItem(RANGE_STORAGE_KEY) as DashboardRange | null;
if (stored === "today" || stored === "7d" || stored === "30d" || stored === "all") {
return stored;
}
} catch {
// ignore
}
return "30d";
});
const [data, setData] = useState<SummaryResult | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const refresh = useCallback(async () => {
setLoading(true);
setError(null);
try {
const result = await invoke<SummaryResult>("get_summary", { range });
setData(result);
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
setError(msg);
} finally {
setLoading(false);
}
}, [range]);
useEffect(() => {
refresh();
}, [refresh]);
const changeRange = useCallback((next: DashboardRange) => {
setRange(next);
try {
localStorage.setItem(RANGE_STORAGE_KEY, next);
} catch {
// ignore
}
}, []);
return { range, changeRange, data, loading, error, refresh };
}
+107
View File
@@ -0,0 +1,107 @@
import { useCallback, useEffect, useState } from "react";
import { invoke } from "@tauri-apps/api/core";
import type {
ActionResponse,
PreviewResult,
SessionStatusFilter,
SessionsResult,
} from "../types/sessions";
export function useSessions() {
const [status, setStatus] = useState<SessionStatusFilter>("active");
const [workspace, setWorkspace] = useState<string>("all");
const [data, setData] = useState<SessionsResult | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const refresh = useCallback(async () => {
setLoading(true);
setError(null);
try {
const result = await invoke<SessionsResult>("list_sessions", {
status,
workspace: workspace === "all" ? null : workspace,
});
setData(result);
} catch (err) {
setData(null);
setError(err instanceof Error ? err.message : String(err));
} finally {
setLoading(false);
}
}, [status, workspace]);
useEffect(() => {
refresh();
}, [refresh]);
const archiveSession = useCallback(
async (workspaceId: string, sessionId: string) => {
await invoke<ActionResponse>("archive_session", { workspaceId, sessionId });
await refresh();
},
[refresh]
);
const unarchiveSession = useCallback(
async (workspaceId: string, sessionId: string) => {
await invoke<ActionResponse>("unarchive_session", {
workspaceId,
sessionId,
});
await refresh();
},
[refresh]
);
const deleteSession = useCallback(
async (workspaceId: string, sessionId: string, sessionStatus?: string) => {
await invoke<ActionResponse>("delete_session", {
workspaceId,
sessionId,
status: sessionStatus ?? null,
});
await refresh();
},
[refresh]
);
const deleteWorkspace = useCallback(
async (workspaceId: string) => {
await invoke<ActionResponse>("delete_workspace", {
workspaceId,
confirm: true,
});
await refresh();
},
[refresh]
);
const getPreview = useCallback(
async (workspaceId: string, sessionId: string, sessionStatus?: string) => {
return invoke<PreviewResult>("get_session_preview", {
workspaceId,
sessionId,
status: sessionStatus ?? null,
});
},
[]
);
return {
status,
setStatus,
workspace,
setWorkspace,
data,
loading,
error,
setError,
refresh,
archiveSession,
unarchiveSession,
deleteSession,
deleteWorkspace,
getPreview,
};
}
+62
View File
@@ -0,0 +1,62 @@
import { useEffect, useState } from "react";
export type Theme = "light" | "dark" | "auto";
export type ResolvedTheme = "light" | "dark";
const STORAGE_KEY = "kimi-switch-theme";
function getSystemTheme(): ResolvedTheme {
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
}
function getInitialTheme(): Theme {
try {
const stored = localStorage.getItem(STORAGE_KEY) as Theme | null;
if (stored === "light" || stored === "dark" || stored === "auto") return stored;
} catch {
// ignore
}
return "auto";
}
function applyTheme(theme: Theme): ResolvedTheme {
const resolved: ResolvedTheme = theme === "auto" ? getSystemTheme() : theme;
const root = document.documentElement;
root.classList.toggle("dark", resolved === "dark");
root.classList.toggle("light", resolved === "light");
return resolved;
}
export function useTheme() {
const [theme, setThemeState] = useState<Theme>(getInitialTheme);
const [resolvedTheme, setResolvedTheme] = useState<ResolvedTheme>(() => applyTheme(getInitialTheme()));
// Apply theme on change
useEffect(() => {
const resolved = applyTheme(theme);
setResolvedTheme(resolved);
}, [theme]);
// Listen to system theme changes when in "auto" mode
useEffect(() => {
if (theme !== "auto") return;
const media = window.matchMedia("(prefers-color-scheme: dark)");
const handler = () => {
const resolved = applyTheme("auto");
setResolvedTheme(resolved);
};
media.addEventListener("change", handler);
return () => media.removeEventListener("change", handler);
}, [theme]);
const setTheme = (next: Theme) => {
setThemeState(next);
try {
localStorage.setItem(STORAGE_KEY, next);
} catch {
// ignore
}
};
return { theme, resolvedTheme, setTheme };
}
+71
View File
@@ -0,0 +1,71 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { invoke } from "@tauri-apps/api/core";
export interface UpdateInfo {
current: string;
latest: string;
updateAvailable: boolean;
releaseUrl: string;
downloadUrl: string | null;
}
const THROTTLE_KEY = "kimi-switch-last-update-check";
// Both the throttle window and the periodic re-check run on an 8h cadence:
// long-running sessions still pick up new releases without a restart, and the
// actual backend call is still gated so short windows don't double-fire.
const CHECK_INTERVAL_MS = 8 * 60 * 60 * 1000; // 8 hours
export function useUpdateCheck() {
const [updateInfo, setUpdateInfo] = useState<UpdateInfo | null>(null);
const [checking, setChecking] = useState(false);
const [lastChecked, setLastChecked] = useState<number | null>(() => {
try {
const v = localStorage.getItem(THROTTLE_KEY);
return v ? Number(v) : null;
} catch {
return null;
}
});
const doCheck = useCallback(async (force: boolean) => {
if (!force) {
if (lastChecked && Date.now() - lastChecked < CHECK_INTERVAL_MS) return;
}
setChecking(true);
try {
const info = await invoke<UpdateInfo>("check_for_update");
setUpdateInfo(info);
const now = Date.now();
setLastChecked(now);
try {
localStorage.setItem(THROTTLE_KEY, String(now));
} catch {
// ignore
}
} catch {
// silently fail — network errors, parse errors, etc. should not nag
// the user. They can still trigger a manual check from Settings.
} finally {
setChecking(false);
}
}, [lastChecked]);
// Keep a ref to the latest doCheck so the periodic interval doesn't need
// to be reset on every lastChecked change.
const doCheckRef = useRef(doCheck);
useEffect(() => {
doCheckRef.current = doCheck;
}, [doCheck]);
// On mount: throttled auto-check + periodic re-check while the window
// stays open. Errors are silent (handled inside doCheck).
useEffect(() => {
doCheckRef.current(false);
const interval = setInterval(() => doCheckRef.current(false), CHECK_INTERVAL_MS);
return () => clearInterval(interval);
}, []);
const checkNow = useCallback(() => doCheck(true), [doCheck]);
return { updateInfo, checking, checkNow, lastChecked };
}