diff --git a/frontend/README.md b/frontend/README.md index 9a9467625..370283488 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -945,3 +945,26 @@ it natively. Each component lives in its own self-registering directory under view, so a catalog/renderer mismatch never breaks the page. To add a component, drop a folder there (frontend) and declare it in the agent's catalog (backend — see `veadk.a2ui.BaseA2UICatalog`). + +### Sleeping agents + +Agent categories query live Sessions and Session snapshots without restoring them, +including legacy requests with `autoResumeSnapshots=true`. Studio presents the +latest saved record for each logical agent, unless a live Session already +represents it. Live and wakeable cards share the Ready label and styling; saved +records show Never expires. Details use the same Ready label and explain +separately when opening requires waking the agent. Snapshot `SessionMetadata` supplies its display +name, creator and agent kind, including on SDK versions that omit this field. +Owners can list, wake and delete their own records; administrators can also manage +legacy records without owner metadata. Records with a different agent kind are +excluded when kinds share a Tool. + +Opening a sleeping agent explicitly restores it and then opens its normal entry +point. The UI explains the wait using “waking” language and permits retry after a +failure. Same-agent wake requests are serialized within a service instance, and +retries check for an existing live Session. This is not a distributed lock. +Deleting a sleeping agent requires confirmation and sends `ToolId` and `SnapshotId` +to `DeleteSessionSnapshot`; it deletes the selected saved record only. If that +logical agent has older records, the next latest record can appear on refresh. +Failed records remain visible for deletion but cannot be opened. The UI uses +agent terminology rather than exposing these control-plane resource types. diff --git a/frontend/src/adk/sandbox.ts b/frontend/src/adk/sandbox.ts index b635f00a5..efc22be56 100644 --- a/frontend/src/adk/sandbox.ts +++ b/frontend/src/adk/sandbox.ts @@ -18,6 +18,12 @@ export const CODEX_PROJECT_HANDOFF_PAIRING_TTL_SECONDS = 60 * 60; export const SANDBOX_DISPLAY_NAME_MAX_LENGTH = 40; export type SandboxAgentKind = "deepseek-harness" | "openclaw" | "hermes"; +/** Present agent availability without changing the underlying lifecycle state. */ +export function sandboxCardStatus(status: string): string { + const normalized = status.trim().toLowerCase(); + return ["ready", "running", "wakeable"].includes(normalized) ? "ready" : normalized; +} + export function sandboxStatusLabel(status: string): string { switch (status.trim().toLowerCase()) { case "ready": @@ -697,8 +703,8 @@ function parseSnapshot( } function sandboxListUrl(base: string, options?: SandboxListOptions): string { - if (!options?.autoResumeSnapshots) return base; - const params = new URLSearchParams({ autoResumeSnapshots: "true" }); + if (options?.autoResumeSnapshots === undefined) return base; + const params = new URLSearchParams({ autoResumeSnapshots: String(options.autoResumeSnapshots) }); return `${base}?${params.toString()}`; } diff --git a/frontend/src/i18n/resources/en-US/adk.json b/frontend/src/i18n/resources/en-US/adk.json index 24cfc4487..11e8747bc 100644 --- a/frontend/src/i18n/resources/en-US/adk.json +++ b/frontend/src/i18n/resources/en-US/adk.json @@ -237,7 +237,7 @@ "sandbox": { "status": { "ready": "Ready", - "wakeable": "Wakeable", + "wakeable": "Asleep", "creating": "Creating", "starting": "Starting", "pending": "Pending", @@ -276,9 +276,9 @@ "missingSessionForTerminal": "The AgentKit session for the terminal is missing.", "openTerminalFailed": "Unable to open the {{kind}} terminal.", "deleteAgentFailed": "Unable to delete the {{kind}} agent.", - "missingSnapshot": "The AgentKit snapshot to wake is missing.", - "resumeSnapshotFailed": "Unable to wake the agent from its snapshot. Try again later.", - "deleteSnapshotFailed": "Unable to delete the agent snapshot.", + "missingSnapshot": "The agent to wake was not found.", + "resumeSnapshotFailed": "Unable to wake the agent. Try again later.", + "deleteSnapshotFailed": "Unable to delete the agent. Try again later.", "missingSessionToConnect": "The AgentKit session to connect is missing.", "connectCodexFailed": "Unable to connect to the Codex agent. Try again later.", "sessionNotReady": "The AgentKit session is not ready. Current status: {{status}}.", diff --git a/frontend/src/i18n/resources/en-US/newChat.json b/frontend/src/i18n/resources/en-US/newChat.json index 122213cfc..47d1f71d6 100644 --- a/frontend/src/i18n/resources/en-US/newChat.json +++ b/frontend/src/i18n/resources/en-US/newChat.json @@ -77,7 +77,8 @@ "loadType": "Load {{type}}", "connectGeneral": "Connect to General Agent", "openLocal": "Open local Agent", - "openType": "Open {{type}}" + "openType": "Open {{type}}", + "wakingHint": "Waking the agent. This may take some time." }, "skill": { "spaceAria": "Skill spaces", diff --git a/frontend/src/i18n/resources/en-US/sandbox.json b/frontend/src/i18n/resources/en-US/sandbox.json index 079bf8634..6998a5f0c 100644 --- a/frontend/src/i18n/resources/en-US/sandbox.json +++ b/frontend/src/i18n/resources/en-US/sandbox.json @@ -143,7 +143,7 @@ }, "agentDetails": { "back": "Back to Agents", - "subtitle": "{{agent}} AgentKit Session details", + "subtitle": "{{agent}} agent details", "type": "Agent type", "status": "Status", "createdBy": "Created by", @@ -161,9 +161,12 @@ "wake": "Wake Agent", "open": "Open Agent", "deleteTitle": "Delete Agent?", - "deleteDescription": "This will delete “{{name}}” and its AgentKit {{resource}}. This action cannot be undone.", + "deleteDescription": "Delete “{{name}}” and its saved data? This cannot be undone.", "deleting": "Deleting…", - "confirmDelete": "Delete" + "confirmDelete": "Delete", + "sleepingHint": "This agent is asleep. Waking it before opening may take some time.", + "wakingHint": "Waking the agent. This may take some time.", + "agentId": "Agent ID" }, "agentWorkspace": { "back": "Back to Agents", diff --git a/frontend/src/i18n/resources/en-US/ui.json b/frontend/src/i18n/resources/en-US/ui.json index 10dcbee2b..9f9188dfb 100644 --- a/frontend/src/i18n/resources/en-US/ui.json +++ b/frontend/src/i18n/resources/en-US/ui.json @@ -1338,7 +1338,7 @@ "remainingTime": "Time remaining", "expiringSoon": "Expiring soon", "sandboxRemaining": "{{hours}} hr {{minutes}} min", - "wakeable": "Wakeable", + "wakeable": "Asleep", "neverExpires": "Never expires", "editDraftNamed": "Edit draft {{name}}", "viewProgress": "View progress", @@ -1381,7 +1381,7 @@ }, "sandboxStatus": { "ready": "Ready", - "wakeable": "Wakeable", + "wakeable": "Asleep", "creating": "Creating", "starting": "Starting", "initializing": "Starting", @@ -1394,7 +1394,8 @@ "deleting": "Deleting", "deleted": "Deleted", "unknown": "Unknown status" - } + }, + "wakingHint": "Waking the agent. This may take some time." }, "skillCenter": { "library": "Skill library", diff --git a/frontend/src/i18n/resources/zh-CN/adk.json b/frontend/src/i18n/resources/zh-CN/adk.json index 8114e2b99..9475a42ca 100644 --- a/frontend/src/i18n/resources/zh-CN/adk.json +++ b/frontend/src/i18n/resources/zh-CN/adk.json @@ -237,7 +237,7 @@ "sandbox": { "status": { "ready": "就绪", - "wakeable": "可唤醒", + "wakeable": "已休眠", "creating": "创建中", "starting": "启动中", "pending": "等待中", @@ -276,9 +276,9 @@ "missingSessionForTerminal": "缺少要打开 Terminal 的 AgentKit Session。", "openTerminalFailed": "无法打开 {{kind}} Terminal。", "deleteAgentFailed": "无法删除 {{kind}} 智能体。", - "missingSnapshot": "缺少要唤醒的 AgentKit Snapshot。", - "resumeSnapshotFailed": "无法从快照唤醒智能体,请稍后重试。", - "deleteSnapshotFailed": "无法删除智能体快照。", + "missingSnapshot": "未找到要唤醒的智能体。", + "resumeSnapshotFailed": "无法唤醒智能体,请稍后重试。", + "deleteSnapshotFailed": "无法删除智能体,请稍后重试。", "missingSessionToConnect": "缺少要连接的 AgentKit Session。", "connectCodexFailed": "无法连接 Codex 智能体,请稍后重试。", "sessionNotReady": "AgentKit Session 尚未就绪,当前状态:{{status}}。", diff --git a/frontend/src/i18n/resources/zh-CN/newChat.json b/frontend/src/i18n/resources/zh-CN/newChat.json index 5693ae261..00a30059c 100644 --- a/frontend/src/i18n/resources/zh-CN/newChat.json +++ b/frontend/src/i18n/resources/zh-CN/newChat.json @@ -77,7 +77,8 @@ "loadType": "加载 {{type}}", "connectGeneral": "连接通用智能体", "openLocal": "打开本地智能体", - "openType": "打开 {{type}}" + "openType": "打开 {{type}}", + "wakingHint": "正在唤醒智能体,可能需要一些时间。" }, "skill": { "spaceAria": "技能空间", diff --git a/frontend/src/i18n/resources/zh-CN/sandbox.json b/frontend/src/i18n/resources/zh-CN/sandbox.json index ddabf50db..e7d356f83 100644 --- a/frontend/src/i18n/resources/zh-CN/sandbox.json +++ b/frontend/src/i18n/resources/zh-CN/sandbox.json @@ -143,7 +143,7 @@ }, "agentDetails": { "back": "返回智能体列表", - "subtitle": "{{agent}} AgentKit Session 详情", + "subtitle": "{{agent}} 智能体详情", "type": "智能体类型", "status": "状态", "createdBy": "创建人", @@ -161,9 +161,12 @@ "wake": "唤醒智能体", "open": "打开智能体", "deleteTitle": "删除智能体?", - "deleteDescription": "将删除“{{name}}”及其 AgentKit {{resource}},此操作无法撤销。", + "deleteDescription": "将删除“{{name}}”及其保存的数据,此操作无法撤销。", "deleting": "删除中…", - "confirmDelete": "确认删除" + "confirmDelete": "确认删除", + "sleepingHint": "该智能体已休眠,进入时需要唤醒,可能需要一些时间。", + "wakingHint": "正在唤醒智能体,可能需要一些时间。", + "agentId": "智能体 ID" }, "agentWorkspace": { "back": "返回智能体列表", diff --git a/frontend/src/i18n/resources/zh-CN/ui.json b/frontend/src/i18n/resources/zh-CN/ui.json index fe5d03f78..0dc3bf5c7 100644 --- a/frontend/src/i18n/resources/zh-CN/ui.json +++ b/frontend/src/i18n/resources/zh-CN/ui.json @@ -1338,7 +1338,7 @@ "remainingTime": "剩余时间", "expiringSoon": "即将清空", "sandboxRemaining": "{{hours}} 小时 {{minutes}} 分钟", - "wakeable": "可唤醒", + "wakeable": "已休眠", "neverExpires": "永不过期", "editDraftNamed": "编辑草稿 {{name}}", "viewProgress": "查看进度", @@ -1381,7 +1381,7 @@ }, "sandboxStatus": { "ready": "就绪", - "wakeable": "可唤醒", + "wakeable": "已休眠", "creating": "创建中", "starting": "启动中", "initializing": "启动中", @@ -1394,7 +1394,8 @@ "deleting": "删除中", "deleted": "已删除", "unknown": "未知状态" - } + }, + "wakingHint": "正在唤醒智能体,可能需要一些时间。" }, "skillCenter": { "library": "技能库", diff --git a/frontend/src/ui/MyAgents.css b/frontend/src/ui/MyAgents.css index 682017d03..b37df7f08 100644 --- a/frontend/src/ui/MyAgents.css +++ b/frontend/src/ui/MyAgents.css @@ -158,12 +158,6 @@ color: hsl(142 55% 28%); } -.my-agent-status-label[data-wakeable] { - border-color: hsl(43 90% 48% / 0.3); - background: hsl(45 96% 52% / 0.14); - color: hsl(38 78% 36%); -} - .my-agent-compatibility-trigger { flex: 0 0 auto; display: inline-flex; @@ -374,3 +368,30 @@ } } + +.my-agent-wake-note { + margin: 8px 0 0; + color: hsl(var(--muted-foreground)); + font-size: 12px; + line-height: 1.5; +} + +.my-agent-wake-note[role="alert"] { + position: relative; + z-index: 2; + min-width: 0; + min-height: 0; + max-height: 3em; + display: -webkit-box; + overflow: hidden; + overflow-wrap: anywhere; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + color: hsl(var(--destructive)); +} + +.my-agent-error-tooltip { + max-height: min(320px, 60vh); + overflow: auto; + overflow-wrap: anywhere; +} diff --git a/frontend/src/ui/MyAgents.tsx b/frontend/src/ui/MyAgents.tsx index 47035bac1..b85dd3725 100644 --- a/frontend/src/ui/MyAgents.tsx +++ b/frontend/src/ui/MyAgents.tsx @@ -1,3 +1,4 @@ +import { TextShimmer } from "./text-shimmer/TextShimmer"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import type { SVGProps } from "react"; import { EmptyMessage } from "@openai/apps-sdk-ui/components/EmptyMessage"; @@ -29,6 +30,7 @@ import { } from "../adk/cloudProvider"; import { sandboxClient, + sandboxCardStatus, type SandboxAgentResource, type SandboxAgentKind, } from "../adk/sandbox"; @@ -236,7 +238,7 @@ function runtimeToAgent(runtime: CloudRuntime, t: TFunction<"ui">): MyAgentCardD } function sandboxToAgent(session: SandboxAgentResource, t: TFunction<"ui">): MyAgentCardData { - const normalizedStatus = session.status.trim().toLowerCase(); + const normalizedStatus = sandboxCardStatus(session.status); return { id: session.id, name: session.displayName || t("myAgents.namedAgent", { name: session.toolName }), @@ -349,6 +351,7 @@ function AgentCard({ compatibility, onRetryCompatibility, connecting, + connectError, connected, deploymentTask, nowMs, @@ -363,6 +366,7 @@ function AgentCard({ compatibility?: RuntimeCompatibility; onRetryCompatibility?: (agent: MyAgentCardData) => void; connecting?: boolean; + connectError?: string; connected?: boolean; deploymentTask?: DeploymentTaskUpdate; nowMs: number; @@ -388,13 +392,13 @@ function AgentCard({ else onViewDetails?.(agent); return; } - if (!actionable) return; + if (!actionable && !agent.sandbox) return; if (deploymentTask) onViewDeploymentTask?.(deploymentTask); else onViewDetails?.(agent); }; const cardTargetEnabled = agent.draft ? Boolean(deploymentTask ? onViewDeploymentTask : onViewDetails) - : actionable && Boolean(deploymentTask ? onViewDeploymentTask : onViewDetails); + : (actionable || Boolean(agent.sandbox)) && Boolean(deploymentTask ? onViewDeploymentTask : onViewDetails); const cardTargetLabel = agent.draft ? deploymentTask ? t("myAgents.viewDeploymentProgress", { name: agent.name }) @@ -427,13 +431,11 @@ function AgentCard({ }, ...(agent.sandbox ? [{ label: t("myAgents.remainingTime"), - value: agent.sandbox.resourceType === "snapshot" - ? t("myAgents.wakeable") - : agent.sandbox.persistent - ? t("myAgents.neverExpires") - : formatSandboxRemainingTime(agent.sandbox.expireAt, nowMs, t), + value: agent.sandbox.resourceType === "snapshot" || agent.sandbox.persistent + ? t("myAgents.neverExpires") + : formatSandboxRemainingTime(agent.sandbox.expireAt, nowMs, t), className: `my-agent-expiry${ - agent.sandbox.resourceType === "session" && agent.sandbox.persistent + agent.sandbox.resourceType === "snapshot" || agent.sandbox.persistent ? "" : " is-expiring" }`, @@ -515,8 +517,7 @@ function AgentCard({ ) : agent.sandbox ? ( {agent.description} @@ -588,6 +589,14 @@ function AgentCard({ ) : null} /> + {connectError ? ( + +

