mirror of
https://github.com/multica-ai/multica.git
synced 2026-06-19 12:48:56 +02:00
Compare commits
1 Commits
feat/cli-v
...
agent/j/b8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5a72117374 |
@@ -19,6 +19,8 @@ import {
|
||||
} from "lucide-react";
|
||||
import { WorkspaceAvatar } from "@/features/workspace";
|
||||
import { useIssueDraftStore } from "@/features/issues/stores/draft-store";
|
||||
import { runtimeListOptions, latestCliVersionOptions } from "@core/runtimes/queries";
|
||||
import { runtimeNeedsUpdate } from "@/features/runtimes/version";
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
@@ -67,6 +69,14 @@ function DraftDot() {
|
||||
return <span className="absolute top-0 right-0 size-1.5 rounded-full bg-brand" />;
|
||||
}
|
||||
|
||||
function RuntimeUpdateDot({ wsId }: { wsId: string }) {
|
||||
const { data: runtimes = [] } = useQuery(runtimeListOptions(wsId));
|
||||
const { data: latestVersion } = useQuery(latestCliVersionOptions());
|
||||
const hasUpdates = runtimes.some((r) => runtimeNeedsUpdate(r, latestVersion ?? null));
|
||||
if (!hasUpdates) return null;
|
||||
return <span className="ml-auto size-2 rounded-full bg-destructive" />;
|
||||
}
|
||||
|
||||
export function AppSidebar() {
|
||||
const pathname = usePathname();
|
||||
const router = useRouter();
|
||||
@@ -224,6 +234,9 @@ export function AppSidebar() {
|
||||
>
|
||||
<item.icon />
|
||||
<span>{item.label}</span>
|
||||
{item.label === "Runtimes" && wsId && (
|
||||
<RuntimeUpdateDot wsId={wsId} />
|
||||
)}
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { queryOptions } from "@tanstack/react-query";
|
||||
import { api } from "@/shared/api";
|
||||
import { fetchLatestVersion } from "@/features/runtimes/version";
|
||||
|
||||
export const runtimeKeys = {
|
||||
all: (wsId: string) => ["runtimes", wsId] as const,
|
||||
@@ -12,3 +13,11 @@ export function runtimeListOptions(wsId: string) {
|
||||
queryFn: () => api.listRuntimes({ workspace_id: wsId }),
|
||||
});
|
||||
}
|
||||
|
||||
export function latestCliVersionOptions() {
|
||||
return queryOptions({
|
||||
queryKey: ["cli", "latest-version"] as const,
|
||||
queryFn: fetchLatestVersion,
|
||||
staleTime: 10 * 60 * 1000, // 10 minutes
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,21 +1,11 @@
|
||||
import type { AgentRuntime } from "@/shared/types";
|
||||
import { formatLastSeen } from "../utils";
|
||||
import { getCliVersion } from "../version";
|
||||
import { RuntimeModeIcon, StatusBadge, InfoField } from "./shared";
|
||||
import { PingSection } from "./ping-section";
|
||||
import { UpdateSection } from "./update-section";
|
||||
import { UsageSection } from "./usage-section";
|
||||
|
||||
function getCliVersion(metadata: Record<string, unknown>): string | null {
|
||||
if (
|
||||
metadata &&
|
||||
typeof metadata.cli_version === "string" &&
|
||||
metadata.cli_version
|
||||
) {
|
||||
return metadata.cli_version;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function RuntimeDetail({ runtime }: { runtime: AgentRuntime }) {
|
||||
const cliVersion =
|
||||
runtime.runtime_mode === "local" ? getCliVersion(runtime.metadata) : null;
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
import { Server } from "lucide-react";
|
||||
import { ArrowUpCircle, Server } from "lucide-react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import type { AgentRuntime } from "@/shared/types";
|
||||
import { latestCliVersionOptions } from "@core/runtimes/queries";
|
||||
import { runtimeNeedsUpdate } from "../version";
|
||||
import { RuntimeModeIcon } from "./shared";
|
||||
|
||||
function RuntimeListItem({
|
||||
runtime,
|
||||
isSelected,
|
||||
hasUpdate,
|
||||
onClick,
|
||||
}: {
|
||||
runtime: AgentRuntime;
|
||||
isSelected: boolean;
|
||||
hasUpdate: boolean;
|
||||
onClick: () => void;
|
||||
}) {
|
||||
return (
|
||||
@@ -19,16 +24,28 @@ function RuntimeListItem({
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`flex h-8 w-8 shrink-0 items-center justify-center rounded-lg ${
|
||||
className={`relative flex h-8 w-8 shrink-0 items-center justify-center rounded-lg ${
|
||||
runtime.status === "online" ? "bg-success/10" : "bg-muted"
|
||||
}`}
|
||||
>
|
||||
<RuntimeModeIcon mode={runtime.runtime_mode} />
|
||||
{hasUpdate && (
|
||||
<span className="absolute -top-1 -right-1 size-2.5 rounded-full bg-destructive ring-2 ring-background" />
|
||||
)}
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="truncate text-sm font-medium">{runtime.name}</div>
|
||||
<div className="mt-0.5 truncate text-xs text-muted-foreground">
|
||||
{runtime.provider} · {runtime.runtime_mode}
|
||||
{hasUpdate ? (
|
||||
<span className="inline-flex items-center gap-1 text-destructive">
|
||||
<ArrowUpCircle className="h-3 w-3" />
|
||||
Update available
|
||||
</span>
|
||||
) : (
|
||||
<>
|
||||
{runtime.provider} · {runtime.runtime_mode}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
@@ -49,6 +66,8 @@ export function RuntimeList({
|
||||
selectedId: string;
|
||||
onSelect: (id: string) => void;
|
||||
}) {
|
||||
const { data: latestVersion } = useQuery(latestCliVersionOptions());
|
||||
|
||||
return (
|
||||
<div className="overflow-y-auto h-full border-r">
|
||||
<div className="flex h-12 items-center justify-between border-b px-4">
|
||||
@@ -79,6 +98,7 @@ export function RuntimeList({
|
||||
key={runtime.id}
|
||||
runtime={runtime}
|
||||
isSelected={runtime.id === selectedId}
|
||||
hasUpdate={runtimeNeedsUpdate(runtime, latestVersion ?? null)}
|
||||
onClick={() => onSelect(runtime.id)}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -9,47 +9,7 @@ import {
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { api } from "@/shared/api";
|
||||
import type { RuntimeUpdateStatus } from "@/shared/types";
|
||||
|
||||
const GITHUB_RELEASES_URL =
|
||||
"https://api.github.com/repos/multica-ai/multica/releases/latest";
|
||||
const CACHE_TTL_MS = 10 * 60 * 1000; // 10 minutes
|
||||
|
||||
let cachedLatestVersion: string | null = null;
|
||||
let cachedAt = 0;
|
||||
|
||||
async function fetchLatestVersion(): Promise<string | null> {
|
||||
if (cachedLatestVersion && Date.now() - cachedAt < CACHE_TTL_MS) {
|
||||
return cachedLatestVersion;
|
||||
}
|
||||
try {
|
||||
const resp = await fetch(GITHUB_RELEASES_URL, {
|
||||
headers: { Accept: "application/vnd.github+json" },
|
||||
});
|
||||
if (!resp.ok) return null;
|
||||
const data = await resp.json();
|
||||
cachedLatestVersion = data.tag_name ?? null;
|
||||
cachedAt = Date.now();
|
||||
return cachedLatestVersion;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function stripV(v: string): string {
|
||||
return v.replace(/^v/, "");
|
||||
}
|
||||
|
||||
function isNewer(latest: string, current: string): boolean {
|
||||
const l = stripV(latest).split(".").map(Number);
|
||||
const c = stripV(current).split(".").map(Number);
|
||||
for (let i = 0; i < Math.max(l.length, c.length); i++) {
|
||||
const lv = l[i] ?? 0;
|
||||
const cv = c[i] ?? 0;
|
||||
if (lv > cv) return true;
|
||||
if (lv < cv) return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
import { fetchLatestVersion, isNewer } from "../version";
|
||||
|
||||
const statusConfig: Record<
|
||||
RuntimeUpdateStatus,
|
||||
|
||||
67
apps/web/features/runtimes/version.ts
Normal file
67
apps/web/features/runtimes/version.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import type { AgentRuntime } from "@/shared/types";
|
||||
|
||||
const GITHUB_RELEASES_URL =
|
||||
"https://api.github.com/repos/multica-ai/multica/releases/latest";
|
||||
const CACHE_TTL_MS = 10 * 60 * 1000; // 10 minutes
|
||||
|
||||
let cachedLatestVersion: string | null = null;
|
||||
let cachedAt = 0;
|
||||
|
||||
export async function fetchLatestVersion(): Promise<string | null> {
|
||||
if (cachedLatestVersion && Date.now() - cachedAt < CACHE_TTL_MS) {
|
||||
return cachedLatestVersion;
|
||||
}
|
||||
try {
|
||||
const resp = await fetch(GITHUB_RELEASES_URL, {
|
||||
headers: { Accept: "application/vnd.github+json" },
|
||||
});
|
||||
if (!resp.ok) return null;
|
||||
const data = await resp.json();
|
||||
cachedLatestVersion = data.tag_name ?? null;
|
||||
cachedAt = Date.now();
|
||||
return cachedLatestVersion;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function stripV(v: string): string {
|
||||
return v.replace(/^v/, "");
|
||||
}
|
||||
|
||||
export function isNewer(latest: string, current: string): boolean {
|
||||
const l = stripV(latest).split(".").map(Number);
|
||||
const c = stripV(current).split(".").map(Number);
|
||||
for (let i = 0; i < Math.max(l.length, c.length); i++) {
|
||||
const lv = l[i] ?? 0;
|
||||
const cv = c[i] ?? 0;
|
||||
if (lv > cv) return true;
|
||||
if (lv < cv) return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function getCliVersion(
|
||||
metadata: Record<string, unknown>,
|
||||
): string | null {
|
||||
if (
|
||||
metadata &&
|
||||
typeof metadata.cli_version === "string" &&
|
||||
metadata.cli_version
|
||||
) {
|
||||
return metadata.cli_version;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Check if a single runtime has an update available. */
|
||||
export function runtimeNeedsUpdate(
|
||||
runtime: AgentRuntime,
|
||||
latestVersion: string | null,
|
||||
): boolean {
|
||||
if (!latestVersion) return false;
|
||||
if (runtime.runtime_mode !== "local") return false;
|
||||
const current = getCliVersion(runtime.metadata);
|
||||
if (!current) return false;
|
||||
return isNewer(latestVersion, current);
|
||||
}
|
||||
Reference in New Issue
Block a user