diff --git a/src/app/os/keys/page.tsx b/src/app/os/keys/page.tsx
index 086f3e1..aced8ad 100644
--- a/src/app/os/keys/page.tsx
+++ b/src/app/os/keys/page.tsx
@@ -1,44 +1,386 @@
"use client";
-import { useState, useEffect, useMemo } from "react";
-import { useSearchParams } from "next/navigation";
+import {
+ ChevronDown,
+ ChevronRight,
+ CircleMinus,
+ CirclePlus,
+ GitCompare,
+ History,
+ Minus,
+ Plus,
+ Search,
+ X,
+} from "lucide-react";
import Link from "next/link";
-import { Search, X } from "lucide-react";
+import { useRouter, useSearchParams } from "next/navigation";
+import { useEffect, useMemo, useState } from "react";
-import { Input } from "@/components/ui/input";
-import { Button } from "@/components/ui/button";
import { HeaderPortal } from "@/components/header-portal";
-import { createEngine } from "@/lib/engine";
-import { tokenizeKeys, getTopTokens } from "@/lib/tokenizer";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import {
+ Popover,
+ PopoverContent,
+ PopoverTrigger,
+} from "@/components/ui/popover";
import { useQueryFilter } from "@/hooks/use-query-filter";
+import { createEngine } from "@/lib/engine";
+import { getTopTokens, tokenizeKeys } from "@/lib/tokenizer";
+import type { OS } from "@/lib/types";
+import { cn } from "@/lib/utils";
+
+function versionTag(os: OS) {
+ return `${os.version}_${os.build}`;
+}
+
+function isCurrentVersion(os: OS, build: string) {
+ return os.build === build || versionTag(os) === build;
+}
+
+function matchesVersion(os: OS, tag: string) {
+ return os.build === tag || versionTag(os) === tag;
+}
+
+function compareOSVersion(a: OS, b: OS) {
+ const vA = a.version.split(".").map(Number);
+ const vB = b.version.split(".").map(Number);
+ for (let i = 0; i < Math.max(vA.length, vB.length); i++) {
+ const diff = (vB[i] || 0) - (vA[i] || 0);
+ if (diff !== 0) return diff;
+ }
+
+ return b.build.localeCompare(a.build);
+}
+
+const keySkeletons = Array.from(
+ { length: 30 },
+ (_, index) => `key-skeleton-${index}`,
+);
+
+type DisplayedKey = {
+ key: string;
+ os: string;
+ status: "current" | "added" | "removed";
+};
+
+type VersionHistoryPanelProps = {
+ activeCompareTag: string | null;
+ build: string;
+ className?: string;
+ compareWith: string | null;
+ group: string;
+ groupedVersions: Array<[string, OS[]]>;
+ listClassName?: string;
+ setCompareWith: (value: string | null) => void;
+ switchVersion: (version: OS) => void;
+ versionsCount: number;
+};
+
+function KeyLink({ entry }: { entry: DisplayedKey }) {
+ const isNew = entry.status === "added";
+ const isRemoved = entry.status === "removed";
+
+ return (
+
+ {(isNew || isRemoved) && (
+
+ {isRemoved ? (
+
+ ) : (
+
+ )}
+
+ )}
+ {entry.key}
+
+ );
+}
+
+function VersionHistoryPanel({
+ activeCompareTag,
+ build,
+ className,
+ compareWith,
+ group,
+ groupedVersions,
+ listClassName,
+ setCompareWith,
+ switchVersion,
+ versionsCount,
+}: VersionHistoryPanelProps) {
+ return (
+
+
+
+ Version History
+
+ {versionsCount}
+
+
+
+ {groupedVersions.map(([major, groupVersions]) => (
+
+ isCurrentVersion(version, build) ||
+ (activeCompareTag && matchesVersion(version, activeCompareTag)),
+ )}
+ className="group"
+ >
+
+
+
+ {group} {major}.x
+
+
+ {groupVersions.length}
+
+
+
+
+ {groupVersions.map((version) => {
+ const tag = versionTag(version);
+ const isCurrent = isCurrentVersion(version, build);
+ const isComparing = activeCompareTag
+ ? matchesVersion(version, activeCompareTag)
+ : false;
+
+ return (
+
+
+
+ {!isCurrent && (
+
+ )}
+
+ );
+ })}
+
+
+ ))}
+
+
+ );
+}
export default function Keys() {
const params = useSearchParams();
+ const router = useRouter();
const os = params.get("os") as string;
const [group, build] = os ? os.split("/") : ["", ""];
const [loading, setLoading] = useState(true);
const [keys, setKeys] = useState([]);
+ const [versions, setVersions] = useState([]);
+ const [compareKeys, setCompareKeys] = useState(null);
+ const [compareLoading, setCompareLoading] = useState(false);
+ const [compareError, setCompareError] = useState(false);
+ const [changesExpanded, setChangesExpanded] = useState(false);
const { keyword, setKeyword, debouncedKeyword } = useQueryFilter();
+ const compareWith = params.get("diff");
+
useEffect(() => {
+ let cancelled = false;
+
async function load() {
+ if (!group || !build) {
+ setKeys([]);
+ setVersions([]);
+ return;
+ }
+
const engine = await createEngine(group);
- const allKeys = await engine.getKeys(build);
+ const [allKeys, osList] = await Promise.all([
+ engine.getKeys(build),
+ engine.listOS().catch(() => [] as OS[]),
+ ]);
allKeys.sort((a, b) => a.localeCompare(b));
- setKeys(allKeys);
+ osList.sort(compareOSVersion);
+
+ if (!cancelled) {
+ setKeys(allKeys);
+ setVersions(osList);
+ }
}
setLoading(true);
- load().finally(() => setLoading(false));
+ load()
+ .catch(() => {
+ if (!cancelled) {
+ setKeys([]);
+ setVersions([]);
+ }
+ })
+ .finally(() => {
+ if (!cancelled) {
+ setLoading(false);
+ }
+ });
+
+ return () => {
+ cancelled = true;
+ };
}, [group, build]);
+ const currentVersionIndex = useMemo(
+ () => versions.findIndex((version) => isCurrentVersion(version, build)),
+ [build, versions],
+ );
+
+ const currentVersion =
+ currentVersionIndex === -1 ? undefined : versions[currentVersionIndex];
+
+ const explicitCompareVersion = compareWith
+ ? versions.find((version) => matchesVersion(version, compareWith))
+ : undefined;
+
+ const compareVersion = explicitCompareVersion;
+
+ const compareTag = compareWith;
+ const currentTag = currentVersion ? versionTag(currentVersion) : build;
+ const activeCompareTag =
+ compareTag &&
+ compareTag !== build &&
+ compareTag !== currentTag &&
+ compareTag !== currentVersion?.build
+ ? compareTag
+ : null;
+
+ useEffect(() => {
+ let cancelled = false;
+
+ if (!group || !activeCompareTag) {
+ setCompareKeys(null);
+ setCompareLoading(false);
+ setCompareError(false);
+ setChangesExpanded(false);
+ return;
+ }
+
+ async function loadCompareKeys() {
+ const engine = await createEngine(group);
+ const baseKeys = await engine.getKeys(activeCompareTag!);
+ baseKeys.sort((a, b) => a.localeCompare(b));
+
+ if (!cancelled) {
+ setCompareKeys(baseKeys);
+ setCompareError(false);
+ }
+ }
+
+ setCompareKeys(null);
+ setCompareLoading(true);
+ setCompareError(false);
+ loadCompareKeys()
+ .catch(() => {
+ if (!cancelled) {
+ setCompareKeys([]);
+ setCompareError(true);
+ setChangesExpanded(false);
+ }
+ })
+ .finally(() => {
+ if (!cancelled) {
+ setCompareLoading(false);
+ }
+ });
+
+ return () => {
+ cancelled = true;
+ };
+ }, [group, activeCompareTag]);
+
const filtered = useMemo(
() =>
keys.filter((key) =>
- key.toLowerCase().includes(debouncedKeyword.toLowerCase())
+ key.toLowerCase().includes(debouncedKeyword.toLowerCase()),
),
- [debouncedKeyword, keys]
+ [debouncedKeyword, keys],
);
const topTokens = useMemo(() => {