feat(web): redesign file and software job workspace
This commit is contained in:
parent
487d139d39
commit
7708828c72
|
|
@ -8,6 +8,8 @@
|
|||
|
||||
## Unreleased
|
||||
|
||||
- 右侧工作区改为“文件 / 软件作业”双面板,标准页面和 embed 模式保持一致,折叠后仍可查看作业数量与完成提示;文件面板新增“当前对话 / 全部文件”切换、当前目录搜索、排序和修改时间展示。
|
||||
|
||||
- 专业软件任务提交后由系统统一发送完成通知和产物,Agent 不再自行等待并重复报告;Origin PNG 改由清理异常文字基线后的矢量页渲染,三维图在指定毫米画布时也会同步缩放模板图层,避免横线及坐标标题、色标和文字裁切。
|
||||
|
||||
- 专业软件任务完成后可按用户意图仅报告产物,或自动读取图表和数据并给出分析;Job 中心重新整理了状态、输入输出、耗时和执行详情,支持查看结果、复制 Job ID、深入分析及重新分析。
|
||||
|
|
|
|||
|
|
@ -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"/);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;">管理</a>
|
||||
<button id="hd-chpw" title="修改登录密码">改密码</button>
|
||||
<button id="hd-logout">退出登录</button>
|
||||
<!-- 手机 tab(桌面 display:none):任务 / 对话 / 文件 -->
|
||||
<!-- 手机 tab(桌面 display:none):对话列表 / 对话 / 文件 / 软件作业 -->
|
||||
<div class="mobile-tabs" role="tablist">
|
||||
<button id="mv-tab-left" data-mv="mv-left" class="active">任务</button>
|
||||
<button id="mv-tab-left" data-mv="mv-left" class="active">列表</button>
|
||||
<button id="mv-tab-mid" data-mv="mv-mid">对话</button>
|
||||
<button id="mv-tab-right" data-mv="mv-right">文件</button>
|
||||
<button id="mv-tab-right" data-mv="mv-right" data-right-tab="files">文件</button>
|
||||
<button id="mv-tab-jobs" data-mv="mv-right" data-right-tab="jobs">作业<span id="software-job-mobile-badge" class="workspace-badge" hidden></span></button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
|
@ -2135,34 +2160,54 @@
|
|||
|
||||
<!-- right -->
|
||||
<div id="pane-right">
|
||||
<div class="pane-head">
|
||||
<span class="label">文件</span>
|
||||
<span id="files-proj" class="muted small" style="margin-left:6px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0;flex:0 1 auto;" title=""></span>
|
||||
<div class="pane-head right-workspace-head" role="tablist" aria-label="工作区">
|
||||
<span class="mobile-workspace-title files">文件</span>
|
||||
<span class="mobile-workspace-title jobs">软件作业</span>
|
||||
<button id="right-tab-files" class="workspace-tab active" type="button" data-right-panel-tab="files" role="tab" aria-selected="true" title="文件">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 6.5A2.5 2.5 0 0 1 5.5 4H10l2 2h6.5A2.5 2.5 0 0 1 21 8.5v9A2.5 2.5 0 0 1 18.5 20h-13A2.5 2.5 0 0 1 3 17.5z"></path></svg>
|
||||
<span>文件</span>
|
||||
</button>
|
||||
<button id="right-tab-jobs" class="workspace-tab" type="button" data-right-panel-tab="jobs" role="tab" aria-selected="false" title="软件作业">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="4" y="5" width="16" height="14" rx="3"></rect><path d="M8 9h8M8 13h5"></path><path d="M9 5V3m6 2V3"></path></svg>
|
||||
<span>作业</span><i id="software-job-entry-dot" class="sj-entry-dot"></i><span id="software-job-badge" class="workspace-badge" hidden></span>
|
||||
</button>
|
||||
<span class="spacer"></span>
|
||||
<button id="btn-toggle-hidden" class="small" title="显示点目录" aria-label="显示点目录" aria-pressed="false">.*</button>
|
||||
<button id="btn-file-add" class="small dd-toggle" title="添加文件或文件夹" aria-label="添加文件或文件夹">+ 添加</button>
|
||||
<button id="btn-refresh-files" class="small">↻</button>
|
||||
<button id="pane-toggle-right" class="small" title="折叠文件列表">›</button>
|
||||
<button id="btn-toggle-hidden" class="small file-panel-control" title="显示技术目录" aria-label="显示技术目录" aria-pressed="false">.*</button>
|
||||
<button id="btn-file-add" class="small dd-toggle file-panel-control" title="添加文件或文件夹" aria-label="添加文件或文件夹">+ 添加</button>
|
||||
<button id="btn-refresh-files" class="small file-panel-control" title="刷新文件">↻</button>
|
||||
<button id="pane-toggle-right" class="small" title="折叠工作区">›</button>
|
||||
</div>
|
||||
<div id="right-files-panel" class="right-workspace-panel" role="tabpanel" aria-labelledby="right-tab-files">
|
||||
<div id="file-context" class="file-context">
|
||||
<button id="files-scope-task" type="button" class="active">当前对话</button>
|
||||
<button id="files-scope-all" type="button">全部文件</button>
|
||||
<span id="files-proj" class="muted small" title=""></span>
|
||||
</div>
|
||||
<div class="file-tools">
|
||||
<input id="file-search" type="search" placeholder="搜索当前目录" aria-label="搜索当前目录" />
|
||||
<select id="file-sort" aria-label="文件排序" title="文件排序">
|
||||
<option value="name-asc">名称 A→Z</option>
|
||||
<option value="name-desc">名称 Z→A</option>
|
||||
<option value="mtime-desc">最近修改</option>
|
||||
<option value="mtime-asc">最早修改</option>
|
||||
<option value="size-desc">大小 大→小</option>
|
||||
<option value="size-asc">大小 小→大</option>
|
||||
</select>
|
||||
</div>
|
||||
<div id="file-upload-status" class="upload-status"></div>
|
||||
<div id="file-crumbs" class="crumbs muted">加载中…</div>
|
||||
<div id="file-list"></div>
|
||||
<div id="software-job-center" aria-live="polite">
|
||||
<button id="software-job-toggle" type="button" title="查看专业软件任务">
|
||||
<span class="sj-entry-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="5" width="16" height="14" rx="3"></rect><path d="M8 9h8M8 13h5"></path><path d="M9 5V3m6 2V3"></path></svg><i id="software-job-entry-dot" class="sj-entry-dot"></i></span>
|
||||
<span class="sj-entry-copy">
|
||||
<span class="sj-entry-title">专业软件任务</span>
|
||||
<span id="software-job-count">暂无任务</span>
|
||||
</span>
|
||||
<span class="sj-entry-arrow" aria-hidden="true">›</span>
|
||||
</button>
|
||||
</div>
|
||||
<div id="storage-foot" class="storage-foot" title="">
|
||||
<span id="app-version" title="版本号"></span>
|
||||
<span class="lbl">存储</span>
|
||||
<span class="bar"><i id="storage-foot-bar"></i></span>
|
||||
<span class="txt" id="storage-foot-txt"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="right-jobs-panel" class="right-workspace-panel" role="tabpanel" aria-labelledby="right-tab-jobs" aria-hidden="true">
|
||||
<div class="software-job-summary"><strong>软件作业</strong><span id="software-job-count">暂无作业</span><button id="software-job-refresh" class="small" type="button" title="刷新作业">↻</button></div>
|
||||
<div id="software-job-list" aria-live="polite"></div>
|
||||
</div>
|
||||
<div id="file-droparea">松开以上传到当前目录</div>
|
||||
<input type="file" id="upload-input" multiple style="display:none;" />
|
||||
</div>
|
||||
|
|
@ -2265,16 +2310,6 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div id="software-job-backdrop"></div>
|
||||
<aside id="software-job-panel" aria-hidden="true" aria-label="专业软件任务列表">
|
||||
<div class="sj-panel-head">
|
||||
<span class="sj-panel-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="5" width="16" height="14" rx="3"></rect><path d="M8 9h8M8 13h5"></path><path d="M9 5V3m6 2V3"></path></svg></span>
|
||||
<strong>专业软件任务</strong><span class="spacer"></span>
|
||||
<button id="software-job-close" class="small" type="button" title="关闭">×</button>
|
||||
</div>
|
||||
<div id="software-job-list"></div>
|
||||
</aside>
|
||||
|
||||
<script type="module" src="js/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -589,6 +589,7 @@ function selectDraftFolder(name) {
|
|||
state.draftNewDirMode = false;
|
||||
state.draftWorkingDir = name;
|
||||
setDraftFolderOpen(false);
|
||||
state.filesScope = "all";
|
||||
state.filesPath = name;
|
||||
loadFiles();
|
||||
syncDraftAction();
|
||||
|
|
@ -600,6 +601,7 @@ function selectDraftNewFolder(name) {
|
|||
if (!trimmed) return;
|
||||
state.draftNewDirMode = true;
|
||||
state.draftWorkingDir = trimmed;
|
||||
state.filesScope = "all";
|
||||
state.filesPath = "";
|
||||
loadFiles();
|
||||
setDraftFolderOpen(false);
|
||||
|
|
@ -693,6 +695,7 @@ export async function showNewConversationDraft() {
|
|||
state.outline = [];
|
||||
state.draftNewDirMode = false;
|
||||
state.draftModelProfile = defaultDraftModelProfile();
|
||||
state.filesScope = "all";
|
||||
state.filesPath = "";
|
||||
document.querySelectorAll(".task-row").forEach(el => el.classList.remove("active"));
|
||||
syncChannelCardActive(null);
|
||||
|
|
@ -799,6 +802,7 @@ export async function selectTask(tid) {
|
|||
// 文件面板自动跳到该 task 的 working_dir(user_root 下一级子目录),
|
||||
// 不强绑定 — 用户可点 crumb 回上层看 user_root 其他目录
|
||||
const wdName = meta.working_dir ? meta.working_dir.split("/").filter(Boolean).pop() : "";
|
||||
state.filesScope = wdName ? "task" : "all";
|
||||
state.filesPath = wdName || "";
|
||||
await loadFiles();
|
||||
refreshConcurrentWarnings(); // 同 wd 其他 task 活跃软警告 — 后台 fire-and-forget
|
||||
|
|
@ -1932,6 +1936,7 @@ export async function openSoftwareJobResults(taskId, outputDir) {
|
|||
await selectTask(taskId);
|
||||
const workingDir = state.taskMeta?.working_dir || "";
|
||||
const base = workingDir.split("/").filter(Boolean).pop() || "";
|
||||
state.filesScope = base ? "task" : "all";
|
||||
state.filesPath = [base, outputDir].filter(Boolean).join("/");
|
||||
await loadFiles();
|
||||
}
|
||||
|
|
@ -2633,6 +2638,7 @@ $("chat-stream").addEventListener("click", (e) => {
|
|||
}
|
||||
const inlineImg = e.target.closest && e.target.closest(".art-media-image[data-rel]");
|
||||
if (inlineImg) {
|
||||
if (inlineImg.dataset.previewUrl) return;
|
||||
const rel = inlineImg.dataset.rel;
|
||||
if (rel) openFilePreview(rel, inlineImg.dataset.taskId || "", inlineImg.dataset.legacyPath === "1", inlineImg.dataset.artifactId || "");
|
||||
return;
|
||||
|
|
@ -2713,6 +2719,7 @@ async function createTaskFromDraft() {
|
|||
});
|
||||
state.taskId = t.task_id;
|
||||
state.taskMeta = t;
|
||||
state.filesScope = "task";
|
||||
state.filesPath = workingDir;
|
||||
localStorage.setItem(LS_RECENT_WORKING_DIR, workingDir);
|
||||
$("chat-stream").innerHTML = "";
|
||||
|
|
|
|||
|
|
@ -14,8 +14,21 @@ import { downloadFile } from "./media.js";
|
|||
import { selectTask, loadTaskList } from "./chat.js";
|
||||
import { loadFolderSuggestions } from "./newtask.js";
|
||||
import { dialogConfirm, dialogPrompt, message } from "./dialog.js";
|
||||
import { setRightPanelTab } from "./layout.js";
|
||||
|
||||
// ───── files(user-rooted,不绑 task) ─────
|
||||
let lastFilesData = null;
|
||||
|
||||
function currentTaskFolder() {
|
||||
const raw = state.taskMeta?.working_dir || "";
|
||||
return raw.split("/").filter(Boolean).pop() || "";
|
||||
}
|
||||
|
||||
function resetFileSearch() {
|
||||
state.filesQuery = "";
|
||||
$("file-search").value = "";
|
||||
}
|
||||
|
||||
$("btn-refresh-files").onclick = () => loadFiles();
|
||||
$("btn-toggle-hidden").onclick = () => {
|
||||
state.showHiddenDirs = !state.showHiddenDirs;
|
||||
|
|
@ -29,6 +42,29 @@ $("btn-file-add").onclick = (e) => {
|
|||
]);
|
||||
};
|
||||
$("upload-input").addEventListener("change", uploadSelected);
|
||||
$("files-scope-task").onclick = () => {
|
||||
const folder = currentTaskFolder();
|
||||
if (!folder) return;
|
||||
state.filesScope = "task";
|
||||
state.filesPath = folder;
|
||||
resetFileSearch();
|
||||
loadFiles();
|
||||
};
|
||||
$("files-scope-all").onclick = () => {
|
||||
state.filesScope = "all";
|
||||
state.filesPath = "";
|
||||
resetFileSearch();
|
||||
loadFiles();
|
||||
};
|
||||
$("file-search").oninput = (event) => {
|
||||
state.filesQuery = event.currentTarget.value;
|
||||
if (lastFilesData) renderFiles(lastFilesData);
|
||||
};
|
||||
$("file-sort").onchange = (event) => {
|
||||
state.filesSort = event.currentTarget.value;
|
||||
if (lastFilesData) renderFiles(lastFilesData);
|
||||
};
|
||||
$("file-sort").value = state.filesSort;
|
||||
|
||||
async function createFolder() {
|
||||
const rawName = await dialogPrompt({
|
||||
|
|
@ -187,6 +223,7 @@ function _hasFiles(ev) {
|
|||
$("pane-right").addEventListener("dragenter", (e) => {
|
||||
if (!_hasFiles(e)) return;
|
||||
e.preventDefault();
|
||||
setRightPanelTab("files");
|
||||
_dragDepth++;
|
||||
$("file-droparea").classList.add("show");
|
||||
});
|
||||
|
|
@ -225,6 +262,7 @@ export function scheduleFilesRefresh() {
|
|||
|
||||
export async function loadFiles() {
|
||||
try {
|
||||
if (lastFilesData && lastFilesData.current !== state.filesPath) resetFileSearch();
|
||||
const params = new URLSearchParams();
|
||||
if (state.filesPath) params.set("path", state.filesPath);
|
||||
if (state.showHiddenDirs && state.taskId) {
|
||||
|
|
@ -233,21 +271,50 @@ export async function loadFiles() {
|
|||
}
|
||||
const qs = params.toString();
|
||||
const data = await api("GET", "/v1/files" + (qs ? "?" + qs : ""));
|
||||
lastFilesData = data;
|
||||
renderFiles(data);
|
||||
} catch (e) {
|
||||
if (e.status === 401) { logout(); return; }
|
||||
$("file-crumbs").innerHTML = `<span class="muted">${escapeHtml(e.message)}</span>`;
|
||||
$("file-list").innerHTML = "";
|
||||
lastFilesData = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 切换文件面板浏览路径
|
||||
function navFiles(newPath) {
|
||||
state.filesPath = newPath || "";
|
||||
if (!state.filesPath) state.filesScope = "all";
|
||||
resetFileSearch();
|
||||
loadFiles();
|
||||
}
|
||||
|
||||
function compareFileEntries(a, b) {
|
||||
if (a.is_dir !== b.is_dir) return a.is_dir ? -1 : 1;
|
||||
const [field, direction] = (state.filesSort || "name-asc").split("-");
|
||||
let compared = 0;
|
||||
if (field === "mtime") compared = new Date(a.mtime || 0) - new Date(b.mtime || 0);
|
||||
else if (field === "size") compared = Number(a.size || 0) - Number(b.size || 0);
|
||||
else compared = a.name.localeCompare(b.name, "zh-CN", { numeric: true, sensitivity: "base" });
|
||||
if (compared === 0 && field !== "name") {
|
||||
compared = a.name.localeCompare(b.name, "zh-CN", { numeric: true, sensitivity: "base" });
|
||||
}
|
||||
return direction === "desc" ? -compared : compared;
|
||||
}
|
||||
|
||||
function fileMtime(value) {
|
||||
if (!value) return "";
|
||||
const date = new Date(value);
|
||||
if (Number.isNaN(date.getTime())) return "";
|
||||
return date.toLocaleString("zh-CN", {
|
||||
year: "numeric", month: "2-digit", day: "2-digit",
|
||||
hour: "2-digit", minute: "2-digit", hour12: false,
|
||||
});
|
||||
}
|
||||
|
||||
function renderFiles(data) {
|
||||
$("file-search").value = state.filesQuery;
|
||||
$("file-sort").value = state.filesSort;
|
||||
// 当前所在的"项目"= 路径第一段;空路径 → user_root,无项目上下文
|
||||
const segs = (data.current || "").split("/").filter(Boolean);
|
||||
const projName = segs[0] || "";
|
||||
|
|
@ -255,9 +322,19 @@ function renderFiles(data) {
|
|||
const projShort = projName.length > 12 ? projName.slice(0, 11) + "…" : projName;
|
||||
$("files-proj").textContent = projShort ? "· " + projShort : "· (根目录)";
|
||||
$("files-proj").title = projName || data.root || "";
|
||||
const taskFolder = currentTaskFolder();
|
||||
const taskScope = state.filesScope === "task" && !!taskFolder;
|
||||
const currentInTask = !!taskFolder && (
|
||||
data.current === taskFolder || data.current.startsWith(taskFolder + "/")
|
||||
);
|
||||
$("files-scope-task").disabled = !taskFolder;
|
||||
$("files-scope-task").classList.toggle("active", taskScope);
|
||||
$("files-scope-all").classList.toggle("active", !taskScope);
|
||||
const hiddenToggle = $("btn-toggle-hidden");
|
||||
hiddenToggle.disabled = !currentInTask;
|
||||
hiddenToggle.setAttribute("aria-pressed", state.showHiddenDirs ? "true" : "false");
|
||||
hiddenToggle.title = state.showHiddenDirs ? "隐藏点目录" : "显示点目录";
|
||||
hiddenToggle.title = !currentInTask ? "技术目录仅在当前对话目录内可查看"
|
||||
: (state.showHiddenDirs ? "隐藏技术目录" : "显示技术目录");
|
||||
hiddenToggle.setAttribute("aria-label", hiddenToggle.title);
|
||||
// crumbs root 标"我的"(user_root),更直观;其余原样
|
||||
const cr = data.crumbs.map((c, i) => {
|
||||
|
|
@ -280,15 +357,25 @@ function renderFiles(data) {
|
|||
state.entriesByRel = {};
|
||||
return;
|
||||
}
|
||||
const query = state.filesQuery.trim().toLocaleLowerCase("zh-CN");
|
||||
const visibleEntries = data.entries
|
||||
.filter((entry) => !query || entry.name.toLocaleLowerCase("zh-CN").includes(query))
|
||||
.sort(compareFileEntries);
|
||||
if (!visibleEntries.length) {
|
||||
$("file-list").innerHTML = `<div class="empty">未找到“${escapeHtml(state.filesQuery.trim())}”</div>`;
|
||||
state.entriesByRel = {};
|
||||
for (const e of data.entries) state.entriesByRel[e.rel] = e;
|
||||
$("file-list").innerHTML = data.entries.map((e) => {
|
||||
return;
|
||||
}
|
||||
state.entriesByRel = {};
|
||||
for (const e of visibleEntries) state.entriesByRel[e.rel] = e;
|
||||
$("file-list").innerHTML = visibleEntries.map((e) => {
|
||||
const cls = e.is_dir ? "ico-dir" : "ico-file";
|
||||
const fullTitle = e.rel || e.name;
|
||||
return `
|
||||
<div class="file-row" data-rel="${escapeHtml(e.rel)}" title="${escapeHtml(fullTitle)}">
|
||||
<span class="${cls} name" data-rel="${escapeHtml(e.rel)}" data-isdir="${e.is_dir}" title="${escapeHtml(fullTitle)}">
|
||||
${escapeHtml(e.name)}
|
||||
<span class="file-main">
|
||||
<span class="${cls} name" data-rel="${escapeHtml(e.rel)}" data-isdir="${e.is_dir}" title="${escapeHtml(fullTitle)}">${escapeHtml(e.name)}</span>
|
||||
<span class="mtime">修改 ${escapeHtml(fileMtime(e.mtime))}</span>
|
||||
</span>
|
||||
<span class="size">${humanSize(e.size)}</span>
|
||||
<button class="dd-toggle file-menu" data-rel="${escapeHtml(e.rel)}" title="文件操作">⋯</button>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// 三栏布局:pane 折叠(rail 模式 + localStorage)+ 拖拽 splitter + 手机单列视图。
|
||||
// 顶层即绑定 toggle/splitter/mobile-tab 事件并应用初始状态(import 时执行,DOM 已就绪)。
|
||||
// 只 setMobileView / mqPhone 对外(selectTask 在手机端选中任务时切到对话面板)。
|
||||
// 右栏是应用内工作区:展开时在“文件 / 软件作业”之间切换,折叠时两个
|
||||
// activity icon 仍可达。这一层不依赖外层 header,标准 / embed 共用。
|
||||
import { $ } from "./dom.js";
|
||||
import { LS_LEFT_COLLAPSED, LS_RIGHT_COLLAPSED, LS_LEFT_WIDTH, LS_RIGHT_WIDTH } from "./state.js";
|
||||
|
||||
|
|
@ -33,7 +33,40 @@ function applyRightCollapsed(collapsed) {
|
|||
document.body.classList.toggle("right-collapsed", collapsed);
|
||||
const btn = $("pane-toggle-right");
|
||||
btn.textContent = collapsed ? "‹" : "›";
|
||||
btn.title = collapsed ? "展开文件列表" : "折叠文件列表";
|
||||
btn.title = collapsed ? "展开工作区" : "折叠工作区";
|
||||
}
|
||||
|
||||
let rightPanelTab = "files";
|
||||
|
||||
function syncMobileTabSelection(view) {
|
||||
for (const button of document.querySelectorAll(".mobile-tabs button")) {
|
||||
const sameView = button.dataset.mv === view;
|
||||
const sameRightTab = view !== "mv-right"
|
||||
|| (button.dataset.rightTab || "files") === rightPanelTab;
|
||||
button.classList.toggle("active", sameView && sameRightTab);
|
||||
}
|
||||
}
|
||||
|
||||
export function setRightPanelTab(tab, { expand = true } = {}) {
|
||||
rightPanelTab = tab === "jobs" ? "jobs" : "files";
|
||||
document.body.classList.toggle("right-tab-jobs", rightPanelTab === "jobs");
|
||||
$("right-files-panel").setAttribute("aria-hidden", String(rightPanelTab !== "files"));
|
||||
$("right-jobs-panel").setAttribute("aria-hidden", String(rightPanelTab !== "jobs"));
|
||||
for (const button of document.querySelectorAll("[data-right-panel-tab]")) {
|
||||
const active = button.dataset.rightPanelTab === rightPanelTab;
|
||||
button.classList.toggle("active", active);
|
||||
button.setAttribute("aria-selected", String(active));
|
||||
}
|
||||
if (expand && document.body.classList.contains("right-collapsed")) {
|
||||
localStorage.setItem(LS_RIGHT_COLLAPSED, "");
|
||||
applyRightCollapsed(false);
|
||||
}
|
||||
const mobileView = document.body.classList.contains("mv-right") ? "mv-right"
|
||||
: (document.body.classList.contains("mv-mid") ? "mv-mid" : "mv-left");
|
||||
syncMobileTabSelection(mobileView);
|
||||
document.dispatchEvent(new CustomEvent("right-panel-changed", {
|
||||
detail: { tab: rightPanelTab },
|
||||
}));
|
||||
}
|
||||
$("pane-toggle-left").onclick = () => {
|
||||
const next = !document.body.classList.contains("left-collapsed");
|
||||
|
|
@ -91,13 +124,14 @@ attachPaneSplitter("split-right", "right");
|
|||
// body.mv-{left,mid,right} 控制当前显示的 pane;桌面下三 pane 都可见,本函数仅维护 class
|
||||
// 进入手机视口时清掉 collapsed(只 DOM,不动 localStorage —— 回桌面用户偏好仍生效)
|
||||
export const mqPhone = window.matchMedia("(max-width: 640px)");
|
||||
export function setMobileView(view) {
|
||||
export function setMobileView(view, requestedRightTab = "") {
|
||||
// view ∈ "mv-left" | "mv-mid" | "mv-right"
|
||||
if (view === "mv-right" && requestedRightTab) {
|
||||
setRightPanelTab(requestedRightTab, { expand: false });
|
||||
}
|
||||
document.body.classList.remove("mv-left", "mv-mid", "mv-right");
|
||||
document.body.classList.add(view);
|
||||
for (const b of document.querySelectorAll(".mobile-tabs button")) {
|
||||
b.classList.toggle("active", b.dataset.mv === view);
|
||||
}
|
||||
syncMobileTabSelection(view);
|
||||
}
|
||||
function applyMobileMode() {
|
||||
if (mqPhone.matches) {
|
||||
|
|
@ -116,5 +150,9 @@ function applyMobileMode() {
|
|||
mqPhone.addEventListener("change", applyMobileMode);
|
||||
applyMobileMode();
|
||||
for (const b of document.querySelectorAll(".mobile-tabs button")) {
|
||||
b.onclick = () => setMobileView(b.dataset.mv);
|
||||
b.onclick = () => setMobileView(b.dataset.mv, b.dataset.rightTab || "");
|
||||
}
|
||||
for (const button of document.querySelectorAll("[data-right-panel-tab]")) {
|
||||
button.onclick = () => setRightPanelTab(button.dataset.rightPanelTab);
|
||||
}
|
||||
setRightPanelTab("files", { expand: false });
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// 专业软件任务中心:右栏固定摘要、抽屉列表、游标分页和终态通知。
|
||||
// 软件作业面板:右侧工作区列表、游标分页、活动数与终态未读通知。
|
||||
import { api } from "./api.js";
|
||||
import { state } from "./state.js";
|
||||
import { $ } from "./dom.js";
|
||||
|
|
@ -9,6 +9,7 @@ import {
|
|||
selectTask,
|
||||
} from "./chat.js";
|
||||
import { dialogConfirm, message } from "./dialog.js";
|
||||
import { mqPhone, setMobileView, setRightPanelTab } from "./layout.js";
|
||||
|
||||
const ACTIVE = new Set(["queued", "offered", "dispatched", "running", "disconnected", "cancelling"]);
|
||||
const TERMINAL = new Set(["succeeded", "failed", "cancelled"]);
|
||||
|
|
@ -19,9 +20,9 @@ let timer = null;
|
|||
let known = new Map();
|
||||
let jobs = [];
|
||||
let nextCursor = null;
|
||||
let expanded = false;
|
||||
let loadingMore = false;
|
||||
let initialized = false;
|
||||
const unreadTerminal = new Set();
|
||||
|
||||
const statusLabel = {
|
||||
queued: "等待计算节点", offered: "正在分配节点", dispatched: "节点已接收",
|
||||
|
|
@ -57,15 +58,16 @@ function statusIcon(status) {
|
|||
}
|
||||
|
||||
export function initSoftwareJobs() {
|
||||
if (initialized || !$("software-job-center")) return;
|
||||
if (initialized || !$("software-job-list")) return;
|
||||
initialized = true;
|
||||
$("software-job-toggle").onclick = openDrawer;
|
||||
$("software-job-close").onclick = closeDrawer;
|
||||
$("software-job-backdrop").onclick = closeDrawer;
|
||||
$("software-job-refresh").onclick = refreshSoftwareJobs;
|
||||
$("software-job-list").addEventListener("scroll", onListScroll);
|
||||
document.addEventListener("software-job-submitted", refreshSoftwareJobs);
|
||||
document.addEventListener("keydown", (event) => {
|
||||
if (event.key === "Escape" && expanded) closeDrawer();
|
||||
document.addEventListener("right-panel-changed", (event) => {
|
||||
if (event.detail?.tab === "jobs") {
|
||||
unreadTerminal.clear();
|
||||
render();
|
||||
}
|
||||
});
|
||||
refreshSoftwareJobs();
|
||||
}
|
||||
|
|
@ -146,31 +148,26 @@ document.addEventListener("visibilitychange", () => {
|
|||
if (!document.hidden && state.token) refreshSoftwareJobs(); else schedule();
|
||||
});
|
||||
|
||||
function openDrawer() {
|
||||
expanded = true;
|
||||
render();
|
||||
}
|
||||
|
||||
function closeDrawer() {
|
||||
expanded = false;
|
||||
render();
|
||||
}
|
||||
|
||||
function render() {
|
||||
const active = jobs.filter((job) => ACTIVE.has(job.status));
|
||||
const latest = jobs[0];
|
||||
const unreadJob = jobs.find((job) => unreadTerminal.has(job.job_id));
|
||||
const entryDot = $("software-job-entry-dot");
|
||||
entryDot.className = "sj-entry-dot";
|
||||
if (active.length) entryDot.classList.add("active");
|
||||
else if (latest?.status === "succeeded") entryDot.classList.add("succeeded");
|
||||
else if (latest?.status === "failed") entryDot.classList.add("failed");
|
||||
if (unreadJob?.status === "failed") entryDot.classList.add("failed");
|
||||
else if (unreadJob) entryDot.classList.add("succeeded");
|
||||
else if (active.length) entryDot.classList.add("active");
|
||||
for (const id of ["software-job-badge", "software-job-mobile-badge"]) {
|
||||
const badge = $(id);
|
||||
badge.hidden = active.length === 0;
|
||||
badge.textContent = active.length ? String(active.length) : "";
|
||||
}
|
||||
$("mv-tab-jobs").classList.toggle("has-unread", unreadTerminal.size > 0);
|
||||
$("mv-tab-jobs").classList.toggle("has-unread-failed", unreadJob?.status === "failed");
|
||||
$("software-job-count").textContent = active.length
|
||||
? `${active.length} 个进行中 · ${statusLabel[active[0].status]}`
|
||||
: (latest ? `最近:${statusLabel[latest.status] || latest.status}` : "暂无任务");
|
||||
$("software-job-panel").classList.toggle("open", expanded);
|
||||
$("software-job-panel").setAttribute("aria-hidden", String(!expanded));
|
||||
$("software-job-backdrop").classList.toggle("open", expanded);
|
||||
if (expanded) renderList();
|
||||
: (latest ? `最近:${statusLabel[latest.status] || latest.status}` : "暂无作业");
|
||||
renderList();
|
||||
}
|
||||
|
||||
function renderList() {
|
||||
|
|
@ -313,7 +310,8 @@ async function handleAction(event, button) {
|
|||
const taskId = button.dataset.taskId;
|
||||
if (action === "results") {
|
||||
await openSoftwareJobResults(taskId, jobs.find((item) => item.job_id === jobId)?.output_dir || "");
|
||||
closeDrawer();
|
||||
setRightPanelTab("files");
|
||||
if (mqPhone.matches) setMobileView("mv-right", "files");
|
||||
return;
|
||||
}
|
||||
if (action === "analyze") {
|
||||
|
|
@ -323,7 +321,6 @@ async function handleAction(event, button) {
|
|||
if (taskId) await selectTask(taskId);
|
||||
await refreshCurrentTaskAfterSoftwareJob(taskId);
|
||||
message("已排队分析结果", "success");
|
||||
closeDrawer();
|
||||
refreshSoftwareJobs();
|
||||
} catch (error) {
|
||||
button.disabled = false;
|
||||
|
|
@ -332,13 +329,13 @@ async function handleAction(event, button) {
|
|||
return;
|
||||
}
|
||||
if (taskId) await selectTask(taskId);
|
||||
closeDrawer();
|
||||
}
|
||||
|
||||
function notifyTerminal(job) {
|
||||
const ok = job.status === "succeeded";
|
||||
const label = ok ? "已完成" : (job.status === "cancelled" ? "已取消" : "失败");
|
||||
const summary = job.request_summary || {};
|
||||
if (!document.body.classList.contains("right-tab-jobs")) unreadTerminal.add(job.job_id);
|
||||
message(`${summary.display_name || "专业软件任务"}${label}`, ok ? "success" : "error", 6000);
|
||||
void refreshCurrentTaskAfterSoftwareJob(job.task_id);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,6 +48,9 @@ export const state = {
|
|||
draftNewDirMode: false,
|
||||
draftModelProfile: "",
|
||||
filesPath: "",
|
||||
filesScope: "all", // all=用户根目录视图;task=跟随当前对话工作目录
|
||||
filesQuery: "", // 仅过滤当前已浏览目录
|
||||
filesSort: "name-asc",
|
||||
showHiddenDirs: false, // 仅当前 task 工作目录子树生效;user_root 永不展示
|
||||
// 同 wd 内除自己外其他活跃 task(run_status in running/cancelling),供 banner 显示
|
||||
concurrentWarnings: [],
|
||||
|
|
|
|||
Loading…
Reference in New Issue