diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a6dd8f..68cf2c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ ## Unreleased +- 右侧工作区改为“文件 / 软件作业”双面板,标准页面和 embed 模式保持一致,折叠后仍可查看作业数量与完成提示;文件面板新增“当前对话 / 全部文件”切换、当前目录搜索、排序和修改时间展示。 + - 专业软件任务提交后由系统统一发送完成通知和产物,Agent 不再自行等待并重复报告;Origin PNG 改由清理异常文字基线后的矢量页渲染,三维图在指定毫米画布时也会同步缩放模板图层,避免横线及坐标标题、色标和文字裁切。 - 专业软件任务完成后可按用户意图仅报告产物,或自动读取图表和数据并给出分析;Job 中心重新整理了状态、输入输出、耗时和执行详情,支持查看结果、复制 Job ID、深入分析及重新分析。 diff --git a/tests/frontend_files.test.mjs b/tests/frontend_files.test.mjs index cd8d96b..08db2d4 100644 --- a/tests/frontend_files.test.mjs +++ b/tests/frontend_files.test.mjs @@ -3,6 +3,9 @@ import fs from "node:fs"; import test from "node:test"; const filesJs = fs.readFileSync("web/static/js/files.js", "utf8"); +const layoutJs = fs.readFileSync("web/static/js/layout.js", "utf8"); +const jobsJs = fs.readFileSync("web/static/js/software_jobs.js", "utf8"); +const pageHtml = fs.readFileSync("web/static/dev.html", "utf8"); test("file menu exposes an explicit confirmed replacement action", () => { assert.match(filesJs, /label: "替换文件"/); @@ -17,3 +20,35 @@ test("ordinary uploads default to rename conflict policy", () => { assert.match(filesJs, /fd\.append\("conflict", opts\.conflict \|\| "rename"\)/); assert.match(filesJs, /同名文件已保留为/); }); + +test("file workspace exposes explicit task and all-files contexts", () => { + assert.match(pageHtml, /id="files-scope-task"[^>]*>当前对话); + assert.match(pageHtml, /id="files-scope-all"[^>]*>全部文件); + assert.match(filesJs, /state\.filesScope = "task"/); + assert.match(filesJs, /state\.filesScope = "all"/); +}); + +test("current directory supports search, sort, and modification time", () => { + assert.match(pageHtml, /id="file-search"/); + assert.match(pageHtml, /id="file-sort"/); + assert.match(pageHtml, /value="mtime-desc"/); + assert.match(pageHtml, /value="size-desc"/); + assert.match(filesJs, /visibleEntries/); + assert.match(filesJs, /fileMtime\(e\.mtime\)/); +}); + +test("software jobs share the embed-safe right workspace", () => { + assert.match(pageHtml, /data-right-panel-tab="files"/); + assert.match(pageHtml, /data-right-panel-tab="jobs"/); + assert.match(pageHtml, /id="right-jobs-panel"/); + assert.doesNotMatch(pageHtml, /id="software-job-backdrop"/); + assert.doesNotMatch(pageHtml, /id="software-job-panel"/); + assert.match(layoutJs, /setRightPanelTab/); + assert.match(jobsJs, /unreadTerminal/); +}); + +test("mobile navigation distinguishes list, files, and software jobs", () => { + assert.match(pageHtml, /id="mv-tab-left"[^>]*>\s*列表/); + assert.match(pageHtml, /id="mv-tab-right"[^>]*data-right-tab="files"/); + assert.match(pageHtml, /id="mv-tab-jobs"[^>]*data-right-tab="jobs"/); +}); diff --git a/tests/test_static_vendor.py b/tests/test_static_vendor.py index c97188e..f98f4ae 100644 --- a/tests/test_static_vendor.py +++ b/tests/test_static_vendor.py @@ -26,19 +26,19 @@ def _frontend_source() -> str: class StaticVendorTests(unittest.TestCase): - def test_dev_console_has_software_job_center(self) -> None: + def test_dev_console_has_embed_safe_software_job_workspace(self) -> None: html = DEV_HTML.read_text(encoding="utf-8") source = (JS_DIR / "software_jobs.js").read_text(encoding="utf-8") chat_source = (JS_DIR / "chat.js").read_text(encoding="utf-8") - self.assertIn('id="software-job-center"', html) - self.assertIn('id="software-job-toggle"', html) - self.assertIn("#software-job-center {", html) - self.assertIn("#software-job-toggle {", html) + self.assertIn('id="right-tab-jobs"', html) + self.assertIn('id="right-jobs-panel"', html) + self.assertIn('id="software-job-badge"', html) + self.assertIn(".right-workspace-panel {", html) self.assertLess( - html.index('id="software-job-center"'), + html.index('id="right-tab-jobs"'), html.index('id="storage-foot"'), ) - self.assertIn('id="software-job-panel"', html) + self.assertNotIn('id="software-job-panel"', html) self.assertIn("before_created_at", source) self.assertIn("before_job_id", source) self.assertIn("onListScroll", source) @@ -137,14 +137,14 @@ class StaticVendorTests(unittest.TestCase): html = DEV_HTML.read_text(encoding="utf-8") files_js = (JS_DIR / "files.js").read_text(encoding="utf-8") - self.assertIn('id="btn-toggle-hidden" class="small"', html) + self.assertRegex(html, r'id="btn-toggle-hidden" class="[^"]*\bsmall\b') self.assertNotIn( 'id="btn-toggle-hidden" class="small" title="显示点目录" ' 'aria-label="显示点目录" aria-pressed="false" hidden', html, ) self.assertNotIn("hiddenToggle.hidden", files_js) - self.assertIn('id="btn-file-add" class="small dd-toggle"', html) + self.assertRegex(html, r'id="btn-file-add" class="[^"]*\bsmall\b[^"]*\bdd-toggle\b') self.assertNotIn('id="btn-new-folder"', html) self.assertNotIn('id="btn-src-pick"', html) self.assertNotIn('id="btn-upload"', html) diff --git a/web/static/dev.html b/web/static/dev.html index 751aaeb..4f87f59 100644 --- a/web/static/dev.html +++ b/web/static/dev.html @@ -520,12 +520,15 @@ body.right-collapsed #app.ready { --right-grid-width: 40px; } body.right-collapsed #pane-right > * { display: none; } body.right-collapsed #pane-right > .pane-head:first-child { - display: flex; justify-content: center; align-items: center; + display: flex; flex-direction: column; justify-content: flex-start; align-items: center; padding: 6px 4px; border-bottom: none; background: transparent; - position: static; + position: static; height: 100%; } body.right-collapsed #pane-right > .pane-head:first-child > * { display: none; } - body.right-collapsed #pane-right > .pane-head:first-child > #pane-toggle-right { display: inline-block; } + body.right-collapsed #pane-right > .pane-head:first-child > .workspace-tab, + body.right-collapsed #pane-right > .pane-head:first-child > #pane-toggle-right { display: inline-flex; } + body.right-collapsed #pane-right > .pane-head:first-child > #pane-toggle-right { margin-top: auto; } + body.right-collapsed .workspace-tab span:not(.workspace-badge) { display: none; } header { grid-area: head; background: #fff; border-bottom: 1px solid var(--border); @@ -556,9 +559,14 @@ #task-scroll { flex: 1; min-height: 0; overflow: auto; } /* min-height: 0 + overflow: hidden 让内部 flex 子项的 overflow: auto 真正生效(否则被默认 min-height: auto 顶出) */ #pane-mid { grid-area: mid; display: flex; flex-direction: column; border-right: 1px solid var(--border); background: var(--panel); min-height: 0; min-width: 0; overflow: hidden; position: relative; } - /* flex 列:pane-head / crumbs / 上传状态固定,#file-list 独占滚动,存储条钉底 */ + /* 右侧工作区:页签头固定,文件 / 作业面板各自管理内部滚动 */ #pane-right { grid-area: right; border-right: none; display: flex; flex-direction: column; overflow: hidden; background: var(--panel); min-height: 0; } - #pane-right > .pane-head, #pane-right > #file-crumbs, #pane-right > .upload-status { flex-shrink: 0; } + #pane-right > .pane-head { flex-shrink: 0; } + .right-workspace-panel { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; } + #right-jobs-panel { display: none; } + body.right-tab-jobs #right-files-panel { display: none; } + body.right-tab-jobs #right-jobs-panel { display: flex; } + body.right-tab-jobs .file-panel-control { display: none; } #file-list { flex: 1 1 auto; overflow: auto; min-height: 0; } .splitter { @@ -1069,42 +1077,30 @@ overflow: hidden; text-overflow: ellipsis; white-space: nowrap; margin-top: 2px; } .proc-toast.fail { border-color: rgba(192,57,43,0.5); } - #software-job-center { display: block; flex-shrink: 0; font-size: 12px; } - #software-job-toggle { - width: 100%; display: grid; grid-template-columns: 30px minmax(0, 1fr) auto; - align-items: center; gap: 8px; padding: 9px 12px; text-align: left; - border: 0; border-top: 1px solid var(--border); background: #fff; cursor: pointer; + .right-workspace-head { gap: 4px; } + .mobile-workspace-title { display: none; font-size: 13px; font-weight: 600; } + .workspace-tab { + position: relative; display: inline-flex; align-items: center; justify-content: center; gap: 5px; + min-width: 0; padding: 4px 7px; border: 0; border-radius: 6px; background: transparent; + color: var(--muted); cursor: pointer; font-size: 12px; } - #software-job-toggle:hover { background: #fafafa; } - .sj-entry-icon { - position: relative; width: 30px; height: 30px; display: grid; place-items: center; border-radius: 8px; - color: var(--accent); background: var(--accent-soft); border: 1px solid rgba(192,57,43,.14); + .workspace-tab:hover { background: var(--hover); color: var(--text); } + .workspace-tab.active { background: var(--accent-soft); color: var(--accent); font-weight: 600; } + .workspace-tab svg { width: 16px; height: 16px; flex-shrink: 0; } + .workspace-badge { + min-width: 16px; height: 16px; padding: 0 4px; display: inline-flex; align-items: center; justify-content: center; + border-radius: 999px; background: var(--accent); color: #fff; font-size: 10px; font-style: normal; line-height: 1; } - .sj-entry-dot { position: absolute; right: -2px; bottom: -2px; width: 9px; height: 9px; border: 2px solid #fff; border-radius: 50%; background: #9ca3af; } + .sj-entry-dot { position: absolute; right: 2px; top: 1px; width: 8px; height: 8px; border: 2px solid #fafafa; border-radius: 50%; background: transparent; } .sj-entry-dot.active { background: #e28a2b; box-shadow: 0 0 0 2px rgba(226,138,43,.14); } .sj-entry-dot.succeeded { background: #35a164; } .sj-entry-dot.failed { background: var(--danger); } - .sj-entry-icon svg, .sj-panel-icon svg { width: 17px; height: 17px; } - .sj-entry-copy { min-width: 0; display: flex; flex-direction: column; gap: 2px; } - .sj-entry-title { font-weight: 600; color: var(--text); } - #software-job-count { color: var(--muted); font-size: 11px; font-weight: 400; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } - .sj-entry-arrow { color: var(--muted); font-size: 18px; } - #software-job-backdrop { - position: fixed; inset: 0; z-index: 129; background: rgba(0,0,0,.18); - opacity: 0; visibility: hidden; transition: opacity .18s ease, visibility .18s ease; + .software-job-summary { + min-height: 39px; flex-shrink: 0; display: flex; align-items: center; gap: 8px; + padding: 7px 10px; border-bottom: 1px solid var(--border); background: #fafafa; font-size: 12px; } - #software-job-backdrop.open { opacity: 1; visibility: visible; } - #software-job-panel { - position: fixed; z-index: 130; top: 0; right: 0; bottom: 0; - width: min(480px, 94vw); display: flex; flex-direction: column; background: #fff; - box-shadow: -8px 0 28px rgba(0,0,0,.16); transform: translateX(105%); - visibility: hidden; transition: transform .2s ease, visibility .2s ease; - } - #software-job-panel.open { transform: translateX(0); visibility: visible; } - .sj-panel-head { flex-shrink: 0; display: flex; align-items: center; gap: 10px; padding: 13px 16px; border-bottom: 1px solid var(--border); background: linear-gradient(180deg, #fff, #fffafa); } - .sj-panel-icon { width: 32px; height: 32px; display: grid; place-items: center; border-radius: 9px; color: #fff; background: linear-gradient(135deg, var(--accent), #8e2a20); box-shadow: 0 3px 8px rgba(192,57,43,.2); } - .sj-panel-head strong { font-size: 15px; } - .sj-panel-head .spacer { flex: 1; } + .software-job-summary strong { white-space: nowrap; } + #software-job-count { flex: 1; min-width: 0; color: var(--muted); font-size: 11px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } #software-job-list { flex: 1; min-height: 0; overflow: auto; padding: 6px 10px 18px; } .sj-card { margin: 8px 0; padding: 11px 12px; border: 1px solid var(--border-soft); border-radius: 9px; background: #fff; box-shadow: 0 1px 3px rgba(0,0,0,.035); transition: border-color .15s ease, box-shadow .15s ease; } .sj-card:hover { border-color: var(--border); box-shadow: 0 3px 10px rgba(0,0,0,.055); } @@ -1262,6 +1258,21 @@ #chat-form.drag-over * { pointer-events: none; } /* ───── files ───── */ + .file-context { + min-height: 38px; display: flex; align-items: center; gap: 4px; padding: 6px 10px; + border-bottom: 1px solid var(--border); background: #fff; + } + .file-context button { + padding: 4px 7px; border: 1px solid transparent; border-radius: 6px; background: transparent; + color: var(--muted); font-size: 11px; cursor: pointer; white-space: nowrap; + } + .file-context button:hover:not(:disabled) { background: var(--hover); color: var(--text); } + .file-context button.active { border-color: var(--accent-soft); background: var(--accent-soft); color: var(--accent); } + .file-context button:disabled { opacity: .45; cursor: default; } + #files-proj { margin-left: auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .file-tools { display: flex; gap: 6px; padding: 6px 10px; border-bottom: 1px solid var(--border-soft); background: #fafafa; } + .file-tools input { flex: 1 1 auto; min-width: 0; padding: 5px 7px; font-size: 12px; } + .file-tools select { flex: 0 0 105px; min-width: 0; padding: 4px; font-size: 11px; } .crumbs { padding: 8px 12px; border-bottom: 1px solid var(--border); font-size: 12px; background: #fafafa; } .crumbs a { margin-right: 4px; } .file-row { @@ -1269,7 +1280,9 @@ align-items: center; gap: 8px; } .file-row:hover { background: var(--hover); } - .file-row .name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .file-row .file-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; } + .file-row .name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .file-row .mtime { color: var(--muted); font-size: 10px; font-family: var(--mono); } .file-row .size { font-size: 11px; color: var(--muted); font-family: var(--mono); } .ico-dir::before { content: "▸ "; color: var(--accent); } .ico-file::before { content: "· "; color: var(--muted); } @@ -1538,6 +1551,7 @@ /* ───── mobile tab nav (header) — 桌面隐藏,手机断点内显示 ───── */ .mobile-tabs { display: none; gap: 4px; } .mobile-tabs button { + position: relative; padding: 5px 10px; font-size: 12px; line-height: 1.2; background: transparent; border: 1px solid var(--border); border-radius: var(--r-md); color: var(--muted); cursor: pointer; @@ -1546,6 +1560,12 @@ .mobile-tabs button.active { background: var(--accent-soft); border-color: var(--accent); color: var(--accent); } + .mobile-tabs .workspace-badge { position: absolute; top: -5px; right: -3px; } + .mobile-tabs button.has-unread::after { + content: ""; position: absolute; top: -4px; left: -3px; width: 7px; height: 7px; + border: 2px solid #fff; border-radius: 50%; background: #35a164; + } + .mobile-tabs button.has-unread-failed::after { background: var(--danger); } /* ───── responsive: tablet (641-1024px) ───── 断点内强制 rail(纯 CSS,不写 localStorage;回桌面用户偏好仍生效) */ @@ -1586,6 +1606,10 @@ body.mv-right #pane-right { display: flex; } /* 折叠按钮在手机不可见 */ #pane-toggle-left, #pane-toggle-right, .splitter { display: none !important; } + .right-workspace-head .workspace-tab { display: none; } + .mobile-workspace-title.files { display: inline; } + body.right-tab-jobs .mobile-workspace-title.files { display: none; } + body.right-tab-jobs .mobile-workspace-title.jobs { display: inline; } /* header 紧凑化;品牌名较长 → 标题单行省略,不挤掉右侧按钮 */ header { padding: 6px 10px; gap: 6px; flex-wrap: wrap; } @@ -1995,11 +2019,12 @@ title="管理后台(仅管理员)" style="display:none;">管理 - +