diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 6d544aa5..8bea11dc 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1167,6 +1167,7 @@ export default function App() { onCreate={onCreate} onAgentAdded={onAgentAdded} author={String(userInfo?.email ?? userId ?? "")} + features={features} /> ) : createView === "template" ? ( setCreateView("menu")} onCreate={onCreate} /> diff --git a/frontend/src/adk/client.ts b/frontend/src/adk/client.ts index 24a016c5..25775c64 100644 --- a/frontend/src/adk/client.ts +++ b/frontend/src/adk/client.ts @@ -4,6 +4,8 @@ import { withAuth } from "./auth"; import { parseSSE } from "./sse"; +import type { AgentProject } from "../create/project"; +import type { AgentDraft } from "../create/types"; /** An ADK event as serialised over `/run_sse` (camelCase, by_alias=True). */ export interface AdkUsage { @@ -156,6 +158,39 @@ function apiFetch(path: string, init: RequestInit = {}, ep: AdkEndpoint = {}): P return fetch(withAuth(`${API_BASE}${path}`), init); } +function formatErrorDetail(detail: unknown): string { + if (typeof detail === "string") return detail; + if (Array.isArray(detail)) { + return detail + .map((item) => { + if (item && typeof item === "object" && "msg" in item) { + const loc = Array.isArray((item as { loc?: unknown }).loc) + ? (item as { loc?: unknown[] }).loc?.join(".") + : ""; + const msg = String((item as { msg?: unknown }).msg ?? ""); + return loc ? `${loc}: ${msg}` : msg; + } + return String(item); + }) + .filter(Boolean) + .join("\n"); + } + if (detail && typeof detail === "object") return JSON.stringify(detail); + return ""; +} + +async function httpErrorMessage(res: Response, fallback: string): Promise { + const text = await res.text().catch(() => ""); + if (!text) return `${fallback} (${res.status})`; + try { + const data = JSON.parse(text) as { detail?: unknown; error?: unknown }; + const detail = formatErrorDetail(data.detail ?? data.error); + return detail || text || `${fallback} (${res.status})`; + } catch { + return text || `${fallback} (${res.status})`; + } +} + export async function listApps(): Promise { const res = await apiFetch(`/list-apps`); if (!res.ok) throw new Error(`list-apps failed: ${res.status}`); @@ -469,6 +504,8 @@ export interface UiFeatures { addAgent: boolean; manageAgents: boolean; addAgentkit: boolean; + generatedAgentTestRun?: boolean; + generatedAgentTestRunDisabledReason?: string; } export interface UiConfig { @@ -490,6 +527,7 @@ const DEFAULT_UI_CONFIG: UiConfig = { addAgent: true, manageAgents: true, addAgentkit: true, + generatedAgentTestRun: true, }, defaultView: "chat", agentsSource: "local", @@ -627,3 +665,93 @@ export async function getRuntimeDetail( } return res.json(); } + +export interface GeneratedAgentTestRun { + runId: string; + appName: string; + expiresAt: number; +} + +export async function generateAgentProject( + draft: AgentDraft, +): Promise { + const res = await apiFetch("/web/generated-agent-projects", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ draft }), + }); + if (!res.ok) { + throw new Error(await httpErrorMessage(res, "生成项目失败")); + } + return res.json(); +} + +export async function createGeneratedAgentTestRun( + draft: AgentDraft, +): Promise { + const res = await apiFetch("/web/generated-agent-test-runs", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ draft }), + }); + if (!res.ok) { + throw new Error(await httpErrorMessage(res, "创建调试运行失败")); + } + return res.json(); +} + +export async function createGeneratedAgentTestSession( + runId: string, + userId: string, +): Promise { + const res = await apiFetch(`/web/generated-agent-test-runs/${runId}/sessions`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ userId }), + }); + if (!res.ok) { + throw new Error(await httpErrorMessage(res, "创建调试会话失败")); + } + const session = await res.json(); + return session.id; +} + +export async function* runGeneratedAgentTestSSE({ + runId, + userId, + sessionId, + text, + signal, +}: { + runId: string; + userId: string; + sessionId: string; + text: string; + signal?: AbortSignal; +}): AsyncGenerator { + const parts: Record[] = text.trim() ? [{ text }] : []; + const res = await apiFetch(`/web/generated-agent-test-runs/${runId}/run_sse`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + user_id: userId, + session_id: sessionId, + new_message: { role: "user", parts }, + streaming: true, + }), + signal, + }); + if (!res.ok) throw new Error(await httpErrorMessage(res, "调试运行失败")); + for await (const evt of parseSSE(res)) { + yield evt as AdkEvent; + } +} + +export async function deleteGeneratedAgentTestRun(runId: string): Promise { + const res = await apiFetch(`/web/generated-agent-test-runs/${runId}`, { + method: "DELETE", + }); + if (!res.ok && res.status !== 404) { + throw new Error(await httpErrorMessage(res, "清理调试运行失败")); + } +} diff --git a/frontend/src/create/CustomCreate.css b/frontend/src/create/CustomCreate.css index 04af966b..cf5fed0c 100644 --- a/frontend/src/create/CustomCreate.css +++ b/frontend/src/create/CustomCreate.css @@ -22,7 +22,7 @@ /* Left: the Agent structure tree. */ .cw-tree { flex-shrink: 0; - width: 320px; + width: 280px; overflow-y: auto; padding: 16px 14px; border-right: 1px solid hsl(var(--border)); @@ -150,7 +150,7 @@ flex-shrink: 0; background: hsl(var(--panel)); border-bottom: 1px solid hsl(var(--border)); - padding: 20px 44px 14px 16px; + padding: 18px 16px 12px; } .cw-typebar-inner { max-width: 1080px; @@ -172,7 +172,7 @@ } .cw-lower { display: flex; - gap: 36px; + gap: 24px; align-items: flex-start; } .cw-detail .cw-form-col { @@ -181,6 +181,260 @@ margin: 0; } +/* Right: generated-agent debug panel. */ +.cw-debug { + flex-shrink: 0; + width: 300px; + min-height: 0; + display: flex; + flex-direction: column; + border-left: 1px solid hsl(var(--border)); + background: hsl(var(--panel)); +} +.cw-debug-head { + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 16px 16px 10px; +} +.cw-debug-title { + display: inline-flex; + align-items: center; + gap: 7px; + font-size: 14px; + font-weight: 650; + color: hsl(var(--foreground)); +} +.cw-debug-badge { + flex-shrink: 0; + padding: 3px 8px; + border-radius: 999px; + background: hsl(var(--secondary)); + color: hsl(var(--secondary-foreground)); + font-size: 11px; + font-weight: 600; +} +.cw-debug-badge-ready { + background: hsl(var(--primary) / 0.1); + color: hsl(var(--foreground)); +} +.cw-debug-badge-error { + background: hsl(var(--destructive) / 0.1); + color: hsl(var(--destructive)); +} +.cw-debug-badge-stale { + background: hsl(38 92% 50% / 0.14); + color: hsl(32 95% 38%); +} +.cw-debug-sub { + flex-shrink: 0; + display: flex; + flex-direction: column; + gap: 3px; + padding: 0 16px 14px; + color: hsl(var(--muted-foreground)); + font-size: 12px; + line-height: 1.45; +} +.cw-debug-body { + flex: 1; + min-height: 0; + overflow-y: auto; + padding: 10px 16px 18px; +} +.cw-debug-empty { + min-height: 120px; + display: flex; + align-items: center; + justify-content: center; + padding: 14px; + border: 1px dashed hsl(var(--border)); + border-radius: 10px; + color: hsl(var(--muted-foreground)); + font-size: 13px; + line-height: 1.6; + text-align: center; +} +.cw-debug-progress { + display: flex; + flex-direction: column; + gap: 8px; +} +.cw-debug-logline { + display: flex; + align-items: center; + gap: 8px; + min-height: 28px; + padding: 7px 9px; + border-radius: 9px; + background: hsl(var(--foreground) / 0.04); + color: hsl(var(--muted-foreground)); + font-size: 12.5px; +} +.cw-debug-logline .cw-i { + color: hsl(var(--foreground)); +} +.cw-debug-error { + display: flex; + flex-direction: column; + gap: 12px; + color: hsl(var(--destructive)); + font-size: 13px; + line-height: 1.5; +} +.cw-debug-stale { + flex-shrink: 0; + display: flex; + align-items: flex-start; + gap: 10px; + margin: 0 14px 12px; + padding: 11px 12px; + border: 1px solid hsl(38 92% 50% / 0.26); + border-radius: 10px; + background: hsl(38 92% 50% / 0.08); +} +.cw-debug-stale-text { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + gap: 3px; + color: hsl(var(--foreground)); + font-size: 12.5px; + line-height: 1.45; +} +.cw-debug-stale-text span { + color: hsl(var(--muted-foreground)); +} +.cw-debug-stale-btn { + flex-shrink: 0; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 6px; + min-height: 30px; + padding: 6px 10px; + border: none; + border-radius: 8px; + background: hsl(var(--foreground)); + color: hsl(var(--background)); + font: inherit; + font-size: 12px; + font-weight: 600; + cursor: pointer; + transition: opacity 0.12s, transform 0.1s; +} +.cw-debug-stale-btn:hover:not(:disabled) { + opacity: 0.86; +} +.cw-debug-stale-btn:active:not(:disabled) { + transform: scale(0.96); +} +.cw-debug-stale-btn:disabled { + opacity: 0.45; + cursor: default; +} +.cw-debug-chat { + display: flex; + flex-direction: column; + gap: 18px; +} +.cw-debug-msg { + display: flex; + flex-direction: column; + gap: 6px; + max-width: 100%; +} +.cw-debug-msg-user { + align-items: flex-end; +} +.cw-debug-msg-assistant { + align-items: flex-start; +} +.cw-debug-role { + display: none; +} +.cw-debug-content { + max-width: 100%; + color: hsl(var(--foreground)); + font-size: 14px; + line-height: 1.65; + word-break: break-word; +} +.cw-debug-msg-user .cw-debug-content { + max-width: 88%; + padding: 9px 14px; + border-radius: 18px; + background: hsl(var(--secondary)); +} +.cw-debug-msg-assistant .cw-debug-content { + width: 100%; +} +.cw-debug-msg-error { + color: hsl(var(--destructive)); +} +.cw-debug-composer { + flex-shrink: 0; + padding: 10px 14px 14px; + border-top: 1px solid hsl(var(--border)); + background: hsl(var(--panel)); +} +.cw-debug-composerbox { + display: flex; + align-items: flex-end; + gap: 6px; + padding: 6px 6px 6px 10px; + border: 1px solid hsl(var(--border)); + border-radius: 24px; + background: hsl(var(--background)); +} +.cw-debug-input { + flex: 1; + min-width: 0; + max-height: 120px; + padding: 8px 4px; + border: none; + outline: none; + resize: none; + overflow-y: auto; + background: transparent; + color: hsl(var(--foreground)); + font: inherit; + font-size: 14px; + line-height: 1.5; +} +.cw-debug-input::placeholder { + color: hsl(var(--muted-foreground)); +} +.cw-debug-send { + flex-shrink: 0; + display: inline-flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + border: none; + border-radius: 50%; + cursor: pointer; + transition: opacity 0.15s, transform 0.1s, background 0.12s, color 0.12s; +} +.cw-debug-send { + background: hsl(var(--primary)); + color: hsl(var(--primary-foreground)); +} +.cw-debug-send:hover:not(:disabled) { + opacity: 0.85; +} +.cw-debug-send:active:not(:disabled) { + transform: scale(0.94); +} +.cw-debug-send:disabled { + opacity: 0.3; + cursor: default; +} + /* ---------- bottom action bar (validation + generate) ---------- */ .cw-footer-bar { flex-shrink: 0; @@ -391,7 +645,7 @@ top: 0; align-self: flex-start; flex-shrink: 0; - width: 208px; + width: 176px; padding: 4px 0; } .cw-steps { @@ -909,6 +1163,12 @@ .cw-seg-sm .cw-seg-title { font-size: 12.5px; } +.cw-mcp-note { + margin: 0; + font-size: 12.5px; + line-height: 1.5; + color: hsl(var(--muted-foreground)); +} .cw-mcp .cw-add-sub { margin-top: 0; } @@ -1078,17 +1338,32 @@ flex-direction: column; gap: 8px; } -/* Horizontal variant for the top bar: compact chips (icon + title) in a row. */ +/* Horizontal variant for the top bar: main-style compact chips. */ .cw-typeradio--row { flex-direction: row; - flex-wrap: wrap; + flex-wrap: nowrap; + align-items: center; gap: 8px; + overflow-x: hidden; + width: 100%; + max-width: 100%; + padding: 1px 0; + border: none; + background: transparent; + scrollbar-width: thin; } .cw-typeradio--row .cw-typeradio-item { flex: 1 1 0; min-width: 0; + min-height: 44px; align-items: center; - padding: 10px 12px; + justify-content: center; + gap: 0; + padding: 10px 10px; + border: 1px solid hsl(var(--border)); + border-radius: 10px; + background: hsl(var(--background)); + color: hsl(var(--muted-foreground)); } .cw-typeradio-item { display: flex; @@ -1107,9 +1382,23 @@ border-color: hsl(var(--foreground) / 0.2); background: hsl(var(--foreground) / 0.03); } +.cw-typeradio--row .cw-typeradio-item:hover { + border-color: hsl(var(--foreground) / 0.18); + background: hsl(var(--foreground) / 0.03); + color: hsl(var(--foreground)); +} .cw-typeradio-item.is-on { - border-color: hsl(var(--primary)); - background: hsl(var(--primary) / 0.05); + border-color: transparent; + background: hsl(var(--background)); + box-shadow: + 0 0 0 1px hsl(var(--foreground) / 0.12), + 0 1px 2px hsl(var(--foreground) / 0.08); +} +.cw-typeradio--row .cw-typeradio-item.is-on { + border-color: hsl(var(--foreground) / 0.26); + background: hsl(var(--foreground) / 0.055); + color: hsl(var(--foreground)); + box-shadow: none; } /* Visually-hidden native input keeps keyboard/a11y behavior. */ .cw-typeradio-input { @@ -1127,6 +1416,9 @@ border: 2px solid hsl(var(--border)); transition: border-color 0.12s; } +.cw-typeradio--row .cw-typeradio-dot { + display: none; +} .cw-typeradio-item.is-on .cw-typeradio-dot { border-color: hsl(var(--primary)); background: radial-gradient( @@ -1144,25 +1436,55 @@ flex-direction: column; gap: 5px; } +.cw-typeradio--row .cw-typeradio-main { + flex: 0 1 auto; + min-width: 0; + max-width: 100%; +} .cw-typeradio-head { display: flex; align-items: center; gap: 7px; } +.cw-typeradio--row .cw-typeradio-head { + flex-wrap: nowrap; + justify-content: center; + min-width: 0; + max-width: 100%; + gap: 6px; +} .cw-typeradio-icon { flex-shrink: 0; width: 16px; height: 16px; color: hsl(var(--muted-foreground)); } +.cw-typeradio--row .cw-typeradio-icon { + width: 16px; + height: 16px; + color: currentColor; +} .cw-typeradio-item.is-on .cw-typeradio-icon { color: hsl(var(--foreground)); } +.cw-typeradio--row .cw-typeradio-item.is-on .cw-typeradio-icon { + color: currentColor; +} .cw-typeradio-title { font-size: 13.5px; font-weight: 600; color: hsl(var(--foreground)); } +.cw-typeradio--row .cw-typeradio-title { + min-width: 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + color: inherit; + font-size: 13.5px; + font-weight: 650; + line-height: 1.25; +} .cw-typeradio-desc { font-size: 11.5px; line-height: 1.5; @@ -1562,6 +1884,37 @@ } /* ---------- responsive ---------- */ +@media (max-width: 1280px) { + .cw-debug { + width: 280px; + } + .cw-tree { + width: 220px; + } + .cw-rail { + width: 160px; + } +} + +@media (max-width: 1080px) { + .cw-editor { + flex-wrap: wrap; + overflow-y: auto; + } + .cw-tree { + height: 260px; + } + .cw-detail { + min-height: 560px; + } + .cw-debug { + width: 100%; + min-height: 360px; + border-left: none; + border-top: 1px solid hsl(var(--border)); + } +} + @media (max-width: 860px) { /* Drop the rail; the form is the single source of navigation by scrolling. */ .cw-rail { diff --git a/frontend/src/create/CustomCreate.tsx b/frontend/src/create/CustomCreate.tsx index 0d0a4ec2..70a87920 100644 --- a/frontend/src/create/CustomCreate.tsx +++ b/frontend/src/create/CustomCreate.tsx @@ -3,6 +3,7 @@ import { AnimatePresence, motion } from "motion/react"; import { ArrowLeft, Bot, + Bug, Boxes, Check, Cloud, @@ -21,6 +22,8 @@ import { Plus, Repeat, Rocket, + Search, + Send, Shapes, Sparkles, Split, @@ -43,18 +46,24 @@ import { TRACING_EXPORTERS, type BackendOption, } from "./veadkCatalog"; -import { generateProject } from "./codegen"; import { draftToYaml, yamlToDraft } from "./configYaml"; import type { AgentProject } from "./project"; import type { SkillSource } from "./skills/types"; -import { downloadSkillHubSkill } from "./skills/skillhub"; -import { downloadSkillSpaceSkill } from "./skills/skillspace"; import { SkillHubPicker } from "./SkillHubPicker"; import { LocalPicker } from "./LocalPicker"; import { SkillSpacePicker } from "./SkillSpacePicker"; import { ProjectPreview } from "../ui/ProjectPreview"; -import { deployAgentkitProject } from "../adk/client"; -import type { DeployStage } from "../adk/client"; +import { Blocks } from "../ui/Blocks"; +import { + createGeneratedAgentTestRun, + createGeneratedAgentTestSession, + deleteGeneratedAgentTestRun, + deployAgentkitProject, + generateAgentProject, + runGeneratedAgentTestSSE, +} from "../adk/client"; +import type { DeployStage, GeneratedAgentTestRun, UiFeatures } from "../adk/client"; +import { applyEvent, emptyAcc, type Block } from "../blocks"; import "./CustomCreate.css"; /** Trigger a browser download of a text file. */ @@ -367,6 +376,9 @@ function McpToolEditor({ }) } /> +

+ stdio MCP 暂不参与调试运行;点击“生成项目”时会完整保留这项配置并生成对应代码。 +

)} @@ -815,6 +827,231 @@ function TreeNode({ ); } +type DebugPhase = "idle" | "building" | "starting" | "ready" | "sending" | "error"; + +interface DebugMessage { + role: "user" | "assistant"; + content: string; + blocks?: Block[]; + error?: string; +} + +function fmtDebugExpiry(expiresAt?: number): string { + if (!expiresAt) return ""; + const ms = expiresAt * 1000 - Date.now(); + if (ms <= 0) return "即将清理"; + const minutes = Math.floor(ms / 60000); + const seconds = Math.floor((ms % 60000) / 1000); + return `${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")} 后自动清理`; +} + +function debugSnapshotKey(draft: AgentDraft): string { + return JSON.stringify(draft); +} + +function DebugPanel({ + enabled, + disabledReason, + phase, + stale, + run, + projectName, + logs, + messages, + input, + error, + onInput, + onSend, + onRestart, +}: { + enabled: boolean; + disabledReason: string; + phase: DebugPhase; + stale: boolean; + run: GeneratedAgentTestRun | null; + projectName: string; + logs: string[]; + messages: DebugMessage[]; + input: string; + error: string | null; + onInput: (v: string) => void; + onSend: () => void; + onRestart: () => void; +}) { + const [, refreshExpiry] = useState(0); + const ready = phase === "ready" || phase === "sending"; + const busy = phase === "building" || phase === "starting" || phase === "sending"; + const badgeState = stale ? "stale" : phase; + const statusText = stale + ? "配置已变更" + : !enabled + ? "未开启" + : phase === "idle" + ? "等待调试" + : phase === "building" + ? "生成代码中" + : phase === "starting" + ? "启动环境中" + : phase === "sending" + ? "运行中" + : phase === "error" + ? "调试失败" + : "可对话"; + + useEffect(() => { + if (!run) return; + const timer = window.setInterval(() => refreshExpiry((v) => v + 1), 1000); + return () => window.clearInterval(timer); + }, [run]); + + return ( +