{connectError}

+
+ ) : null} + {wakeable && connecting ?

+ {t("myAgents.wakingHint")} +

: null} {!agent.sandbox ? ( {agent.description} ) : null} @@ -670,6 +679,7 @@ export function MyAgents({ const [loadingSandboxAgents, setLoadingSandboxAgents] = useState(false); const [sandboxError, setSandboxError] = useState(""); const [connectingAgentId, setConnectingAgentId] = useState(""); + const [connectErrors, setConnectErrors] = useState>({}); const [runtimeCompatibility, setRuntimeCompatibility] = useState< Record >({}); @@ -902,11 +912,11 @@ export function MyAgents({ const sessions = type === "codex" ? await sandboxClient.listSessions({ signal: controller.signal, - autoResumeSnapshots: true, + autoResumeSnapshots: false, }) : await sandboxClient.listAgentSessions(type, { signal: controller.signal, - autoResumeSnapshots: true, + autoResumeSnapshots: false, }); if (sandboxRequestRef.current !== requestId) return; setSandboxAgents(sessions.map((session) => sandboxToAgent(session, t))); @@ -1000,6 +1010,7 @@ export function MyAgents({ const useAgent = useCallback(async (agent: MyAgentCardData) => { if (connectingAgentId) return; setConnectingAgentId(agent.id); + setConnectErrors((current) => ({ ...current, [agent.id]: "" })); try { await new Promise((resolve) => requestAnimationFrame(() => resolve())); if (agent.sandbox) { @@ -1007,6 +1018,8 @@ export function MyAgents({ } else { await onUseAgent(agent); } + } catch (cause) { + setConnectErrors((current) => ({ ...current, [agent.id]: cause instanceof Error ? cause.message : String(cause) })); } finally { setConnectingAgentId(""); } @@ -1320,6 +1333,7 @@ export function MyAgents({ } } : undefined} connecting={agent.id === connectingAgentId} + connectError={connectErrors[agent.id]} connected={agent.runtime?.runtimeId === connectedRuntimeId} onEditDraft={onEditDraft} onDeleteDraft={setDraftToDelete} diff --git a/frontend/src/ui/SandboxAgentDetails.css b/frontend/src/ui/SandboxAgentDetails.css index 204fb87bb..c1c9fd3de 100644 --- a/frontend/src/ui/SandboxAgentDetails.css +++ b/frontend/src/ui/SandboxAgentDetails.css @@ -151,3 +151,11 @@ border-bottom: 0; } } + +.sandbox-agent-wake-note { + margin: 0; + padding: 12px 24px; + color: hsl(var(--muted-foreground)); + font-size: 13px; + line-height: 1.5; +} diff --git a/frontend/src/ui/SandboxAgentDetails.tsx b/frontend/src/ui/SandboxAgentDetails.tsx index b974d9a74..eec95dc7c 100644 --- a/frontend/src/ui/SandboxAgentDetails.tsx +++ b/frontend/src/ui/SandboxAgentDetails.tsx @@ -1,6 +1,7 @@ +import { TextShimmer } from "./text-shimmer/TextShimmer"; import { useState } from "react"; import { useTranslation } from "react-i18next"; -import { sandboxStatusLabel, type SandboxAgentResource } from "../adk/sandbox"; +import { sandboxCardStatus, sandboxStatusLabel, type SandboxAgentResource } from "../adk/sandbox"; import { PageBackButton } from "./PageBackButton"; import "./SandboxAgentDetails.css"; @@ -92,25 +93,17 @@ export function SandboxAgentDetails({
{t("agentDetails.type")}
{label}
-
{t("agentDetails.status")}
{sandboxStatusLabel(session.status)}
+
{t("agentDetails.status")}
{sandboxStatusLabel(sandboxCardStatus(session.status))}
{t("agentDetails.createdBy")}
{session.createdBy?.trim() || t("common.unknownSource")}
-
-
{wakeable ? t("agentDetails.snapshotStatus") : t("agentDetails.toolType")}
-
{wakeable ? session.snapshotStatus || "—" : session.toolType || "—"}
-
{t("agentDetails.createdAt")}
{formatDate(session.createdAt, locale)}
-
-
{wakeable ? t("agentDetails.snapshotReason") : t("agentDetails.expiresAt")}
-
{wakeable ? session.reason || "—" : formatDate(session.expireAt, locale)}
-
-
-
{t(wakeable ? "agentDetails.snapshotId" : "agentDetails.sessionId")}
-
{wakeable ? session.snapshotId : resourceId}
-
- {wakeable && session.sourceSessionId ? ( -
{t("agentDetails.sourceSessionId")}
{session.sourceSessionId}
- ) : null} + {!wakeable ?
{t("agentDetails.expiresAt")}
{formatDate(session.expireAt, locale)}
: null} +
{t("agentDetails.agentId")}
{resourceId}
+ {wakeable && session.status.toLowerCase() === "wakeable" ? ( +

+ {opening ? {t("agentDetails.wakingHint")} : t("agentDetails.sleepingHint")} +

+ ) : null}