From 953044593b558c10832d8b54aeb0bc74cf966deb Mon Sep 17 00:00:00 2001 From: Garming Date: Fri, 18 Sep 2026 13:47:59 +0800 Subject: [PATCH 1/2] feat(studio): make migration home a single-column workspace MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the migration side navigation with one page-level top bar and a single-column body, so the home page is the upload entry followed by recent migrations and "已迁移项目" becomes a top-bar entry. - home: upload panel plus recent migrations (five by default, expandable in place, expired environments offered as view-only) - task view and saved-project library share the same top bar and never stop a running migration when navigating away - load capabilities and migration sessions independently so a session-list failure keeps the upload entry usable, with a retry on each failure path - keep the recent-migration action column wide enough for its labels and align the empty state with the section heading --- frontend/README.md | 7 + .../src/i18n/resources/en-US/migrations.json | 22 +- .../src/i18n/resources/zh-CN/migrations.json | 22 +- .../src/migrations/MigratedProjectsPage.tsx | 6 - .../src/migrations/MigrationWorkspace.css | 189 +++-- .../src/migrations/MigrationWorkspace.tsx | 478 +++++++------ frontend/tests/migrationWorkspace.test.mjs | 47 +- .../{index--Hgqzodn.js => index-BFeVT5ks.js} | 664 +++++++++--------- ...st4ujEZ9.js => CodeDiffEditor-DliKrQrj.js} | 2 +- ...N80y.js => FileExplorerEditor-CUiU3i0Q.js} | 2 +- ...W3.js => MarkdownPromptEditor-C52MG6LY.js} | 2 +- ....js => QuickAgentCreateDialog-5r9dyB8F.js} | 2 +- .../{arc-C0aQ__bD.js => arc-0-rr3fN7.js} | 2 +- ...hannel-DRjKww3c.js => channel-CJviLJhg.js} | 2 +- ...ex.es-DdpZU-FQ.js => index.es-Bxr6oZH9.js} | 2 +- ...n-D7-71qxH.js => jspdf.es.min-D6eGuvbB.js} | 6 +- ...{linear-BxsXTKgI.js => linear-DIgatGKn.js} | 2 +- ...{index-C0EiMigV.css => index-BWtAkmyR.css} | 2 +- ...mP.js => abnfDiagram-N423BO3Z-D23TMKf3.js} | 2 +- ... architectureDiagram-T3A2C74G-GCApJrAt.js} | 2 +- ...d.js => blockDiagram-VBNYF7ZC-BO8gMzkT.js} | 2 +- ...xrJd.js => c4Diagram-5PPSVZJV-Bj_KLmuk.js} | 2 +- ...CWqXOw4U.js => chunk-2GRJ4B5K-BLJyKwvU.js} | 2 +- ...BXhNKOHQ.js => chunk-2Q5K7J3B-9AYZE35k.js} | 2 +- ...DYq5Aq4w.js => chunk-5RXB4S5H-BEPWrO5k.js} | 2 +- ...B_FqNlcE.js => chunk-5VM5RSS4-C9Fr507e.js} | 2 +- ...C_uj7jKB.js => chunk-6Q2QTUOP-C0sHhXlW.js} | 2 +- ...CRWQLShu.js => chunk-GF5L2VYU-Bx7OWCKT.js} | 2 +- ...VQyChqWt.js => chunk-JWPE2WC7-CDB8iSD4.js} | 2 +- ...C7jV4s0i.js => chunk-KBJHAD2P-C-x_pet2.js} | 2 +- ...C_Snr0h1.js => chunk-RYQCIY6F-BdgWAht2.js} | 2 +- ...DFwFUebL.js => chunk-XXDRQBXY-oYI7b-Yd.js} | 2 +- .../mermaid/classDiagram-JCYQIIEL-DB7Ei7Xu.js | 1 + .../mermaid/classDiagram-JCYQIIEL-VbMl1AIX.js | 1 - .../classDiagram-v2-OCEON4UE-DB7Ei7Xu.js | 1 + .../classDiagram-v2-OCEON4UE-VbMl1AIX.js | 1 - ...y.js => cose-bilkent-JH36ORCC-DDzAJJm1.js} | 2 +- ...GbQZ8F.js => cynefin-OW5HDTMX-CvB8WqDe.js} | 2 +- ...js => cynefinDiagram-MW4NZA55-Df7_Pu-6.js} | 2 +- ...DX3PGMOR.js => dagre-VZM6K2ZE-CgM8xS_a.js} | 2 +- ...KjYAAp.js => diagram-7IWD3JNH-CrXeLVqK.js} | 2 +- ...mtpLK2.js => diagram-B4RE2ZJO-IgkiBkmK.js} | 2 +- ...XmFqqR.js => diagram-LBJQPF4R-D36DVzBa.js} | 2 +- ...3sg2Z9.js => diagram-Q27KOJAE-D1yMiDYF.js} | 2 +- ...aBML4t.js => diagram-UB23O5K3-Cvd0h3e5.js} | 2 +- ...kb.js => ebnfDiagram-BXEA7PRR-BM91NSlS.js} | 2 +- ...uJkz.js => erDiagram-JOGREHBK-CKzJUYax.js} | 2 +- ...sO.js => flowDiagram-UKHOOZJN-Bya9YZrg.js} | 2 +- ...l.js => ganttDiagram-PKOTCBZU-iBA6PbqA.js} | 2 +- ...s => gitGraphDiagram-DS77QQ5N-BAbB8bOC.js} | 2 +- ...vA.js => infoDiagram-6WML65LV-djMb3_6N.js} | 2 +- ...s => ishikawaDiagram-WSZJBQD7-DZS7FMrO.js} | 2 +- ...js => journeyDiagram-NVQOT4AX-Cy2nGO08.js} | 2 +- ...=> kanban-definition-27J2QSJJ-Djp6T8Py.js} | 2 +- ...e-D2fLiU9C.js => mermaid.core-CPcqpxhK.js} | 8 +- ...> mindmap-definition-FAOFIHXS-CbrfJDte.js} | 2 +- ...aQ2.js => pegDiagram-VL7TDLO6-C20flUJx.js} | 2 +- ...zZm.js => pieDiagram-7S7Q4E2Y-BGYE4JDB.js} | 2 +- ...s => quadrantDiagram-CIZ2JOQS-CjKJqI66.js} | 2 +- ...s => railroadDiagram-AXF67PYL-PJHkoycZ.js} | 2 +- ...> requirementDiagram-LRYGKXZP-DW_nlPjK.js} | 2 +- ....js => sankeyDiagram-W5VNT64P-C3EwLP4_.js} | 2 +- ...s => sequenceDiagram-SI44F4Z6-s5QVqjFm.js} | 2 +- ...9I.js => sizeCapture-X5ZJPWSS-kdCsD_Qo.js} | 2 +- ...i.js => stateDiagram-OKZ733FA-Df8v_BNl.js} | 2 +- .../stateDiagram-v2-UEYNNEHI-C1FQF9e7.js | 1 - .../stateDiagram-v2-UEYNNEHI-tJX7pRnT.js | 1 + ...H0qk.js => swimlanes-SLNWSIFB-DT_OnNgl.js} | 4 +- .../swimlanesDiagram-ULZ7WXOC-BZJjBuDX.js | 8 - .../swimlanesDiagram-ULZ7WXOC-DzN4n4D0.js | 8 + ... timeline-definition-Z64GVDOM-DcCINUmt.js} | 2 +- ...3f.js => vennDiagram-T6HMQDX7-C0l70sKg.js} | 2 +- ...js => wardleyDiagram-T6FBY63Y-Dpq06fm5.js} | 2 +- ...js => xychartDiagram-ELKLHX3M-D4i7B9k2.js} | 2 +- veadk/webui/index.html | 4 +- veadk/webui/website-integration.js | 4 +- 76 files changed, 833 insertions(+), 760 deletions(-) rename veadk/webui/assets/app/{index--Hgqzodn.js => index-BFeVT5ks.js} (64%) rename veadk/webui/assets/chunks/{CodeDiffEditor-st4ujEZ9.js => CodeDiffEditor-DliKrQrj.js} (99%) rename veadk/webui/assets/chunks/{FileExplorerEditor-DGNXN80y.js => FileExplorerEditor-CUiU3i0Q.js} (96%) rename veadk/webui/assets/chunks/{MarkdownPromptEditor-DzOPzsW3.js => MarkdownPromptEditor-C52MG6LY.js} (99%) rename veadk/webui/assets/chunks/{QuickAgentCreateDialog-B3suviqp.js => QuickAgentCreateDialog-5r9dyB8F.js} (97%) rename veadk/webui/assets/chunks/{arc-C0aQ__bD.js => arc-0-rr3fN7.js} (98%) rename veadk/webui/assets/chunks/{channel-DRjKww3c.js => channel-CJviLJhg.js} (53%) rename veadk/webui/assets/chunks/{index.es-DdpZU-FQ.js => index.es-Bxr6oZH9.js} (99%) rename veadk/webui/assets/chunks/{jspdf.es.min-D7-71qxH.js => jspdf.es.min-D6eGuvbB.js} (99%) rename veadk/webui/assets/chunks/{linear-BxsXTKgI.js => linear-DIgatGKn.js} (98%) rename veadk/webui/assets/styles/{index-C0EiMigV.css => index-BWtAkmyR.css} (90%) rename veadk/webui/assets/visualizations/mermaid/{abnfDiagram-N423BO3Z-JilWJ6mP.js => abnfDiagram-N423BO3Z-D23TMKf3.js} (86%) rename veadk/webui/assets/visualizations/mermaid/{architectureDiagram-T3A2C74G-JdwHCer1.js => architectureDiagram-T3A2C74G-GCApJrAt.js} (99%) rename veadk/webui/assets/visualizations/mermaid/{blockDiagram-VBNYF7ZC-Df-U2-rd.js => blockDiagram-VBNYF7ZC-BO8gMzkT.js} (99%) rename veadk/webui/assets/visualizations/mermaid/{c4Diagram-5PPSVZJV-DRG6xrJd.js => c4Diagram-5PPSVZJV-Bj_KLmuk.js} (99%) rename veadk/webui/assets/visualizations/mermaid/{chunk-2GRJ4B5K-CWqXOw4U.js => chunk-2GRJ4B5K-BLJyKwvU.js} (93%) rename veadk/webui/assets/visualizations/mermaid/{chunk-2Q5K7J3B-BXhNKOHQ.js => chunk-2Q5K7J3B-9AYZE35k.js} (66%) rename veadk/webui/assets/visualizations/mermaid/{chunk-5RXB4S5H-DYq5Aq4w.js => chunk-5RXB4S5H-BEPWrO5k.js} (99%) rename veadk/webui/assets/visualizations/mermaid/{chunk-5VM5RSS4-B_FqNlcE.js => chunk-5VM5RSS4-C9Fr507e.js} (83%) rename veadk/webui/assets/visualizations/mermaid/{chunk-6Q2QTUOP-C_uj7jKB.js => chunk-6Q2QTUOP-C0sHhXlW.js} (99%) rename veadk/webui/assets/visualizations/mermaid/{chunk-GF5L2VYU-CRWQLShu.js => chunk-GF5L2VYU-Bx7OWCKT.js} (99%) rename veadk/webui/assets/visualizations/mermaid/{chunk-JWPE2WC7-VQyChqWt.js => chunk-JWPE2WC7-CDB8iSD4.js} (78%) rename veadk/webui/assets/visualizations/mermaid/{chunk-KBJHAD2P-C7jV4s0i.js => chunk-KBJHAD2P-C-x_pet2.js} (87%) rename veadk/webui/assets/visualizations/mermaid/{chunk-RYQCIY6F-C_Snr0h1.js => chunk-RYQCIY6F-BdgWAht2.js} (99%) rename veadk/webui/assets/visualizations/mermaid/{chunk-XXDRQBXY-DFwFUebL.js => chunk-XXDRQBXY-oYI7b-Yd.js} (53%) create mode 100644 veadk/webui/assets/visualizations/mermaid/classDiagram-JCYQIIEL-DB7Ei7Xu.js delete mode 100644 veadk/webui/assets/visualizations/mermaid/classDiagram-JCYQIIEL-VbMl1AIX.js create mode 100644 veadk/webui/assets/visualizations/mermaid/classDiagram-v2-OCEON4UE-DB7Ei7Xu.js delete mode 100644 veadk/webui/assets/visualizations/mermaid/classDiagram-v2-OCEON4UE-VbMl1AIX.js rename veadk/webui/assets/visualizations/mermaid/{cose-bilkent-JH36ORCC-BPo5owKy.js => cose-bilkent-JH36ORCC-DDzAJJm1.js} (99%) rename veadk/webui/assets/visualizations/mermaid/{cynefin-OW5HDTMX-DMGbQZ8F.js => cynefin-OW5HDTMX-CvB8WqDe.js} (99%) rename veadk/webui/assets/visualizations/mermaid/{cynefinDiagram-MW4NZA55-hpciugHe.js => cynefinDiagram-MW4NZA55-Df7_Pu-6.js} (98%) rename veadk/webui/assets/visualizations/mermaid/{dagre-VZM6K2ZE-DX3PGMOR.js => dagre-VZM6K2ZE-CgM8xS_a.js} (97%) rename veadk/webui/assets/visualizations/mermaid/{diagram-7IWD3JNH-CHKjYAAp.js => diagram-7IWD3JNH-CrXeLVqK.js} (96%) rename veadk/webui/assets/visualizations/mermaid/{diagram-B4RE2ZJO-DomtpLK2.js => diagram-B4RE2ZJO-IgkiBkmK.js} (97%) rename veadk/webui/assets/visualizations/mermaid/{diagram-LBJQPF4R-DtXmFqqR.js => diagram-LBJQPF4R-D36DVzBa.js} (94%) rename veadk/webui/assets/visualizations/mermaid/{diagram-Q27KOJAE-1Q3sg2Z9.js => diagram-Q27KOJAE-D1yMiDYF.js} (98%) rename veadk/webui/assets/visualizations/mermaid/{diagram-UB23O5K3-W7aBML4t.js => diagram-UB23O5K3-Cvd0h3e5.js} (95%) rename veadk/webui/assets/visualizations/mermaid/{ebnfDiagram-BXEA7PRR-wEuLDlkb.js => ebnfDiagram-BXEA7PRR-BM91NSlS.js} (87%) rename veadk/webui/assets/visualizations/mermaid/{erDiagram-JOGREHBK-2_tMuJkz.js => erDiagram-JOGREHBK-CKzJUYax.js} (99%) rename veadk/webui/assets/visualizations/mermaid/{flowDiagram-UKHOOZJN-CQeVjisO.js => flowDiagram-UKHOOZJN-Bya9YZrg.js} (99%) rename veadk/webui/assets/visualizations/mermaid/{ganttDiagram-PKOTCBZU-C3IIaX8l.js => ganttDiagram-PKOTCBZU-iBA6PbqA.js} (99%) rename veadk/webui/assets/visualizations/mermaid/{gitGraphDiagram-DS77QQ5N-DrjyVv1p.js => gitGraphDiagram-DS77QQ5N-BAbB8bOC.js} (99%) rename veadk/webui/assets/visualizations/mermaid/{infoDiagram-6WML65LV-BbVY_cvA.js => infoDiagram-6WML65LV-djMb3_6N.js} (69%) rename veadk/webui/assets/visualizations/mermaid/{ishikawaDiagram-WSZJBQD7-BEJc4VJC.js => ishikawaDiagram-WSZJBQD7-DZS7FMrO.js} (99%) rename veadk/webui/assets/visualizations/mermaid/{journeyDiagram-NVQOT4AX-BSviOGH7.js => journeyDiagram-NVQOT4AX-Cy2nGO08.js} (98%) rename veadk/webui/assets/visualizations/mermaid/{kanban-definition-27J2QSJJ-DFhtM-Vn.js => kanban-definition-27J2QSJJ-Djp6T8Py.js} (99%) rename veadk/webui/assets/visualizations/mermaid/{mermaid.core-D2fLiU9C.js => mermaid.core-CPcqpxhK.js} (98%) rename veadk/webui/assets/visualizations/mermaid/{mindmap-definition-FAOFIHXS-B6s3-NWt.js => mindmap-definition-FAOFIHXS-CbrfJDte.js} (98%) rename veadk/webui/assets/visualizations/mermaid/{pegDiagram-VL7TDLO6-C6HBYaQ2.js => pegDiagram-VL7TDLO6-C20flUJx.js} (87%) rename veadk/webui/assets/visualizations/mermaid/{pieDiagram-7S7Q4E2Y-4z6DgzZm.js => pieDiagram-7S7Q4E2Y-BGYE4JDB.js} (96%) rename veadk/webui/assets/visualizations/mermaid/{quadrantDiagram-CIZ2JOQS-CojI1hpI.js => quadrantDiagram-CIZ2JOQS-CjKJqI66.js} (99%) rename veadk/webui/assets/visualizations/mermaid/{railroadDiagram-AXF67PYL-DHZSlumf.js => railroadDiagram-AXF67PYL-PJHkoycZ.js} (84%) rename veadk/webui/assets/visualizations/mermaid/{requirementDiagram-LRYGKXZP-BsKDRsl1.js => requirementDiagram-LRYGKXZP-DW_nlPjK.js} (99%) rename veadk/webui/assets/visualizations/mermaid/{sankeyDiagram-W5VNT64P-CsSlHD-u.js => sankeyDiagram-W5VNT64P-C3EwLP4_.js} (99%) rename veadk/webui/assets/visualizations/mermaid/{sequenceDiagram-SI44F4Z6-qMcMCOh2.js => sequenceDiagram-SI44F4Z6-s5QVqjFm.js} (99%) rename veadk/webui/assets/visualizations/mermaid/{sizeCapture-X5ZJPWSS-DN3BsD9I.js => sizeCapture-X5ZJPWSS-kdCsD_Qo.js} (87%) rename veadk/webui/assets/visualizations/mermaid/{stateDiagram-OKZ733FA-BYogjP-i.js => stateDiagram-OKZ733FA-Df8v_BNl.js} (96%) delete mode 100644 veadk/webui/assets/visualizations/mermaid/stateDiagram-v2-UEYNNEHI-C1FQF9e7.js create mode 100644 veadk/webui/assets/visualizations/mermaid/stateDiagram-v2-UEYNNEHI-tJX7pRnT.js rename veadk/webui/assets/visualizations/mermaid/{swimlanes-SLNWSIFB-C8wlH0qk.js => swimlanes-SLNWSIFB-DT_OnNgl.js} (99%) delete mode 100644 veadk/webui/assets/visualizations/mermaid/swimlanesDiagram-ULZ7WXOC-BZJjBuDX.js create mode 100644 veadk/webui/assets/visualizations/mermaid/swimlanesDiagram-ULZ7WXOC-DzN4n4D0.js rename veadk/webui/assets/visualizations/mermaid/{timeline-definition-Z64GVDOM-kz7tuYqZ.js => timeline-definition-Z64GVDOM-DcCINUmt.js} (99%) rename veadk/webui/assets/visualizations/mermaid/{vennDiagram-T6HMQDX7-BDpgD_3f.js => vennDiagram-T6HMQDX7-C0l70sKg.js} (99%) rename veadk/webui/assets/visualizations/mermaid/{wardleyDiagram-T6FBY63Y-BVIQeymk.js => wardleyDiagram-T6FBY63Y-Dpq06fm5.js} (99%) rename veadk/webui/assets/visualizations/mermaid/{xychartDiagram-ELKLHX3M-8BShqB-h.js => xychartDiagram-ELKLHX3M-D4i7B9k2.js} (99%) diff --git a/frontend/README.md b/frontend/README.md index 236edc1f5..2e7200bbd 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -344,6 +344,13 @@ See [deployment and operation](service/studio_release_notifier/README.md). versions; any version can be restored into the intelligent-development flow for another intent-driven iteration after the temporary migration environment has ended. + The workspace is a single-column page without a side navigation: the home is + the upload entry followed by recent migrations (five by default, expandable + in place, with expired environments offered as view-only), “已迁移项目” is a + top-bar entry that opens the saved-project library, and returning home or + opening the project library never stops a running migration. Capability and + session-list failures are reported separately so the upload entry stays + usable and each failure offers its own retry. Pencil icons beside project and version names open the existing-style name dialog; the check icon saves and the close icon cancels. Names are normalized and trimmed, allow 1–128 Unicode characters, and reject control/invisible diff --git a/frontend/src/i18n/resources/en-US/migrations.json b/frontend/src/i18n/resources/en-US/migrations.json index 1671575ab..2c86b0279 100644 --- a/frontend/src/i18n/resources/en-US/migrations.json +++ b/frontend/src/i18n/resources/en-US/migrations.json @@ -145,9 +145,11 @@ "reselect": "Select again", "selectZip": "Select ZIP", "continue": "Continue upload", - "start": "Start migration", + "start": "Upload and analyze", "inputAria": "Select a local project ZIP", - "retention": "The temporary migration environment is retained for 1 hour after creation. Successfully saved source versions are unaffected." + "retention": "The temporary migration environment is retained for 1 hour after creation. Successfully saved source versions are unaffected.", + "uploading": "Uploading…", + "sizeHint": "Project ZIP, up to {{size}}" }, "deployment": { "requiredPlaceholder": "Enter {{key}}", @@ -160,11 +162,21 @@ "title": "Migrate existing project", "newMigration": "New migration", "recent": "Recent migrations", - "sessionsAria": "Migration sessions", "loadingSessions": "Loading migration sessions…", "noSessions": "No migration sessions", "heading": "Migrate an existing Agent project", - "intro": "Upload a local project ZIP. Codex will first perform a read-only analysis, then ask you to confirm the migration method." + "intro": "Upload a local project ZIP. Codex will first perform a read-only analysis, then ask you to confirm the migration method.", + "backToHome": "Migration home", + "navigation": "Migration navigation", + "showMore": "Show more", + "showLess": "Show less", + "projectName": "Project name", + "status": "Status", + "createdAt": "Created", + "actions": "Actions", + "openTask": "Open migration {{name}}", + "continueTask": "Continue", + "viewTask": "View" }, "actions": { "stop": "Stop migration", @@ -178,8 +190,6 @@ "defaultReason": "Dev Sandbox is currently unavailable. Contact your administrator to check the configuration." }, "conversation": { - "requestZip": "Provide a local project ZIP. After upload, I’ll identify the framework, entry point, and migration boundaries, then ask you to confirm the migration method before making changes.", - "zipHint": "Only local ZIP files are supported, up to {{size}}. The migration environment is retained for 1 hour after creation.", "creatingSandbox": "Creating Dev Sandbox", "initializing": "Initializing the migration workspace and checking AgentKit CLI, Codex, and migration capabilities. The project will upload automatically when the environment is ready.", "elapsed": "Elapsed: {{duration}}", diff --git a/frontend/src/i18n/resources/zh-CN/migrations.json b/frontend/src/i18n/resources/zh-CN/migrations.json index 6ca4bd2ea..15e94a47d 100644 --- a/frontend/src/i18n/resources/zh-CN/migrations.json +++ b/frontend/src/i18n/resources/zh-CN/migrations.json @@ -145,9 +145,11 @@ "reselect": "重新选择", "selectZip": "选择 ZIP", "continue": "继续上传", - "start": "开始迁移", + "start": "上传并分析", "inputAria": "选择本地项目 ZIP", - "retention": "临时迁移环境从创建完成起保留 1 小时;保存成功的源码版本不受影响。" + "retention": "临时迁移环境从创建完成起保留 1 小时;保存成功的源码版本不受影响。", + "uploading": "上传中…", + "sizeHint": "支持项目 ZIP,最大 {{size}}" }, "deployment": { "requiredPlaceholder": "请输入 {{key}}", @@ -160,11 +162,21 @@ "title": "从存量迁移", "newMigration": "新建迁移", "recent": "最近迁移", - "sessionsAria": "迁移会话", "loadingSessions": "正在读取迁移会话…", "noSessions": "暂无迁移会话", "heading": "迁移存量 Agent 项目", - "intro": "上传本地项目 ZIP,Codex 将先进行只读分析,再由你确认迁移方式。" + "intro": "上传本地项目 ZIP,Codex 将先进行只读分析,再由你确认迁移方式。", + "backToHome": "迁移首页", + "navigation": "迁移导航", + "showMore": "查看更多", + "showLess": "收起", + "projectName": "项目名称", + "status": "状态", + "createdAt": "创建时间", + "actions": "操作", + "openTask": "查看迁移 {{name}}", + "continueTask": "继续", + "viewTask": "查看" }, "actions": { "stop": "终止迁移", @@ -178,8 +190,6 @@ "defaultReason": "Dev Sandbox 暂不可用,请联系管理员检查配置。" }, "conversation": { - "requestZip": "请提供本地项目 ZIP。上传后我会识别框架、入口和迁移边界,并在执行实际迁移前请你确认迁移方式。", - "zipHint": "仅支持本地 ZIP,最大 {{size}};迁移环境从创建起保留 1 小时。", "creatingSandbox": "正在创建 Dev Sandbox", "initializing": "正在初始化迁移工作目录,并检查 AgentKit CLI、Codex 和迁移能力。环境就绪后将自动上传项目。", "elapsed": "已等待 {{duration}}", diff --git a/frontend/src/migrations/MigratedProjectsPage.tsx b/frontend/src/migrations/MigratedProjectsPage.tsx index dbf2595b6..63933482c 100644 --- a/frontend/src/migrations/MigratedProjectsPage.tsx +++ b/frontend/src/migrations/MigratedProjectsPage.tsx @@ -43,12 +43,6 @@ export function MigratedProjectsPage({ return ( <>
-
-
-

{t("projects.title")}

-

{t("projects.description")}

-
-
header { +.migration-workspace__nav, +.migration-workspace__location, +.migration-workspace__nav nav { display: flex; align-items: center; gap: 8px; - padding: 0 4px; + min-width: 0; } -.migration-history h1 { +.migration-workspace__nav { + min-height: 54px; + justify-content: space-between; + padding: 8px 24px; + border-bottom: 1px solid hsl(var(--border)); + background: hsl(var(--panel)); +} + +.migration-workspace__nav h1 { margin: 0; font-size: 15px; font-weight: 600; - line-height: 1.4; - letter-spacing: 0; + overflow-wrap: anywhere; } +.migration-home { + min-height: 0; + overflow: auto; + padding: 32px 32px 48px; +} + +.migration-home__content { + width: min(100%, var(--migration-content-width)); + margin: 0 auto; +} + +.migration-home__heading { margin-bottom: 24px; } +.migration-home__heading h2 { margin: 0; font-size: 21px; font-weight: 600; } +.migration-home__heading p { margin: 8px 0 0; color: hsl(var(--muted-foreground)); } + +.migration-history { margin-top: 28px; } +.migration-history > header { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 12px; } +.migration-history h2 { margin: 0; font-size: 17px; font-weight: 600; } +.migration-history table { width: 100%; table-layout: fixed; border-spacing: 0; border: 1px solid hsl(var(--border)); border-radius: 12px; overflow: hidden; background: hsl(var(--panel)); } +.migration-history th, .migration-history td { padding: 14px 16px; text-align: left; font-size: 13px; font-weight: 400; } +.migration-history thead { background: hsl(var(--secondary)); color: hsl(var(--muted-foreground)); } +.migration-history thead th:first-child { width: 36%; } +.migration-history thead th:last-child { width: 108px; } +.migration-history tbody th > span { display: block; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } +.migration-history tbody tr + tr > * { border-top: 1px solid hsl(var(--border)); } +.migration-history time { color: hsl(var(--muted-foreground)); overflow-wrap: anywhere; } +.migration-history td .migration-new-button { min-height: 28px; white-space: nowrap; } + .migration-icon-button { width: 32px; height: 32px; @@ -75,14 +102,14 @@ } .migration-new-button { - min-height: 38px; + min-height: 36px; display: flex; align-items: center; justify-content: flex-start; gap: 9px; - margin: 2px 0; + margin: 0; padding: 0 10px; - border: 1px solid transparent; + border: 1px solid hsl(var(--border)); border-radius: 8px; background: transparent; color: hsl(var(--foreground)); @@ -98,79 +125,17 @@ border-color: hsl(var(--border) / 0.72); } -.migration-new-button.is-active, -.migration-new-button[aria-current="page"] { - border-color: hsl(var(--border) / 0.72); - background: hsl(var(--secondary)); -} - -.migration-history__label { - margin: 10px 10px 6px; - color: hsl(var(--muted-foreground)); - font-size: 11px; - font-weight: 550; - letter-spacing: 0.02em; -} - -.migration-new-button:disabled, -.migration-history nav > button:disabled { +.migration-new-button:disabled { cursor: not-allowed; opacity: 0.5; } -.migration-history nav { - min-height: 0; - display: flex; - flex-direction: column; - gap: 2px; - overflow-y: auto; -} - -.migration-history nav > button { - width: 100%; - min-height: 58px; - display: grid; - align-content: center; - gap: 6px; - padding: 9px 10px; - border: 0; - border-radius: 7px; - background: transparent; - color: hsl(var(--foreground)); - font: inherit; - text-align: left; - cursor: pointer; - transition: background-color 140ms ease, color 140ms ease; -} - -.migration-history nav > button:hover:not(:disabled), -.migration-history nav > button.is-active { - background: hsl(var(--secondary)); -} - -.migration-history nav > button > span { - overflow: hidden; - font-size: 13px; - font-weight: 500; - text-overflow: ellipsis; - white-space: nowrap; -} - -.migration-history nav small { - display: flex; - align-items: center; - justify-content: space-between; - gap: 6px; - color: hsl(var(--muted-foreground)); - font-size: 11px; - line-height: 1.35; -} - .migration-history__status { min-width: 0; display: flex; align-items: center; gap: 5px; + flex-wrap: wrap; } .migration-history__status-label { @@ -208,15 +173,10 @@ white-space: nowrap; } -.migration-history nav time { - flex-shrink: 0; -} - .migration-history__empty { - margin: 16px 8px; + margin: 12px 2px; color: hsl(var(--muted-foreground)); - font-size: 12px; - text-align: center; + font-size: 13px; } .migration-main { @@ -225,7 +185,7 @@ display: grid; grid-template-rows: auto minmax(0, 1fr) auto; overflow: hidden; - background: hsl(var(--background)); + background: hsl(var(--canvas)); } .migration-main__top { @@ -233,10 +193,11 @@ background: hsl(var(--panel)); } +.migration-projects-page { display: block; overflow-y: auto; } + .migration-projects-page__content { min-height: 0; - overflow-y: auto; - padding: 28px clamp(24px, 4vw, 56px) 48px; + padding: 28px 32px 48px; } .migration-projects-page__content > .ic-projects-panel { @@ -747,7 +708,7 @@ .migration-confirmation__grid { display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); + grid-template-columns: minmax(0, 1fr); gap: 16px; } @@ -1203,16 +1164,13 @@ min-height: 104px; position: relative; display: grid; - grid-template-rows: minmax(40px, auto) 36px; + grid-template-rows: auto auto; gap: 10px; box-sizing: border-box; padding: 12px 14px; border: 1px solid hsl(var(--foreground) / 0.14); - border-radius: 16px; + border-radius: 12px; background: hsl(var(--panel)); - box-shadow: - 0 8px 32px hsl(var(--foreground) / 0.028), - 0 24px 72px 8px hsl(var(--foreground) / 0.02); transition: background-color 140ms ease, border-color 140ms ease, box-shadow 140ms ease; } @@ -1436,11 +1394,34 @@ outline-offset: 2px; } -@media (max-width: 1120px) { - .migration-workspace { - grid-template-columns: 210px minmax(0, 1fr); - } +.migration-home .migration-composer { width: 100%; padding-bottom: 0; } +.migration-home .migration-composer__box { padding: 20px; gap: 16px; } +.migration-home .migration-composer__content { min-height: 144px; justify-content: center; border: 1px dashed hsl(var(--border)); border-radius: 8px; padding: 16px; } +.migration-upload-prompt { display: grid; justify-items: center; gap: 10px; text-align: center; } +.migration-upload-prompt > svg { width: 28px; height: 28px; color: hsl(var(--muted-foreground)); } +.migration-upload-prompt p { margin: 0; font-size: 15px; } +.migration-upload-prompt small { color: hsl(var(--muted-foreground)); font-size: 12px; } +.migration-composer__actions, .migration-composer__tools { gap: 12px; flex-wrap: wrap; } +.migration-composer__file { min-width: 0; } +.migration-composer__file span { min-width: 0; } +.migration-home .migration-composer > p { text-align: left; font-size: 12px; } + +@media (max-width: 640px) { + .migration-workspace__nav { padding: 8px 12px; flex-wrap: wrap; } + .migration-workspace__nav nav { flex-wrap: wrap; } + .migration-home, .migration-projects-page__content { padding: 24px 16px; } + .migration-home .migration-composer__box { padding: 12px; } + .migration-history th, .migration-history td { padding: 10px 6px; font-size: 12px; } + .migration-history thead th:first-child { width: 30%; } + .migration-history thead th:last-child { width: 76px; } + .migration-history td .migration-new-button { padding: 0 8px; } + .migration-history__status-label { white-space: normal; overflow-wrap: anywhere; } + .migration-main__header, .migration-confirmation__heading { align-items: flex-start; flex-direction: column; } + .migration-main__header-actions { flex-wrap: wrap; } + .migration-composer { width: calc(100% - 32px); } +} +@media (max-width: 1120px) { .migration-main__header { padding-inline: 18px; } diff --git a/frontend/src/migrations/MigrationWorkspace.tsx b/frontend/src/migrations/MigrationWorkspace.tsx index 3e8a659af..c44058d33 100644 --- a/frontend/src/migrations/MigrationWorkspace.tsx +++ b/frontend/src/migrations/MigrationWorkspace.tsx @@ -752,6 +752,10 @@ export function MigrationWorkspace({ const [modelsReloadKey, setModelsReloadKey] = useState(0); const [dragging, setDragging] = useState(false); const [loading, setLoading] = useState(true); + const [loadError, setLoadError] = useState(""); + const [capabilityError, setCapabilityError] = useState(""); + const [loadKey, setLoadKey] = useState(0); + const [showAllTasks, setShowAllTasks] = useState(false); const [action, setAction] = useState< "create" | "upload" | "answer" | "confirm" | "stop" | "download" | "" >(""); @@ -939,26 +943,38 @@ export function MigrationWorkspace({ useEffect(() => { const controller = new AbortController(); setLoading(true); - setError(""); - void Promise.all([ + setLoadError(""); + setCapabilityError(""); + void Promise.allSettled([ getMigrationCapabilities(controller.signal), listMigrationTasks(controller.signal), ]) - .then(([nextCapability, nextTasks]) => { + .then(([capabilityResult, taskResult]) => { if (controller.signal.aborted) return; - setCapability(nextCapability); - setTasks(nextTasks); - }) - .catch((cause: unknown) => { - if (!controller.signal.aborted) { - setError(cause instanceof Error ? cause.message : String(cause)); + if (capabilityResult.status === "fulfilled") { + setCapability(capabilityResult.value); + } else { + setCapabilityError( + capabilityResult.reason instanceof Error + ? capabilityResult.reason.message + : String(capabilityResult.reason), + ); + } + if (taskResult.status === "fulfilled") { + setTasks(taskResult.value); + } else { + setLoadError( + taskResult.reason instanceof Error + ? taskResult.reason.message + : String(taskResult.reason), + ); } }) .finally(() => { if (!controller.signal.aborted) setLoading(false); }); return () => controller.abort(); - }, []); + }, [loadKey]); useEffect(() => { const controller = new AbortController(); @@ -1955,6 +1971,8 @@ export function MigrationWorkspace({ const composerFile = sourceFile; const composerBusy = action === "create" || action === "upload"; + const isHome = page === "new" && !task && action !== "create"; + const navigationBusy = composerBusy || action === "confirm" || action === "answer" || action === "stop" || Boolean(evaluationAction); const showComposer = !task || (task.canUpload && !taskEnvironmentExpired); const expiryCopy = task ? migrationExpiryCopy(task, now) : null; const hasEvaluationTab = Boolean(task?.evaluation?.enabled); @@ -1978,103 +1996,152 @@ export function MigrationWorkspace({ ? evaluationDraftLoadError : null; + const composer = activeTaskTab === "migration" && showComposer && capability?.enabled ? ( +
+
{ + event.preventDefault(); + if (composerBusy) return; + setDragging(true); + }} + onDragOver={(event) => { + event.preventDefault(); + event.dataTransfer.dropEffect = composerBusy ? "none" : "copy"; + }} + onDragLeave={(event: DragEvent) => { + if ( + !event.currentTarget.contains( + event.relatedTarget as Node | null, + ) + ) { + setDragging(false); + } + }} + onDrop={(event) => { + event.preventDefault(); + setDragging(false); + if (composerBusy) return; + selectFile(event.dataTransfer.files?.[0]); + }} + > +
+ {composerFile ? ( +
+ + {composerFile.name} + {formatBytes(composerFile.size)} + +
+ ) : ( +
+ +

{task ? t("upload.reselectPrompt") : t("upload.selectPrompt")}

+ {t("upload.sizeHint", { size: maxSourceSizeLabel })} +
+ )} +
+
+
+ +
+ + setModelsReloadKey((current) => current + 1) + } + /> +
+
+ +
+ { + setEvaluationDraft(value); + setEvaluationErrors({}); + }} + capability={capability.evaluation} + disabled={composerBusy} + configLocked={Boolean(task)} + locked={Boolean(task?.evaluation?.dataset)} + errors={evaluationErrors} + /> + +
+

+ {t("upload.retention")} +

+
+ ) : null; + return ( <>
- + {page === "projects" ? ( + ) : isHome ? ( +
+
+
+

{t("workspace.heading")}

+

{t("workspace.intro")}

+
+ {loading ? {t("workspace.loadingSessions")} : null} + {loadError ? ( +
+ {localeCompatibleBackendText(loadError, locale) || t("errors.loadFailed")} + +
+ ) : null} + {capabilityError ? ( +
+ {localeCompatibleBackendText(capabilityError, locale) || t("errors.loadFailed")} + +
+ ) : null} + {capability && !capability.enabled ? ( +
+ {t("capability.unavailable")} +

{localeCompatibleBackendText(capability.reason, locale) || t("capability.defaultReason")}

+
+ ) : null} + {composer} + {error ? ( +
+ {localeCompatibleBackendText(error, locale) || t("errors.loadFailed")} + +
+ ) : null} +
+
+

{t("workspace.recent")}

+ {tasks.length > 5 ? ( + + ) : null} +
+ {pollError ? ( +
+ {localeCompatibleBackendText(pollError, locale) || t("errors.refreshFailed")} + {pollErrorRetryable ? : null} +
+ ) : null} + {!loading && !loadError && tasks.length === 0 ?

{t("workspace.noSessions")}

: null} + {tasks.length > 0 ? ( + + + + + + + + {(showAllTasks ? tasks : tasks.slice(0, 5)).map((item) => { + const status = migrationHistoryStatus(item); + const environmentExpired = isMigrationEnvironmentExpired(item, now); + const statusLabel = migrationText(status.labelKey); + return ( + + + + + + + ); + })} +
{t("workspace.projectName")}{t("workspace.status")}{t("workspace.createdAt")}{t("workspace.actions")}
{sourceStem(item.sourceFileName)} + {statusLabel} + {environmentExpired ? {t("historyStatus.environmentExpired")} : null} +
+ ) : null} +
+
+
) : (
@@ -2195,6 +2350,7 @@ export function MigrationWorkspace({ {t("capability.unavailable")}

{localeCompatibleBackendText(capability?.reason, locale) + || localeCompatibleBackendText(capabilityError, locale) || t("capability.defaultReason")}

@@ -2202,13 +2358,6 @@ export function MigrationWorkspace({ {!task ? ( <> -
-
AI
-
-

{t("conversation.requestZip")}

- {t("conversation.zipHint", { size: maxSourceSizeLabel })} -
-
{action === "create" && sourceFile ? ( <>
@@ -2708,120 +2857,7 @@ export function MigrationWorkspace({ ) : null}
- {activeTaskTab === "migration" && showComposer && capability?.enabled ? ( -
-
{ - event.preventDefault(); - if (composerBusy) return; - setDragging(true); - }} - onDragOver={(event) => { - event.preventDefault(); - event.dataTransfer.dropEffect = composerBusy ? "none" : "copy"; - }} - onDragLeave={(event: DragEvent) => { - if ( - !event.currentTarget.contains( - event.relatedTarget as Node | null, - ) - ) { - setDragging(false); - } - }} - onDrop={(event) => { - event.preventDefault(); - setDragging(false); - if (composerBusy) return; - selectFile(event.dataTransfer.files?.[0]); - }} - > -
- {composerFile ? ( -
- - {composerFile.name} - {formatBytes(composerFile.size)} - -
- ) : ( -

{task ? t("upload.reselectPrompt") : t("upload.selectPrompt")}

- )} -
-
-
- -
- - setModelsReloadKey((current) => current + 1) - } - /> -
-
- -
- { - setEvaluationDraft(value); - setEvaluationErrors({}); - }} - capability={capability.evaluation} - disabled={composerBusy} - configLocked={Boolean(task)} - locked={Boolean(task?.evaluation?.dataset)} - errors={evaluationErrors} - /> - -
-

- {t("upload.retention")} -

-
- ) : null} + {composer}
)} diff --git a/frontend/tests/migrationWorkspace.test.mjs b/frontend/tests/migrationWorkspace.test.mjs index 5a72bd5c8..595d7883e 100644 --- a/frontend/tests/migrationWorkspace.test.mjs +++ b/frontend/tests/migrationWorkspace.test.mjs @@ -232,7 +232,7 @@ test("keeps new migration and migrated projects as parallel workspace pages", () assert.match(source, /t\("workspace\.recent"\)/); assert.match( source, - /aria-current=\{page === "projects" \? "page" : undefined\}/, + /className="migration-workspace__nav"[\s\S]*?t\("workspace\.backToHome"\)[\s\S]*?t\("workspace\.newMigration"\)/, ); assert.match(source, /page === "projects" \? \(/); assert.match(source, / { ); assert.doesNotMatch(pollingEffect, /setActivity\(null\)/); }); + +test("migration home exposes upload and recent tasks without a navigation sidebar", () => { + const source = readFileSync(workspaceUrl, "utf8"); + const styles = readFileSync(stylesUrl, "utf8"); + assert.doesNotMatch(source, /