Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
588 lines
21 KiB
TypeScript
588 lines
21 KiB
TypeScript
import type { ReactNode } from "react";
|
|
import { useState } from "react";
|
|
import { useExternalStoreSnapshot } from "@tutti-os/ui-react-hooks";
|
|
import {
|
|
Button,
|
|
CheckIcon,
|
|
CopyIcon,
|
|
RefreshIcon,
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
cn
|
|
} from "@tutti-os/ui-system";
|
|
import {
|
|
AgentSetupDialog,
|
|
AgentSetupStepIcon,
|
|
type AgentSetupStepStatus
|
|
} from "../../../shared/agentEnv/AgentSetupDialog.tsx";
|
|
import { useAgentTargetSetupController } from "../../../shared/agentEnv/agentTargetSetupController.tsx";
|
|
import { resolveAgentErrorPresentation } from "../../../shared/agentEnv/agentErrorPresentation.ts";
|
|
import { useAgentVisibleErrorPresentation } from "../../../shared/visibleError/AgentVisibleErrorPresentationContext.tsx";
|
|
import { useAgentHostApi } from "../../../agentActivityHost.tsx";
|
|
import type { AgentHostAgentTargetSetupSnapshot } from "../../../host/agentHostApi.ts";
|
|
import { useTranslation } from "../../../i18n/index.ts";
|
|
import styles from "../AgentGUINode.styles.ts";
|
|
|
|
export interface AgentTargetSetupGateProps {
|
|
children?: ReactNode;
|
|
carouselMountedExternally: boolean;
|
|
dialogOwner?: boolean;
|
|
gateVisible?: boolean;
|
|
}
|
|
|
|
export function AgentTargetSetupGate({
|
|
children,
|
|
carouselMountedExternally,
|
|
dialogOwner = false,
|
|
gateVisible = true
|
|
}: AgentTargetSetupGateProps): React.JSX.Element {
|
|
const controller = useAgentTargetSetupController();
|
|
const { scope } = useAgentVisibleErrorPresentation();
|
|
const { terminalLogin } = useAgentHostApi();
|
|
const { t } = useTranslation();
|
|
const state = useExternalStoreSnapshot(controller);
|
|
const {
|
|
agentTarget,
|
|
agentTargetId,
|
|
authenticatePending,
|
|
dialogOpen,
|
|
enabled,
|
|
installPending,
|
|
selectedAuthMethodId,
|
|
setup,
|
|
terminalLoginAvailable,
|
|
terminalLoginError,
|
|
terminalLoginPhase
|
|
} = state;
|
|
const { snapshot, loading, failed } = setup;
|
|
const authMethods = snapshot?.authMethods ?? [];
|
|
const account = snapshot?.account ?? null;
|
|
const effectiveAuthMethodId = authMethods.some(
|
|
(method) => method.id === selectedAuthMethodId
|
|
)
|
|
? (selectedAuthMethodId ?? "")
|
|
: authMethods.some((method) => method.id === account?.authMethodId)
|
|
? (account?.authMethodId ?? "")
|
|
: (authMethods[0]?.id ?? "");
|
|
const effectiveAuthMethod = authMethods.find(
|
|
(method) => method.id === effectiveAuthMethodId
|
|
);
|
|
const terminalLoginCommand =
|
|
effectiveAuthMethod?.type === "terminal"
|
|
? (effectiveAuthMethod.terminalCommand?.trim() ?? "") || null
|
|
: null;
|
|
const terminalStartupAction =
|
|
effectiveAuthMethod?.type === "terminal"
|
|
? (effectiveAuthMethod.terminalStartupAction ?? null)
|
|
: null;
|
|
|
|
// A shared caller can observe the Owner target's readiness, but must never
|
|
// be routed into this device's setup/login/install workflow. Shared hosts
|
|
// own their separate availability presentation and execution errors.
|
|
if (!enabled || scope === "shared_caller") {
|
|
return <>{children}</>;
|
|
}
|
|
|
|
const setupChecking = !snapshot && !failed;
|
|
const setupBlocked = failed || !snapshot || snapshot.status !== "ready";
|
|
const handleInstall = async () => {
|
|
const plan = snapshot?.plan;
|
|
if (plan) await controller.install(plan.planDigest);
|
|
};
|
|
const handleAuthenticate = async () => {
|
|
if (!effectiveAuthMethodId) return;
|
|
await controller.authenticate(effectiveAuthMethodId);
|
|
};
|
|
const terminalLoginLaunchAvailable =
|
|
terminalLoginAvailable &&
|
|
Boolean(terminalLoginCommand) &&
|
|
(!terminalStartupAction ||
|
|
Boolean(
|
|
terminalLogin?.supportedStartupActionTypes?.includes(
|
|
terminalStartupAction.type
|
|
)
|
|
));
|
|
const handleTerminalLoginStart = async () => {
|
|
if (!terminalLoginCommand) return;
|
|
await controller.startTerminalLogin({
|
|
command: terminalLoginCommand,
|
|
startupAction: terminalStartupAction
|
|
});
|
|
};
|
|
const handleTerminalLoginCancel = () => controller.cancelTerminalLogin();
|
|
const actionRunning = isSetupActionRunning(snapshot?.action?.status);
|
|
const actionFailed = isSetupActionFailed(snapshot?.action?.status);
|
|
const installRetryAvailable =
|
|
snapshot?.status === "failed" &&
|
|
snapshot.action?.kind === "install" &&
|
|
actionFailed &&
|
|
Boolean(snapshot.plan);
|
|
const phase = actionRunning ? (snapshot?.action?.phase ?? null) : null;
|
|
const statusLabel = phase
|
|
? targetSetupPhaseLabel(t, phase)
|
|
: loading
|
|
? t("agentHost.agentGui.targetSetupChecking")
|
|
: null;
|
|
const detectionStatus: AgentSetupStepStatus = failed
|
|
? "error"
|
|
: loading || !snapshot
|
|
? "running"
|
|
: snapshot
|
|
? "ok"
|
|
: "pending";
|
|
const installStatus = resolveInstallStepStatus(snapshot);
|
|
const loginStatus = resolveLoginStepStatus(snapshot);
|
|
const providerLabel = agentTarget?.label ?? agentTargetId;
|
|
const setupFailurePresentation =
|
|
snapshot?.status === "failed"
|
|
? resolveAgentErrorPresentation(snapshot.reason)
|
|
: null;
|
|
const setupFailureMessage = setupFailurePresentation?.messageKey
|
|
? t(setupFailurePresentation.messageKey, { provider: providerLabel })
|
|
: null;
|
|
const accountDetail = account
|
|
? [account.displayName, account.organization].filter(Boolean).join(" · ")
|
|
: undefined;
|
|
const setupDescription = setupChecking
|
|
? t("agentHost.agentGui.targetSetupChecking")
|
|
: snapshot?.status === "auth_required"
|
|
? t("agentHost.agentGui.targetSetupAuthRequired")
|
|
: snapshot?.status === "ready"
|
|
? t("agentHost.agentGui.targetSetupReady")
|
|
: (setupFailureMessage ??
|
|
t("agentHost.agentGui.targetSetupDescription"));
|
|
const authenticationAvailable =
|
|
snapshot?.status === "auth_required" || snapshot?.status === "ready";
|
|
const signInApplicable =
|
|
authMethods.length > 0 ||
|
|
account !== null ||
|
|
snapshot?.status === "auth_required" ||
|
|
snapshot?.status === "authenticating";
|
|
// Prefer setup projection over the local authenticate promise. OAuth can mark
|
|
// the account ready before the host login call settles.
|
|
const loginActionPending =
|
|
authenticatePending && snapshot?.status !== "ready";
|
|
|
|
return (
|
|
<>
|
|
{gateVisible && setupBlocked ? (
|
|
<div className={styles.emptyHero}>
|
|
<div
|
|
className={cn(styles.emptyHeroBody, styles.emptyProviderGate)}
|
|
data-testid="agent-target-setup-gate"
|
|
role="status"
|
|
>
|
|
{carouselMountedExternally ? (
|
|
<div
|
|
aria-hidden="true"
|
|
className={styles.emptyHeroCarouselPlaceholder}
|
|
/>
|
|
) : null}
|
|
<h2 className={styles.emptyHeroTitle}>
|
|
{t("agentHost.agentGui.targetSetupTitle", {
|
|
provider: providerLabel
|
|
})}
|
|
</h2>
|
|
<p className={styles.emptyProviderGateDescription}>
|
|
{setupDescription}
|
|
</p>
|
|
{!setupChecking ? (
|
|
<Button
|
|
type="button"
|
|
className={styles.emptyProviderGateAction}
|
|
onClick={() => controller.setDialogOpen(true)}
|
|
>
|
|
{t("agentHost.agentGui.targetSetupOpen")}
|
|
</Button>
|
|
) : null}
|
|
</div>
|
|
</div>
|
|
) : (
|
|
children
|
|
)}
|
|
|
|
{dialogOwner ? (
|
|
<AgentSetupDialog
|
|
open={dialogOpen}
|
|
onOpenChange={controller.setDialogOpen}
|
|
title={t("agentHost.agentGui.targetSetupTitle", {
|
|
provider: providerLabel
|
|
})}
|
|
description={setupDescription}
|
|
footer={
|
|
<Button
|
|
size="dialog"
|
|
variant="secondary"
|
|
type="button"
|
|
disabled={loading}
|
|
onClick={() => void controller.refresh()}
|
|
>
|
|
<RefreshIcon className="size-4" />
|
|
{t("agentHost.agentGui.targetSetupRetry")}
|
|
</Button>
|
|
}
|
|
>
|
|
<div className="flex flex-col gap-4">
|
|
<p className="m-0 text-[13px] text-[var(--text-secondary)]">
|
|
{statusLabel ??
|
|
(snapshot?.status === "ready"
|
|
? t("agentHost.agentGui.targetSetupComplete", {
|
|
provider: providerLabel
|
|
})
|
|
: t("agentHost.agentGui.targetSetupRemaining", {
|
|
provider: providerLabel
|
|
}))}
|
|
</p>
|
|
|
|
<ol className="m-0 flex list-none flex-col divide-y divide-[var(--border-1)] p-0">
|
|
<SetupTrackRow
|
|
label={t("agentHost.agentGui.targetSetupStage.detect")}
|
|
status={detectionStatus}
|
|
detail={
|
|
snapshot?.runtimeVersion
|
|
? `${snapshot.runtimeVersion} · ${snapshot.runtimeSource ?? ""}`
|
|
: undefined
|
|
}
|
|
/>
|
|
|
|
<SetupTrackRow
|
|
label={t("agentHost.agentGui.targetSetupStage.install")}
|
|
status={installStatus}
|
|
warning={snapshot?.status === "not_installed"}
|
|
detail={
|
|
snapshot?.plan
|
|
? `${snapshot.plan.packageName}@${snapshot.plan.packageVersion}`
|
|
: undefined
|
|
}
|
|
action={
|
|
(snapshot?.status === "not_installed" ||
|
|
installRetryAvailable) &&
|
|
snapshot.plan ? (
|
|
<Button
|
|
type="button"
|
|
size="sm"
|
|
disabled={installPending}
|
|
onClick={() => void handleInstall()}
|
|
>
|
|
{installPending
|
|
? t("agentHost.agentGui.targetSetupStarting")
|
|
: t(
|
|
installRetryAvailable
|
|
? "agentHost.agentGui.targetSetupReinstall"
|
|
: "agentHost.agentGui.targetSetupInstall"
|
|
)}
|
|
</Button>
|
|
) : undefined
|
|
}
|
|
>
|
|
{snapshot?.plan ? (
|
|
<span className="mt-2 block break-all text-[12px] text-[var(--text-secondary)]">
|
|
{snapshot.plan.installRoot}
|
|
</span>
|
|
) : null}
|
|
</SetupTrackRow>
|
|
|
|
{signInApplicable ? (
|
|
<SetupTrackRow
|
|
label={t(
|
|
snapshot?.status === "ready"
|
|
? "agentHost.agentGui.targetSetupLoggedInAccount"
|
|
: "agentHost.agentGui.targetSetupStage.login"
|
|
)}
|
|
status={loginStatus}
|
|
warning={snapshot?.status === "auth_required"}
|
|
detail={
|
|
snapshot?.status === "ready" ? accountDetail : undefined
|
|
}
|
|
action={
|
|
authenticationAvailable &&
|
|
authMethods.length > 0 &&
|
|
!terminalLoginCommand ? (
|
|
<Button
|
|
type="button"
|
|
size="sm"
|
|
disabled={!effectiveAuthMethodId || loginActionPending}
|
|
onClick={() => void handleAuthenticate()}
|
|
>
|
|
{loginActionPending
|
|
? t("agentHost.agentGui.targetSetupAuthStarting")
|
|
: snapshot?.status === "ready"
|
|
? t("agentHost.agentGui.targetSetupReauthenticate")
|
|
: t("agentHost.agentGui.targetSetupAuthenticate")}
|
|
</Button>
|
|
) : terminalLoginCommand &&
|
|
terminalLoginLaunchAvailable &&
|
|
terminalLoginPhase !== "waiting" ? (
|
|
<Button
|
|
type="button"
|
|
size="sm"
|
|
onClick={() => void handleTerminalLoginStart()}
|
|
>
|
|
{t("agentHost.agentGui.targetSetupTerminalLoginStart")}
|
|
</Button>
|
|
) : undefined
|
|
}
|
|
>
|
|
{terminalLoginCommand ? (
|
|
<TerminalLoginGuide
|
|
command={terminalLoginCommand}
|
|
error={
|
|
terminalLoginError === "timed_out"
|
|
? t(
|
|
"agentHost.agentGui.targetSetupTerminalLoginTimedOut"
|
|
)
|
|
: terminalLoginError === "unavailable"
|
|
? t(
|
|
"agentHost.agentGui.targetSetupTerminalLoginUnavailable"
|
|
)
|
|
: null
|
|
}
|
|
onCancelLogin={
|
|
terminalLoginPhase === "waiting"
|
|
? handleTerminalLoginCancel
|
|
: undefined
|
|
}
|
|
waiting={terminalLoginPhase === "waiting"}
|
|
/>
|
|
) : null}
|
|
{snapshot?.status === "auth_required" &&
|
|
authMethods.length > 0 ? (
|
|
<label className="mt-2 flex flex-col gap-1 text-[12px] text-[var(--text-secondary)]">
|
|
{t("agentHost.agentGui.targetSetupAuthMethod")}
|
|
<Select
|
|
value={effectiveAuthMethodId}
|
|
onValueChange={controller.selectAuthMethod}
|
|
>
|
|
<SelectTrigger
|
|
aria-label={t(
|
|
"agentHost.agentGui.targetSetupAuthMethod"
|
|
)}
|
|
>
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent
|
|
style={{ zIndex: "var(--z-dialog-popover)" }}
|
|
>
|
|
{authMethods.map((method) => (
|
|
<SelectItem key={method.id} value={method.id}>
|
|
{method.name}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</label>
|
|
) : snapshot?.status === "auth_required" ? (
|
|
<p className="mt-2 mb-0 text-[12px] text-[var(--text-secondary)]">
|
|
{t("agentHost.agentGui.targetSetupNoAuthMethods")}
|
|
</p>
|
|
) : null}
|
|
</SetupTrackRow>
|
|
) : null}
|
|
</ol>
|
|
|
|
{failed || snapshot?.status === "failed" || actionFailed ? (
|
|
<p className="m-0 text-[12px] text-[var(--state-danger)]">
|
|
<span className="block">
|
|
{snapshot?.action?.kind === "authenticate"
|
|
? t("agentHost.agentGui.targetSetupAuthFailed")
|
|
: t("agentHost.agentGui.targetSetupFailed")}
|
|
</span>
|
|
{setupFailureMessage ? (
|
|
<span className="mt-1 block break-words text-[var(--text-secondary)]">
|
|
{setupFailureMessage}
|
|
</span>
|
|
) : snapshot?.action?.errorMessage?.trim() ? (
|
|
<span className="mt-1 block break-words text-[var(--text-secondary)]">
|
|
{snapshot.action.errorMessage.trim()}
|
|
</span>
|
|
) : null}
|
|
</p>
|
|
) : null}
|
|
</div>
|
|
</AgentSetupDialog>
|
|
) : null}
|
|
</>
|
|
);
|
|
}
|
|
|
|
function TerminalLoginGuide({
|
|
command,
|
|
error,
|
|
onCancelLogin,
|
|
waiting = false
|
|
}: {
|
|
command: string;
|
|
error?: string | null;
|
|
onCancelLogin?: () => void;
|
|
waiting?: boolean;
|
|
}): React.JSX.Element {
|
|
const { t } = useTranslation();
|
|
const { clipboard } = useAgentHostApi();
|
|
const [copiedCommand, setCopiedCommand] = useState<string | null>(null);
|
|
const copied = copiedCommand === command;
|
|
const handleCopy = async () => {
|
|
try {
|
|
await clipboard.writeText(command);
|
|
setCopiedCommand(command);
|
|
} catch (error) {
|
|
// Clipboard unavailable; the command text remains selectable.
|
|
console.warn("agent-gui: clipboard copy failed", error);
|
|
}
|
|
};
|
|
return (
|
|
<div className="mt-2 flex flex-col gap-2">
|
|
<p className="m-0 text-[12px] text-[var(--text-secondary)]">
|
|
{waiting
|
|
? t("agentHost.agentGui.targetSetupTerminalLoginWaiting")
|
|
: t("agentHost.agentGui.targetSetupTerminalAuthHint")}
|
|
</p>
|
|
<div className="flex items-start gap-2">
|
|
<code className="min-w-0 flex-1 select-all break-all rounded-md border border-[var(--line-2)] bg-[var(--background-fronted)] px-3 py-2 font-mono text-[11px] leading-5 text-[var(--text-primary)]">
|
|
{command}
|
|
</code>
|
|
<Button
|
|
type="button"
|
|
size="icon-sm"
|
|
variant="ghost"
|
|
aria-label={
|
|
copied
|
|
? t("agentHost.agentGui.targetSetupCommandCopied")
|
|
: t("agentHost.agentGui.targetSetupCopyCommand")
|
|
}
|
|
title={
|
|
copied
|
|
? t("agentHost.agentGui.targetSetupCommandCopied")
|
|
: t("agentHost.agentGui.targetSetupCopyCommand")
|
|
}
|
|
onClick={() => void handleCopy()}
|
|
>
|
|
{copied ? (
|
|
<CheckIcon aria-hidden="true" />
|
|
) : (
|
|
<CopyIcon aria-hidden="true" />
|
|
)}
|
|
</Button>
|
|
</div>
|
|
{waiting && onCancelLogin ? (
|
|
<div className="flex items-center gap-2">
|
|
<Button type="button" size="sm" onClick={onCancelLogin}>
|
|
{t("agentHost.agentGui.targetSetupTerminalLoginCancel")}
|
|
</Button>
|
|
</div>
|
|
) : null}
|
|
{error ? (
|
|
<p className="m-0 text-[12px] text-[var(--state-danger)]">{error}</p>
|
|
) : null}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function isSetupActionRunning(status: string | undefined): boolean {
|
|
return status === "queued" || status === "running";
|
|
}
|
|
|
|
function isSetupActionFailed(status: string | undefined): boolean {
|
|
return status === "failed" || status === "interrupted";
|
|
}
|
|
|
|
function SetupTrackRow({
|
|
action,
|
|
children,
|
|
detail,
|
|
label,
|
|
status,
|
|
warning = false
|
|
}: {
|
|
action?: ReactNode;
|
|
children?: ReactNode;
|
|
detail?: string;
|
|
label: string;
|
|
status: AgentSetupStepStatus;
|
|
warning?: boolean;
|
|
}): React.JSX.Element {
|
|
return (
|
|
<li
|
|
data-status={status}
|
|
className={`flex items-start gap-2.5 py-3 ${status === "pending" ? "opacity-50" : ""}`}
|
|
>
|
|
<span className="mt-0.5 shrink-0">
|
|
<AgentSetupStepIcon status={status} warning={warning} />
|
|
</span>
|
|
<span className="min-w-0 flex-1">
|
|
<span className="flex min-w-0 items-baseline gap-2">
|
|
<span
|
|
className={`shrink-0 text-[13px] font-medium ${status === "error" ? (warning ? "text-[var(--state-warning)]" : "text-[var(--state-danger)]") : "text-[var(--text-primary)]"}`}
|
|
>
|
|
{label}
|
|
</span>
|
|
{detail ? (
|
|
<span className="min-w-0 truncate text-[12px] text-[var(--text-secondary)]">
|
|
{detail}
|
|
</span>
|
|
) : null}
|
|
</span>
|
|
{children}
|
|
</span>
|
|
{action}
|
|
</li>
|
|
);
|
|
}
|
|
|
|
function resolveInstallStepStatus(
|
|
snapshot: AgentHostAgentTargetSetupSnapshot | null
|
|
): AgentSetupStepStatus {
|
|
if (!snapshot) return "pending";
|
|
if (snapshot.status === "installing") return "running";
|
|
if (snapshot.status === "not_installed") return "error";
|
|
if (
|
|
snapshot.status === "auth_required" ||
|
|
snapshot.status === "authenticating" ||
|
|
snapshot.status === "ready" ||
|
|
snapshot.runtimeSource
|
|
) {
|
|
return "ok";
|
|
}
|
|
return snapshot.status === "failed" ? "error" : "pending";
|
|
}
|
|
|
|
function resolveLoginStepStatus(
|
|
snapshot: AgentHostAgentTargetSetupSnapshot | null
|
|
): AgentSetupStepStatus {
|
|
if (!snapshot) return "pending";
|
|
if (snapshot.status === "authenticating") return "running";
|
|
if (snapshot.status === "auth_required") return "error";
|
|
if (snapshot.status === "ready") return "ok";
|
|
if (
|
|
snapshot.status === "failed" &&
|
|
snapshot.action?.kind === "authenticate"
|
|
) {
|
|
return "error";
|
|
}
|
|
return "pending";
|
|
}
|
|
|
|
function targetSetupPhaseLabel(
|
|
t: ReturnType<typeof useTranslation>["t"],
|
|
phase: NonNullable<AgentHostAgentTargetSetupSnapshot["action"]>["phase"]
|
|
): string {
|
|
switch (phase) {
|
|
case "preparing":
|
|
return t("agentHost.agentGui.targetSetupPhase.preparing");
|
|
case "installing":
|
|
return t("agentHost.agentGui.targetSetupPhase.installing");
|
|
case "verifying":
|
|
return t("agentHost.agentGui.targetSetupPhase.verifying");
|
|
case "probing":
|
|
return t("agentHost.agentGui.targetSetupPhase.probing");
|
|
case "activating":
|
|
return t("agentHost.agentGui.targetSetupPhase.activating");
|
|
case "authenticating":
|
|
return t("agentHost.agentGui.targetSetupPhase.authenticating");
|
|
case "complete":
|
|
return t("agentHost.agentGui.targetSetupPhase.complete");
|
|
}
|
|
}
|