mirror of
https://github.com/multica-ai/multica.git
synced 2026-08-12 10:59:06 +02:00
- Create NavigationAdapter interface (push, replace, back, pathname, searchParams) - Create AppLink component replacing next/link in 4 files - Replace useRouter → useNavigation in 3 files (issue-detail, create-issue, create-workspace) - Create WebNavigationProvider wrapping Next.js useRouter/usePathname/useSearchParams - Move ~85 feature UI files (issues, editor, modals, my-issues, skills, runtimes) to packages/views/ - Add store singleton registration pattern (registerAuthStore, registerWorkspaceStore) - Create data-aware wrappers in packages/views/common/ (ActorAvatar, Markdown) - Update all app-layer imports to @multica/views/* - Add @source directive for Tailwind to scan views package - packages/views/ has zero next/* imports Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
121 lines
3.9 KiB
TypeScript
121 lines
3.9 KiB
TypeScript
import { useState, useEffect, useCallback, useRef } from "react";
|
|
import { Loader2, CheckCircle2, XCircle, Zap } from "lucide-react";
|
|
import { Button } from "@multica/ui/components/ui/button";
|
|
import { api } from "@multica/core/api";
|
|
import type { RuntimePingStatus } from "@multica/core/types";
|
|
|
|
const pingStatusConfig: Record<
|
|
RuntimePingStatus,
|
|
{ label: string; icon: typeof Loader2; color: string }
|
|
> = {
|
|
pending: { label: "Waiting for daemon...", icon: Loader2, color: "text-muted-foreground" },
|
|
running: { label: "Running test...", icon: Loader2, color: "text-info" },
|
|
completed: { label: "Connected", icon: CheckCircle2, color: "text-success" },
|
|
failed: { label: "Failed", icon: XCircle, color: "text-destructive" },
|
|
timeout: { label: "Timeout", icon: XCircle, color: "text-warning" },
|
|
};
|
|
|
|
export function PingSection({ runtimeId }: { runtimeId: string }) {
|
|
const [status, setStatus] = useState<RuntimePingStatus | null>(null);
|
|
const [output, setOutput] = useState("");
|
|
const [error, setError] = useState("");
|
|
const [durationMs, setDurationMs] = useState<number | null>(null);
|
|
const [testing, setTesting] = useState(false);
|
|
const pollRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
|
|
const cleanup = useCallback(() => {
|
|
if (pollRef.current) {
|
|
clearInterval(pollRef.current);
|
|
pollRef.current = null;
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => cleanup, [cleanup]);
|
|
|
|
const handleTest = async () => {
|
|
cleanup();
|
|
setTesting(true);
|
|
setStatus("pending");
|
|
setOutput("");
|
|
setError("");
|
|
setDurationMs(null);
|
|
|
|
try {
|
|
const ping = await api.pingRuntime(runtimeId);
|
|
|
|
pollRef.current = setInterval(async () => {
|
|
try {
|
|
const result = await api.getPingResult(runtimeId, ping.id);
|
|
setStatus(result.status as RuntimePingStatus);
|
|
|
|
if (result.status === "completed") {
|
|
setOutput(result.output ?? "");
|
|
setDurationMs(result.duration_ms ?? null);
|
|
setTesting(false);
|
|
cleanup();
|
|
} else if (result.status === "failed" || result.status === "timeout") {
|
|
setError(result.error ?? "Unknown error");
|
|
setDurationMs(result.duration_ms ?? null);
|
|
setTesting(false);
|
|
cleanup();
|
|
}
|
|
} catch {
|
|
// ignore poll errors
|
|
}
|
|
}, 2000);
|
|
} catch {
|
|
setStatus("failed");
|
|
setError("Failed to initiate test");
|
|
setTesting(false);
|
|
}
|
|
};
|
|
|
|
const config = status ? pingStatusConfig[status] : null;
|
|
const Icon = config?.icon;
|
|
const isActive = status === "pending" || status === "running";
|
|
|
|
return (
|
|
<div className="space-y-2">
|
|
<div className="flex items-center gap-2">
|
|
<Button
|
|
variant="outline"
|
|
size="xs"
|
|
onClick={handleTest}
|
|
disabled={testing}
|
|
>
|
|
{testing ? (
|
|
<Loader2 className="h-3 w-3 animate-spin" />
|
|
) : (
|
|
<Zap className="h-3 w-3" />
|
|
)}
|
|
{testing ? "Testing..." : "Test Connection"}
|
|
</Button>
|
|
|
|
{config && Icon && (
|
|
<span className={`inline-flex items-center gap-1 text-xs ${config.color}`}>
|
|
<Icon className={`h-3 w-3 ${isActive ? "animate-spin" : ""}`} />
|
|
{config.label}
|
|
{durationMs != null && (
|
|
<span className="text-muted-foreground">
|
|
({(durationMs / 1000).toFixed(1)}s)
|
|
</span>
|
|
)}
|
|
</span>
|
|
)}
|
|
</div>
|
|
|
|
{status === "completed" && output && (
|
|
<div className="rounded-lg border bg-success/5 px-3 py-2">
|
|
<pre className="text-xs font-mono whitespace-pre-wrap">{output}</pre>
|
|
</div>
|
|
)}
|
|
|
|
{(status === "failed" || status === "timeout") && error && (
|
|
<div className="rounded-lg border border-destructive/20 bg-destructive/5 px-3 py-2">
|
|
<p className="text-xs text-destructive">{error}</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|