chore: init main branch
This commit is contained in:
commit
549c8b5b13
231 files changed
+27286
No files matched your search
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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 };
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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 };
|
||||
}
|
||||
@@ -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 };
|
||||
}
|
||||
Reference in new issue
Block a user