diff --git a/frontend/README.md b/frontend/README.md index 84e642ca5..7e2c00b8f 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -42,8 +42,21 @@ server that `veadk frontend` launches — no separate backend. the normal conversation renderer; leaving the conversation only disconnects it, so the Agent remains available until the user deletes it. OpenClaw and Hermes expose their main interface and Terminal through Studio. -- **AgentKit Skill center**: browse Skill Spaces and their skills with - server-side pagination by region, then inspect the selected Skill content. +- **AgentKit Skill center**: browse Skill Spaces and complete Skill packages + with server-side pagination by region. A new-chat-style composer starts + create or optimize conversations directly from the center, using either a + selected Skill or an uploaded ZIP as the optimization source. Skill + conversations share the normal sidebar history, preserve DevEnv progress + across navigation, and expose the complete generated file tree in a + read-only browser. Cloud deployment binds the active provider's hosted model + credential to both the chat CodeEnv and Skill Workbench DevEnv. Studio uses + the VeFaaS IAM role's temporary credentials for AgentKit control-plane calls; + deployer AK/SK are not copied into the function environment. Skill sources + and publication regions come from the active provider (Beijing/Shanghai for + Volcengine and Singapore for BytePlus). Publishing streams each AgentKit + stage, requires a concrete Skill Space destination, and links the completed + version back to that destination in the center. Preview, ZIP download, and + publishing are bound to the same immutable revision and content digest. - **Automation directory**: browse development and message-channel integrations from the Studio sidebar. The local Coding Agents integration detects Trae, Claude Code, and Codex across macOS, Linux, and Windows, then globally installs diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 0bece1ffc..416483458 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -14,6 +14,7 @@ "@codemirror/lang-markdown": "^6.5.1", "@codemirror/lang-python": "^6.2.1", "@codemirror/lang-yaml": "^6.1.3", + "@codemirror/legacy-modes": "^6.5.3", "@dagrejs/dagre": "^3.0.0", "@mdxeditor/editor": "^4.1.0", "@openai/apps-sdk-ui": "^0.2.2", diff --git a/frontend/package.json b/frontend/package.json index 16750f3f5..5da91704d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -17,6 +17,7 @@ "@codemirror/lang-markdown": "^6.5.1", "@codemirror/lang-python": "^6.2.1", "@codemirror/lang-yaml": "^6.1.3", + "@codemirror/legacy-modes": "^6.5.3", "@dagrejs/dagre": "^3.0.0", "@mdxeditor/editor": "^4.1.0", "@openai/apps-sdk-ui": "^0.2.2", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 6a645023c..3ce6cbd30 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -79,6 +79,9 @@ import { import { Sidebar, type SidebarPage } from "./ui/Sidebar"; import { AgentInfoPanel } from "./ui/AgentTopology"; import { SkillCenterView } from "./ui/SkillCenter"; +import { SkillWorkbench } from "./ui/skill-workbench/SkillWorkbench"; +import { useSkillWorkbenchTasks } from "./ui/skill-workbench/useSkillWorkbenchTasks"; +import type { SkillWorkbenchPublishResult } from "./ui/skill-workbench/types"; import { AddAgentKitView } from "./ui/AddAgentKit"; import { AgentWorkspace } from "./ui/AgentWorkspace"; import { @@ -1145,6 +1148,13 @@ export default function App() { // flashing the notice in the common, configured case). const [hasCreds, setHasCreds] = useState(true); const [skillCenter, setSkillCenter] = useState(false); + const [skillWorkbenchOpen, setSkillWorkbenchOpen] = useState(false); + const [skillCenterFocus, setSkillCenterFocus] = + useState(null); + const skillWorkbenchTasks = useSkillWorkbenchTasks( + features.skillCenter !== false, + userId, + ); const [addAgent, setAddAgent] = useState(false); // The "添加 Agent" chooser (two cards: AgentKit / 从 0 快速创建). const [addMenu, setAddMenu] = useState(false); @@ -1203,7 +1213,7 @@ export default function App() { appName?: string; currentVersion?: number | null; } | null>(null); - const [newRuntimeRegion, setNewRuntimeRegion] = useState( + const [newRuntimeRegion, setNewRuntimeRegion] = useState( defaultCloudRegion(cloudProvider), ); const [focusedDeploymentTaskId, setFocusedDeploymentTaskId] = useState(""); @@ -1733,6 +1743,7 @@ export default function App() { setAppName(""); setCreateView(null); setSkillCenter(false); + setSkillWorkbenchOpen(false); setAddAgent(false); setAddMenu(false); setSearchView(false); @@ -3015,6 +3026,7 @@ export default function App() { setPlatformFeedbackOrigin(null); setCreateView(null); setSkillCenter(false); + setSkillWorkbenchOpen(false); setAddAgent(false); setAddMenu(false); setSearchView(false); @@ -3997,6 +4009,7 @@ export default function App() { setManageAgents(false); setCreateView(null); setSkillCenter(false); + setSkillWorkbenchOpen(false); setAddAgent(false); setAddMenu(false); setSearchView(false); @@ -4192,10 +4205,18 @@ export default function App() { ? "search" : myAgents || manageAgents || sandboxAgentDetailTarget || sandboxAgentWorkspace ? "agents" - : sessionId || createView || skillCenter || addAgent || addMenu + : sessionId || createView || skillCenter || skillWorkbenchOpen || addAgent || addMenu ? null : "new-chat"; + const deleteSkillConversation = async (jobId: string) => { + await skillWorkbenchTasks.deleteTask(jobId); + if (skillWorkbenchTasks.activeJobId === jobId) { + setSkillWorkbenchOpen(false); + setSkillCenter(true); + } + }; + return (
void skillWorkbenchTasks.refreshTasks()} + onDeleteSkillConversation={deleteSkillConversation} + onOpenSkillConversation={(jobId) => { + if (sandboxSession) exitSandboxSession(); + viewSidRef.current = ""; + setSessionId(""); + setCreateView(null); + setAddAgent(false); + setAddMenu(false); + setSearchView(false); + setManageAgents(false); + setAgentDetailTarget(null); + setSandboxAgentDetailTarget(null); + setSandboxAgentWorkspace(null); + setMyAgents(false); + setApplicationsView(null); + setSkillCenter(false); + skillWorkbenchTasks.selectTask(jobId); + setSkillWorkbenchOpen(true); + setError(""); + }} onNewChat={openNewChat} onSearch={() => { setPlatformFeedbackOrigin(null); if (sandboxSession) exitSandboxSession(); setCreateView(null); setSkillCenter(false); + setSkillWorkbenchOpen(false); setAddAgent(false); setAddMenu(false); setManageAgents(false); @@ -4235,6 +4282,7 @@ export default function App() { viewSidRef.current = ""; setSessionId(""); setSkillCenter(false); + setSkillWorkbenchOpen(false); setAddAgent(false); setSearchView(false); setManageAgents(false); @@ -4261,6 +4309,8 @@ export default function App() { setSandboxAgentWorkspace(null); setMyAgents(false); setApplicationsView(null); + setSkillWorkbenchOpen(false); + setSkillCenterFocus(null); setSkillCenter(true); setError(""); }} @@ -4303,6 +4353,7 @@ export default function App() { setPlatformFeedbackOrigin(null); setCreateView(null); setSkillCenter(false); + setSkillWorkbenchOpen(false); setAddAgent(false); setAddMenu(false); setSearchView(false); @@ -4820,8 +4871,48 @@ export default function App() { }} onCancel={() => setAddAgent(false)} /> + ) : skillWorkbenchOpen ? ( + void skillWorkbenchTasks.refreshActiveTask()} + onRetryArtifact={() => { + void skillWorkbenchTasks.refreshActiveArtifact().catch(() => undefined); + }} + onBack={() => { + setSkillWorkbenchOpen(false); + setSkillCenter(true); + }} + onViewPublished={(result) => { + setSkillCenterFocus(result); + setSkillWorkbenchOpen(false); + setSkillCenter(true); + }} + /> ) : skillCenter ? ( - + setSkillCenterFocus(null)} + onStartTask={(args) => { + skillWorkbenchTasks.clearActiveTask(); + setSkillCenterFocus(null); + const pending = skillWorkbenchTasks.startTask(args); + setSkillCenter(false); + setSkillWorkbenchOpen(true); + return pending; + }} + /> ) : visibleCreateView !== null && !hasCreds ? (
= new Set( + [...VOLCENGINE_REGIONS, ...BYTEPLUS_REGIONS].map((option) => option.value), +); export function cloudRegionOptions(provider: CloudProvider): CloudRegionOption[] { return provider === "byteplus" ? BYTEPLUS_REGIONS : VOLCENGINE_REGIONS; } -export function defaultCloudRegion(provider: CloudProvider): string { +export function defaultCloudRegion(provider: CloudProvider): CloudRegion { return cloudRegionOptions(provider)[0]?.value || VOLCENGINE_DEFAULT_REGION; } +export function isSupportedCloudRegion(value: unknown): value is CloudRegion { + return typeof value === "string" && SUPPORTED_CLOUD_REGIONS.has(value); +} + export function formatCloudRegion(region: string, provider?: CloudProvider): string { const options = provider ? cloudRegionOptions(provider) diff --git a/frontend/src/create/skills/skillspace.ts b/frontend/src/create/skills/skillspace.ts index 63fa04835..6eec30141 100644 --- a/frontend/src/create/skills/skillspace.ts +++ b/frontend/src/create/skills/skillspace.ts @@ -49,6 +49,11 @@ export interface SkillSpacePageOptions { project?: string; } +export function formatSkillVersion(version?: string): string { + const normalized = (version || "").trim().replace(/^v+/i, ""); + return normalized ? `v${normalized}` : "—"; +} + async function jfetch(url: string): Promise { const res = await fetch(url, { headers: { accept: "application/json" }, diff --git a/frontend/src/styles.css b/frontend/src/styles.css index e06cbbd26..225e402ff 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -847,6 +847,7 @@ body { } } +.sidebar-skill-icon { position: relative; display: inline-flex; } .sidebar-history { flex: 1; min-height: 0; display: flex; flex-direction: column; } .history-head { display: flex; @@ -877,6 +878,34 @@ body { cursor: pointer; transition: color 0.12s; } +.history-title small { + display: block; + overflow: hidden; + margin-top: 2px; + color: hsl(var(--muted-foreground)); + font-size: 10px; + font-weight: 400; + text-overflow: ellipsis; + white-space: nowrap; +} +.history-load-error { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + margin: 4px 8px; + padding: 7px 8px; + color: hsl(var(--destructive)); + font-size: 11px; +} +.history-load-error button { + border: 0; + background: transparent; + color: inherit; + font: inherit; + text-decoration: underline; + cursor: pointer; +} .history-new-chat .icon { width: 15px; height: 15px; } .history-new-chat:hover { background: transparent; @@ -4247,8 +4276,336 @@ body { min-height: 0; display: flex; flex-direction: column; + padding: 32px 32px 0; + overflow: hidden; + background: hsl(var(--background)); +} +.skillcenter-setup { + flex: 1; + min-width: 0; + min-height: 0; + display: flex; + flex-direction: column; + align-items: center; + overflow-y: auto; + padding: 24px 32px 32px; + background: hsl(var(--background)); +} +.skillcenter-setup-nav { + width: min(768px, 100%); + margin: 0 auto; +} +.skillcenter-setup-nav > button, +.skillcenter-back { + min-height: 28px; + padding: 0; + border: 0; + background: transparent; + color: hsl(var(--muted-foreground)); + font: inherit; + font-size: 12px; + cursor: pointer; +} +.skillcenter-setup-nav > button:hover, +.skillcenter-back:hover { color: hsl(var(--foreground)); } +.skillcenter-chat-intro { + width: 100%; + max-width: 768px; + margin: clamp(64px, 12vh, 124px) auto 24px; + text-align: center; +} +.skillcenter-chat-intro h1 { + margin: 0; + font-size: 26px; + font-weight: 600; + line-height: 1.2; +} +.skillcenter-chat-intro p { + margin: 10px 0 0; + color: hsl(var(--muted-foreground)); + font-size: 13.5px; + line-height: 1.6; +} +.skillcenter-composer-upload:focus-within, +.skillcenter-source-chip:focus-visible, +.skillcenter-source-picker:focus-visible { + outline: 2px solid hsl(var(--ring) / 0.34); + outline-offset: 1px; +} +.skillcenter-setup-composer.composer { + width: 100%; + max-width: 768px; + padding: 0; +} +.skillcenter-setup-composer.composer--new-chat .composer-box { + min-height: 152px; + border-radius: 16px; +} +.skillcenter-setup-composer.composer--new-chat .composer-input-stack { + min-height: 108px; +} +.skillcenter-setup-composer.composer--new-chat .comp-input { + min-height: 82px; + padding-top: 4px; +} +.skillcenter-setup-composer.composer--new-chat .comp-input::placeholder { + color: hsl(var(--muted-foreground)); +} +.skillcenter-context-attachment { + min-height: 30px; + display: flex; + align-items: center; + gap: 8px; + padding: 0 10px 5px; + color: hsl(var(--muted-foreground)); + font-size: 12px; +} +.skillcenter-context-attachment > span { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.skillcenter-source-picker { + min-height: 28px; + flex: 0 0 auto; + padding: 0 9px; + border: 1px solid hsl(var(--border)); + border-radius: 7px; + background: hsl(var(--panel)); + color: hsl(var(--foreground)); + font: inherit; + font-size: 11.5px; + cursor: pointer; +} +.skillcenter-source-picker:hover { background: hsl(var(--muted)); } +.skillcenter-source-chip { + max-width: min(420px, calc(100% - 40px)); + height: 28px; + display: inline-flex; + align-items: center; + gap: 6px; + padding: 0 8px; + border: 1px solid hsl(var(--border)); + border-radius: 7px; + background: hsl(var(--secondary)); + color: hsl(var(--foreground)); + font: inherit; + font-size: 12px; +} +.skillcenter-source-chip > svg { + width: 14px; + height: 14px; + flex: 0 0 auto; +} +.skillcenter-source-chip > span { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.skillcenter-source-chip > small { + flex: 0 0 auto; + color: hsl(var(--muted-foreground)); + font-size: 10.5px; +} +.skillcenter-composer-upload { + position: absolute; + bottom: 10px; + left: 10px; + height: 36px; + display: inline-flex; + align-items: center; + gap: 6px; + padding: 0 10px; + border-radius: 8px; + color: hsl(var(--muted-foreground)); + font-size: 12.5px; + cursor: pointer; +} +.skillcenter-composer-upload:hover { + background: hsl(var(--foreground) / 0.045); + color: hsl(var(--foreground)); +} +.skillcenter-composer-upload .icon { + width: 16px; + height: 16px; +} +.skillcenter-composer-upload input { + position: absolute; + width: 1px; + height: 1px; + opacity: 0; +} +.skillcenter-composer-error { + width: min(768px, 100%); + margin: 8px auto 0; + color: hsl(var(--destructive)); + font-size: 12px; + line-height: 1.5; +} +.skillcenter-upload-limit { + width: min(768px, 100%); + margin: 8px auto 0; + color: hsl(var(--muted-foreground)); + font-size: 11.5px; + line-height: 1.5; +} +.skillcenter-page-head { + min-height: 58px; + flex: 0 0 auto; + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 24px; padding: 0; } +.skillcenter-page-head > div:first-child { min-width: 0; } +.skillcenter-page-head h1 { + margin: 0; + overflow: hidden; + font-size: 21px; + font-weight: 650; + line-height: 1.3; + text-overflow: ellipsis; + white-space: nowrap; +} +.skillcenter-back + h1 { margin-top: 4px; } +.skillcenter-page-head p { + margin: 6px 0 0; + overflow-wrap: anywhere; + color: hsl(var(--muted-foreground)); + font-size: 13px; + line-height: 1.5; +} +.skillcenter-page-actions { + display: flex; + flex: 0 0 auto; + align-items: center; + gap: 8px; +} +.skillcenter-create-action, +.skillcenter-optimize-action, +.skillcenter-primary-action { + min-height: 34px; + padding: 0 14px; + border: 1px solid hsl(var(--border)); + border-radius: 7px; + background: hsl(var(--panel)); + color: hsl(var(--foreground)); + font: inherit; + font-size: 12.5px; + cursor: pointer; +} +.skillcenter-create-action, +.skillcenter-primary-action { + border-color: hsl(var(--foreground)); + background: hsl(var(--foreground)); + color: hsl(var(--background)); +} +.skillcenter-primary-action { + flex: 0 0 auto; + white-space: nowrap; +} +.skillcenter-selection-upload { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 6px; + white-space: nowrap; +} +.skillcenter-selection-upload .icon { + width: 15px; + height: 15px; +} +.skillcenter-selection-upload input { + position: absolute; + width: 1px; + height: 1px; + opacity: 0; +} +.skillcenter-selection-upload:focus-within { + outline: 2px solid hsl(var(--ring) / 0.34); + outline-offset: 1px; +} +.skillcenter-selection-upload.is-disabled { + cursor: not-allowed; + opacity: .46; + pointer-events: none; +} +.skillcenter-create-action:hover:not(:disabled), +.skillcenter-primary-action:hover:not(:disabled) { + border-color: hsl(var(--foreground) / .84); + background: hsl(var(--foreground) / .84); +} +.skillcenter-optimize-action:hover:not(:disabled) { background: hsl(var(--muted)); } +.skillcenter-create-action:disabled, +.skillcenter-optimize-action:disabled { + cursor: not-allowed; + opacity: .46; +} +.skillcenter-capability-notice { + min-height: 38px; + flex: 0 0 auto; + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + margin-top: 14px; + padding: 8px 10px; + border: 1px solid hsl(var(--border)); + border-radius: 7px; + background: hsl(var(--panel)); + color: hsl(var(--muted-foreground)); + font-size: 12px; + line-height: 1.5; +} +.skillcenter-capability-notice.is-error { + border-color: hsl(var(--destructive) / .2); + background: hsl(var(--destructive) / .06); + color: hsl(var(--destructive)); +} +.skillcenter-capability-notice > span { + min-width: 0; + overflow-wrap: anywhere; +} +.skillcenter-capability-notice button { + min-height: 28px; + flex: 0 0 auto; + padding: 0 9px; + border: 1px solid hsl(var(--border)); + border-radius: 6px; + background: hsl(var(--panel)); + color: hsl(var(--foreground)); + font: inherit; + font-size: 11.5px; + cursor: pointer; +} +.skillcenter-capability-notice button:hover { background: hsl(var(--muted)); } +.skillcenter-capability-notice button:focus-visible { + outline: 2px solid hsl(var(--ring) / 0.28); + outline-offset: 1px; +} +.skillcenter-capability-notice + .skillcenter-toolbar { margin-top: 10px; } +.skillcenter-toolbar { + min-height: 54px; + flex: 0 0 auto; + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + margin-top: 18px; +} +.skillcenter-toolbar > span { + color: hsl(var(--muted-foreground)); + font-size: 11.5px; +} +.skill-detail-actions { + display: flex; + flex: 0 0 auto; + align-items: center; + gap: 8px; +} .skillcenter-regions { display: grid; padding: 2px; @@ -4256,6 +4613,8 @@ body { background: hsl(var(--canvas) / 0.62); } .skillcenter-regions button { + min-width: 52px; + height: 32px; border: 0; background: transparent; color: hsl(var(--muted-foreground)); @@ -4280,20 +4639,139 @@ body { background: hsl(var(--muted) / 0.62); } .skillcenter-regions { - grid-template-columns: repeat(2, 58px); + grid-template-columns: repeat(2, 52px); border-radius: 7px; } .skillcenter-regions button { - height: 27px; border-radius: 5px; + font-size: 12px; +} +.skillcenter-results { + flex: 1; + min-height: 0; + display: flex; + flex-direction: column; + overflow-y: auto; + overscroll-behavior: contain; + scrollbar-gutter: stable; + padding-bottom: 28px; +} +.skillcenter-space-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(min(280px, 100%), 1fr)); + align-content: start; + gap: 12px; + padding: 2px 0 20px; +} +.skillcenter-skill-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(min(280px, 100%), 1fr)); + align-content: start; + gap: 12px; + padding: 2px 0 20px; +} +.skillcenter-space-card, +.skillcenter-skill-card { + min-width: 0; + min-height: 196px; + box-sizing: border-box; + overflow: hidden; + border: 1px solid hsl(var(--border)); + border-radius: 8px; + background: hsl(var(--panel)); + color: hsl(var(--foreground)); + box-shadow: 0 1px 2px hsl(var(--foreground) / .035); +} +.skillcenter-space-card { + display: flex; + flex-direction: column; + padding: 16px; + border-radius: 8px; +} +.skillcenter-skill-card { + display: flex; + flex-direction: column; + align-items: stretch; + padding: 16px; + font: inherit; + text-align: left; + cursor: pointer; +} +.skillcenter-space-card:hover, +.skillcenter-skill-card:hover { + border-color: hsl(var(--foreground) / .2); +} +.skillcenter-card-title-row { + min-width: 0; + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 12px; +} +.skillcenter-space-card .skillcenter-item-body { + margin-top: 12px; +} +.skillcenter-space-card h2, +.skillcenter-space-card p { margin-right: 0; margin-left: 0; } +.skillcenter-space-card footer { + display: flex; + justify-content: flex-end; + margin-top: auto; + padding-top: 16px; +} +.skillcenter-space-open { + min-height: 30px; + padding: 0 10px; + border: 1px solid hsl(var(--border)); + border-radius: 7px; + background: hsl(var(--panel)); + color: hsl(var(--foreground)); + font: inherit; font-size: 11.5px; + cursor: pointer; +} +.skillcenter-space-open:hover { background: hsl(var(--muted)); } +.skillcenter-skill-card .skillcenter-item-title { + margin-top: 12px; +} +.skillcenter-skill-card .skillcenter-item-description { + min-height: 40px; + margin-top: 6px; +} +.skillcenter-skill-card .skillcenter-item-meta { + justify-content: space-between; + margin-top: auto; + padding-top: 16px; +} +.skillcenter-card-link { + flex: 0 0 auto; + color: hsl(var(--foreground)); + font-size: 11.5px; + font-weight: 600; +} +.skillcenter-results > .skillcenter-pager { + width: 100%; + box-sizing: border-box; + margin-top: 0; + border: 1px solid hsl(var(--border)); + border-radius: 8px; + background: hsl(var(--panel)); +} +.skillcenter-selection-error { + margin: 0 0 12px; + padding: 9px 11px; + border-radius: 7px; + background: hsl(var(--destructive) / .07); + color: hsl(var(--destructive)); + font-size: 12px; + line-height: 1.5; } .skillcenter-browser { flex: 1; min-width: 0; min-height: 0; display: grid; - grid-template-columns: minmax(270px, 0.9fr) minmax(360px, 1.35fr); + grid-template-columns: minmax(300px, .72fr) minmax(0, 1.78fr); gap: 0; } .skillcenter-panel { @@ -4514,6 +4992,21 @@ body { overflow-wrap: anywhere; } .skillcenter-error { color: hsl(var(--destructive)); } +.skillcenter-error { + flex-direction: column; +} +.skillcenter-error button { + min-height: 30px; + padding: 0 10px; + border: 1px solid hsl(var(--border)); + border-radius: 7px; + background: hsl(var(--panel)); + color: hsl(var(--foreground)); + font: inherit; + font-size: 11.5px; + cursor: pointer; +} +.skillcenter-error button:hover { background: hsl(var(--muted)); } .skillcenter-loading--overlay { position: absolute; inset: 0; @@ -4607,6 +5100,10 @@ body { white-space: nowrap; } .skill-detail-content { flex: 1; min-height: 0; display: flex; flex-direction: column; } +.skill-detail-content > .code-browser-workspace { + flex: 1; + min-height: 0; +} .skill-detail-content-title { height: 40px; flex: 0 0 40px; @@ -4629,19 +5126,17 @@ body { } @media (max-width: 760px) { - .skillcenter-browser { - grid-template-columns: minmax(0, 1fr); - grid-template-rows: repeat(2, minmax(0, 1fr)); - } - .skillcenter-panel + .skillcenter-panel { - border-top: 1px solid hsl(var(--border)); - border-left: 0; - } + .skillcenter { padding: 22px 18px 0; } + .skillcenter-setup { padding: 22px 18px; } + .skillcenter-page-head { align-items: stretch; flex-direction: column; gap: 16px; } + .skillcenter-page-actions { width: 100%; } + .skillcenter-page-actions button, + .skillcenter-page-actions label { flex: 1; } + .skillcenter-toolbar { margin-top: 14px; } .skill-detail-meta { grid-template-columns: repeat(2, minmax(0, 1fr)); } } @media (max-width: 560px) { - .skillcenter-regions { grid-template-columns: repeat(2, 46px); } - .skillcenter-browser { gap: 8px; } + .skillcenter-regions { grid-template-columns: repeat(2, 52px); } .skillcenter-item-meta { gap: 4px 6px; } .skillcenter-meta-text { max-width: 132px; } .skill-detail-backdrop { padding: 8px; } diff --git a/frontend/src/ui/CodeBrowserDialog.css b/frontend/src/ui/CodeBrowserDialog.css index c04c7c4cc..746f6719b 100644 --- a/frontend/src/ui/CodeBrowserDialog.css +++ b/frontend/src/ui/CodeBrowserDialog.css @@ -230,6 +230,17 @@ white-space: nowrap; } +.code-browser-path .code-browser-readonly { + flex: 0 0 auto; + margin-left: auto; + padding: 2px 6px; + border-radius: 999px; + background: hsl(var(--muted)); + color: hsl(var(--muted-foreground)); + font-family: inherit; + font-size: 10px; +} + .code-browser-main { flex: 1; min-width: 0; @@ -258,6 +269,17 @@ overflow: hidden; } +.code-browser-editor > .code-browser-markdown { + height: 100%; + box-sizing: border-box; + overflow: auto; + overflow-wrap: anywhere; + padding: 20px 24px 32px; +} + +.code-browser-markdown > :first-child { margin-top: 0; } +.code-browser-markdown > :last-child { margin-bottom: 0; } + .code-browser-editor > div, .code-browser-editor .cm-theme, .code-browser-editor .cm-editor { diff --git a/frontend/src/ui/CodeBrowserDialog.tsx b/frontend/src/ui/CodeBrowserDialog.tsx index b088621f5..b70129e55 100644 --- a/frontend/src/ui/CodeBrowserDialog.tsx +++ b/frontend/src/ui/CodeBrowserDialog.tsx @@ -16,6 +16,7 @@ import { } from "lucide-react"; import type { AgentProject, ProjectFile } from "../create/project"; import "./CodeBrowserDialog.css"; +import { Markdown } from "./Markdown"; const CodeEditor = lazy(() => import("./CodeEditor")); @@ -52,6 +53,16 @@ function sortedChildren(node: TreeNode): TreeNode[] { }); } +function markdownPreviewText(content: string): string { + const frontmatter = content.match( + /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/, + ); + if (!frontmatter || !/^[A-Za-z_][\w.-]*\s*:/m.test(frontmatter[1])) { + return content; + } + return content.slice(frontmatter[0].length); +} + export interface CodeBrowserDialogProps { project: AgentProject; open: boolean; @@ -59,43 +70,34 @@ export interface CodeBrowserDialogProps { onChange: (project: AgentProject) => void; } -/** Browse and edit generated project files without leaving the deploy view. */ -export function CodeBrowserDialog({ +export interface CodeBrowserWorkspaceProps { + project: AgentProject; + onChange?: (project: AgentProject) => void; + readOnly?: boolean; + renderMarkdown?: boolean; + className?: string; +} + +/** Reusable file tree and editor surface for editable projects and read-only artifacts. */ +export function CodeBrowserWorkspace({ project, - open, - onClose, onChange, -}: CodeBrowserDialogProps) { + readOnly = false, + renderMarkdown = false, + className = "", +}: CodeBrowserWorkspaceProps) { const [selected, setSelected] = useState( project.files[0]?.path ?? null, ); const [collapsed, setCollapsed] = useState>(new Set()); - const closeButtonRef = useRef(null); const tree = useMemo(() => buildTree(project.files), [project.files]); const selectedFile = project.files.find((file) => file.path === selected) ?? null; - useEffect(() => { - if (!open) return; - const previousOverflow = document.body.style.overflow; - document.body.style.overflow = "hidden"; - closeButtonRef.current?.focus(); - const handleKeyDown = (event: KeyboardEvent) => { - if (event.key === "Escape") onClose(); - }; - window.addEventListener("keydown", handleKeyDown); - return () => { - document.body.style.overflow = previousOverflow; - window.removeEventListener("keydown", handleKeyDown); - }; - }, [onClose, open]); - useEffect(() => { if (selectedFile || project.files.length === 0) return; setSelected(project.files[0].path); }, [project.files, selectedFile]); - if (!open) return null; - function toggleFolder(key: string) { setCollapsed((previous) => { const next = new Set(previous); @@ -148,7 +150,7 @@ export function CodeBrowserDialog({ } function handleEdit(content: string) { - if (!selectedFile) return; + if (!selectedFile || readOnly || !onChange) return; onChange({ ...project, files: project.files.map((file) => @@ -157,6 +159,82 @@ export function CodeBrowserDialog({ }); } + return ( +
+ + +
+
+
+
+ {selectedFile ? ( + renderMarkdown && /\.md(?:own)?$/i.test(selectedFile.path) ? ( + + ) : ( + 正在加载编辑器…
} + > + + + ) + ) : ( +
从左侧选择文件以查看内容
+ )} +
+ +
+ ); +} + +/** Browse and edit generated project files without leaving the deploy view. */ +export function CodeBrowserDialog({ + project, + open, + onClose, + onChange, +}: CodeBrowserDialogProps) { + const closeButtonRef = useRef(null); + + useEffect(() => { + if (!open) return; + const previousOverflow = document.body.style.overflow; + document.body.style.overflow = "hidden"; + closeButtonRef.current?.focus(); + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === "Escape") onClose(); + }; + window.addEventListener("keydown", handleKeyDown); + return () => { + document.body.style.overflow = previousOverflow; + window.removeEventListener("keydown", handleKeyDown); + }; + }, [onClose, open]); + + if (!open) return null; + return createPortal(
-
- - -
-
-
-
- {selectedFile ? ( - 正在加载编辑器…
} - > - - - ) : ( -
从左侧选择文件以查看代码
- )} -
- -
+
, document.body, diff --git a/frontend/src/ui/CodeEditor.tsx b/frontend/src/ui/CodeEditor.tsx index 4afa53d4f..4ecca473c 100644 --- a/frontend/src/ui/CodeEditor.tsx +++ b/frontend/src/ui/CodeEditor.tsx @@ -1,16 +1,19 @@ import { useMemo } from "react"; +import { StreamLanguage } from "@codemirror/language"; import type { Extension } from "@codemirror/state"; import { javascript } from "@codemirror/lang-javascript"; import { json } from "@codemirror/lang-json"; import { markdown } from "@codemirror/lang-markdown"; import { python } from "@codemirror/lang-python"; import { yaml } from "@codemirror/lang-yaml"; +import { shell } from "@codemirror/legacy-modes/mode/shell"; import CodeMirror from "@uiw/react-codemirror"; interface CodeEditorProps { value: string; path: string; onChange: (value: string) => void; + readOnly?: boolean; } function languageFor(path: string): Extension[] { @@ -28,14 +31,24 @@ function languageFor(path: string): Extension[] { if (extension === "json" || extension === "jsonc") return [json()]; if (extension === "yaml" || extension === "yml") return [yaml()]; if (["md", "markdown"].includes(extension ?? "")) return [markdown()]; + if (["sh", "bash", "zsh"].includes(extension ?? "")) { + return [StreamLanguage.define(shell)]; + } return []; } -export default function CodeEditor({ value, path, onChange }: CodeEditorProps) { +export default function CodeEditor({ + value, + path, + onChange, + readOnly = false, +}: CodeEditorProps) { const extensions = useMemo(() => languageFor(path), [path]); + const editorOptions = { editable: !readOnly }; return ( ; /** Session ids whose latest reply is currently being evaluated. */ evaluatingSids?: Set; + skillConversations?: SkillWorkbenchTaskListItem[]; + skillConversationsLoading?: boolean; + skillConversationsError?: string; + activeSkillConversationId?: string; + onOpenSkillConversation: (jobId: string) => void; + onDeleteSkillConversation: (jobId: string) => Promise; + onRetrySkillConversations: () => void; onNewChat: () => void; onSearch: () => void; onQuickCreate: () => void; @@ -87,6 +97,68 @@ export interface SidebarProps { onLogout: () => void; } +type SidebarConversation = + | { + kind: "session"; + id: string; + updatedAt: number; + session: AdkSession; + } + | { + kind: "skill"; + id: string; + updatedAt: number; + task: SkillWorkbenchTaskListItem; + }; + +export function mergeSidebarConversations( + sessions: AdkSession[], + skillConversations: SkillWorkbenchTaskListItem[], +): SidebarConversation[] { + return [ + ...sessions.map((session): SidebarConversation => ({ + kind: "session", + id: `session:${session.id}`, + updatedAt: session.lastUpdateTime ?? 0, + session, + })), + ...skillConversations.map((task): SidebarConversation => ({ + kind: "skill", + id: `skill:${task.jobId}`, + updatedAt: task.createdAt, + task, + })), + ].sort((a, b) => b.updatedAt - a.updatedAt); +} + +function skillConversationStatus(task: SkillWorkbenchTaskListItem): string { + if (task.state === "provisioning") return "准备 DevEnv"; + if (task.state === "running") { + if (task.stage === "validating") return "校验中"; + if (task.stage === "packaging") return "打包中"; + return "生成中"; + } + if (task.state === "ready" || task.state === "published") return "已完成"; + if (task.state === "failed") return "失败"; + if (task.state === "expired") return "DevEnv 已释放"; + return "已结束"; +} + +function skillConversationTitle(task: SkillWorkbenchTaskListItem): string { + return task.intent || + (task.operation === "create" + ? "创建 Skill" + : task.operation === "optimize" + ? "优化 Skill" + : "Skill 会话"); +} + +function skillConversationOperation(task: SkillWorkbenchTaskListItem): string { + if (task.operation === "create") return "创建"; + if (task.operation === "optimize") return "优化"; + return "Skill"; +} + /** Stable per-user blue/cyan smoke palette so avatars feel individual without flicker. */ function smokeAvatarStyle(seed: string): CSSProperties { let hash = 2166136261; @@ -289,6 +361,13 @@ export function Sidebar({ access, streamingSids, evaluatingSids, + skillConversations = [], + skillConversationsLoading = false, + skillConversationsError = "", + activeSkillConversationId = "", + onOpenSkillConversation, + onDeleteSkillConversation, + onRetrySkillConversations, onNewChat, onSearch, onQuickCreate, @@ -303,21 +382,22 @@ export function Sidebar({ version, onLogout, }: SidebarProps) { - // Creation and Skill Center live outside the #748-style sidebar. + // Agent creation still lives outside the #748-style sidebar. void onQuickCreate; - void onSkillCenter; void onAddAgent; // Per-module feature gates; a missing flag defaults to shown. const show = (k: keyof NonNullable) => features?.[k] !== false; const [menuFor, setMenuFor] = useState(null); + const [skillDeleteTarget, setSkillDeleteTarget] = + useState(null); + const [deletingSkillConversation, setDeletingSkillConversation] = useState(false); + const [skillDeleteError, setSkillDeleteError] = useState(""); const autoCollapsedRef = useRef( typeof window !== "undefined" && window.matchMedia(SIDEBAR_AUTO_COLLAPSE_QUERY).matches, ); const [collapsed, setCollapsed] = useState(autoCollapsedRef.current); - const sorted = [...sessions].sort( - (a, b) => (b.lastUpdateTime ?? 0) - (a.lastUpdateTime ?? 0), - ); + const conversations = mergeSidebarConversations(sessions, skillConversations); const toggleCollapsed = () => { autoCollapsedRef.current = false; setCollapsed((value) => !value); @@ -403,6 +483,19 @@ export function Sidebar({ 智能体 + {show("skillCenter") && ( + + )} {show("search") && ( )} @@ -424,7 +517,7 @@ export function Sidebar({ {show("history") && (
- 历史会话 + 会话 {show("newChat") && ( - {menuFor === s.id && ( + {menuFor === conversation.id && ( <>
setMenuFor(null)} />
@@ -482,7 +599,12 @@ export function Sidebar({ className="menu-item menu-item--danger" onClick={() => { setMenuFor(null); - onDeleteSession(s.id); + if (conversation.kind === "skill") { + setSkillDeleteError(""); + setSkillDeleteTarget(conversation.task); + } else { + onDeleteSession(conversation.session.id); + } }} > 删除 @@ -493,6 +615,12 @@ export function Sidebar({
); })} + {skillConversationsError ? ( +
+ {skillConversationsError} + +
+ ) : null}
)} @@ -518,6 +646,28 @@ export function Sidebar({ onLogout={onLogout} />
+ {skillDeleteTarget ? ( + setSkillDeleteTarget(null)} + onConfirm={() => { + setDeletingSkillConversation(true); + setSkillDeleteError(""); + void onDeleteSkillConversation(skillDeleteTarget.jobId) + .then(() => setSkillDeleteTarget(null)) + .catch((cause) => { + setSkillDeleteError(cause instanceof Error ? cause.message : String(cause)); + }) + .finally(() => setDeletingSkillConversation(false)); + }} + /> + ) : null} ); } diff --git a/frontend/src/ui/SkillCenter.tsx b/frontend/src/ui/SkillCenter.tsx index b5cb5f1b6..aff5ff025 100644 --- a/frontend/src/ui/SkillCenter.tsx +++ b/frontend/src/ui/SkillCenter.tsx @@ -1,5 +1,6 @@ import { useEffect, useRef, useState } from "react"; import { + formatSkillVersion, getSkillDetail, listSkillSpacesPage, listSkillsInSpacePage, @@ -13,10 +14,19 @@ import { formatCloudRegion, type CloudProvider, } from "../adk/cloudProvider"; -import { Markdown } from "./Markdown"; +import { CodeBrowserWorkspace } from "./CodeBrowserDialog"; +import { isImeCompositionEvent } from "./composerKeyboard"; +import { getSkillWorkbenchCapability } from "./skill-workbench/api"; +import type { + SkillCenterOptimizationSource, + SkillWorkbenchCapability, + SkillWorkbenchOperation, + SkillWorkbenchPublishResult, + SkillWorkbenchTask, +} from "./skill-workbench/types"; +import type { StartSkillWorkbenchTaskArgs } from "./skill-workbench/useSkillWorkbenchTasks"; -const SPACE_PAGE_SIZE = 6; -const SKILL_PAGE_SIZE = 7; +const SKILL_CENTER_PAGE_SIZE = 24; type SkillRegion = string; @@ -69,17 +79,8 @@ function updatedAtLabel(value?: string): string { }).format(date); } -function skillMarkdownBody(value: string): string { - const normalized = value.replace(/\r\n/g, "\n"); - if (!normalized.startsWith("---\n")) return value; - const closingDelimiter = normalized.indexOf("\n---\n", 4); - return closingDelimiter >= 0 - ? normalized.slice(closingDelimiter + 5).trimStart() - : value; -} - /** Hand-drawn Skill Space mark: two connected shelves for a skill collection. */ -function SkillSpaceIcon({ className = "icon" }: { className?: string }) { +export function SkillSpaceIcon({ className = "icon" }: { className?: string }) { return ( @@ -138,6 +139,7 @@ function Pager({ onPage: (page: number) => void; }) { const pageCount = Math.max(1, Math.ceil(total / pageSize)); + if (pageCount <= 1) return null; return (
共 {total} 项 @@ -158,6 +160,228 @@ function EmptyState({ children }: { children: string }) { return
{children}
; } +function ErrorState({ + message, + onRetry, +}: { + message: string; + onRetry: () => void; +}) { + return ( +
+ {message} + +
+ ); +} + +function ComposerSendIcon() { + return ( + + + + ); +} + +function ComposerUploadIcon() { + return ( + + + + ); +} + +function SkillWorkbenchSetup({ + operation, + source, + setSource, + file, + setFile, + intent, + setIntent, + capability, + busy, + error, + onError, + composerRef, + onBack, + onBrowseSources, + onStartTask, +}: { + operation: SkillWorkbenchOperation; + source: SkillCenterOptimizationSource | null; + setSource: (source: SkillCenterOptimizationSource | null) => void; + file: File | null; + setFile: (file: File | null) => void; + intent: string; + setIntent: (intent: string) => void; + capability: SkillWorkbenchCapability | null; + busy: boolean; + error: string; + onError: (message: string) => void; + composerRef: React.RefObject; + onBack: () => void; + onBrowseSources: () => void; + onStartTask?: (args: StartSkillWorkbenchTaskArgs) => Promise; +}) { + const hasSource = Boolean(source || file); + const canSubmit = Boolean( + onStartTask && + capability?.enabled && + intent.trim() && + (operation === "create" || hasSource) && + !busy, + ); + + const submit = () => { + if (!canSubmit || !onStartTask) return; + void onStartTask({ + operation, + intent: intent.trim(), + ...(source ? { source } : {}), + ...(file ? { file } : {}), + }).catch(() => { + // The persistent task controller exposes the same failure in the workbench. + }); + }; + + return ( +
+
+ +
+ +
+

+ {operation === "create" + ? "你希望创建什么 Skill?" + : "你希望如何优化这个 Skill?"} +

+

+ {operation === "create" + ? "描述目标、使用场景和期望输出。" + : "说明需要保留的能力和希望改进的结果。"} +

+
+ +
+
+ {operation === "optimize" ? ( +
+ {source ? ( + <> + + + {source.name} + {formatSkillVersion(source.version)} + + + + ) : file ? ( + + + {file.name} + + ) : ( + <> + 选择要优化的 Skill,或上传 ZIP + + + )} +
+ ) : null} +
+