排序
@@ -805,11 +786,11 @@
新建任务
-
-
+
+
+
@@ -1202,6 +1183,7 @@ function enterApp() {
loadTaskList();
loadFiles(); // 文件面板与 task 解耦 — 启动即拉 user_root
loadModels(); // 模型清单缓存:chat-meta 下拉 + 新建对话框 + 历史小标
+ loadFolderSuggestions(); // 灌 filter-wd select(modal 打开时会重拉,这里让左 pane 先有选项)
}
async function loadModels() {
@@ -1363,16 +1345,15 @@ function taskMenuItems(t) {
// 筛选 / 排序 / 刷新 一律 reset(loadTaskList 默认 append=false);追加由 sentinel observer 触发
$("filter-status").onchange = () => loadTaskList();
$("filter-order").onchange = () => loadTaskList();
+$("filter-wd").onchange = () => loadTaskList(); // select 选完立即筛
$("btn-refresh-tasks").onclick = () => loadTaskList();
-// 搜索 / 工作目录筛选:debounce 300ms,避免每个字符都打 API
+// 搜索 q 是 text input → 300ms debounce 避免每字符打 API
let _filterDebounce = null;
-function scheduleFilter() {
+$("filter-q").addEventListener("input", () => {
clearTimeout(_filterDebounce);
_filterDebounce = setTimeout(() => loadTaskList(), 300);
-}
-$("filter-q").addEventListener("input", scheduleFilter);
-$("filter-wd").addEventListener("input", scheduleFilter);
+});
// 滚动加载:左 pane 整体是 scroll 容器(.pane{overflow:auto}),用 #pane-left 作 root
// rootMargin 提前 200px 触发,体感更顺;阈值 0 即可(刚进入即触发,append 期间 taskLoading 自带防抖)
@@ -1382,11 +1363,6 @@ const _taskScrollObserver = new IntersectionObserver((entries) => {
}
}, { root: $("pane-left"), rootMargin: "200px 0px" });
_taskScrollObserver.observe($("task-sentinel"));
-// 顶部 filter-wd combobox 首次 focus 拉一次 folders;modal 打开会重拉(数据可能变)
-async function ensureFoldersLoaded() {
- if (!state.folders) await loadFolderSuggestions();
-}
-$("filter-wd").addEventListener("focus", ensureFoldersLoaded);
// ───── select task ─────
async function selectTask(tid) {
@@ -2839,18 +2815,20 @@ async function uploadSelected() {
}
// ───── new task ─────
-// wd 跟随 name 自动同步;用户一旦手动改 wd → 脱钩;若再清空 wd → 恢复跟随
+// wd 二级 input 默认跟随 name;用户一旦手改二级 input → 脱钩;清空二级 input 重置 flag
let wdManuallyEdited = false;
$("hd-new").onclick = async () => {
$("nt-name").value = "";
- $("nt-wd-sel").value = "";
+ $("nt-wd-sel").value = "__new__"; // 默认选 sentinel
+ $("nt-wd-new").value = "";
+ $("nt-wd-new").style.display = ""; // sentinel 选中态 → 二级 input 可见
$("nt-desc").value = ""; $("nt-skill").value = "";
$("nt-err").textContent = "";
$("nt-wd-hint").textContent = "";
wdManuallyEdited = false;
- wdCombo.hide();
$("new-task-modal").classList.add("show");
await Promise.all([loadFolderSuggestions(), loadSkillOptions(), loadModels()]);
+ $("nt-wd-sel").value = "__new__"; // populateFolderSelects 重渲后再保险一次
populateModelSelect();
$("nt-name").focus();
};
@@ -2865,11 +2843,14 @@ function populateModelSelect() {
`
`
).join("");
}
-$("nt-cancel").onclick = () => { wdCombo.hide(); $("new-task-modal").classList.remove("show"); };
+$("nt-cancel").onclick = () => $("new-task-modal").classList.remove("show");
$("nt-go").onclick = async () => {
const name = $("nt-name").value.trim();
- // wd 走联动 + fallback:正常情况已跟随 name;若用户清空了 wd 也兜底用 name
- const working_dir = $("nt-wd-sel").value.trim() || name;
+ const sel = $("nt-wd-sel").value;
+ // sentinel:用二级 input 值,空则 fallback name;选已有目录:直接用 value
+ const working_dir = sel === "__new__"
+ ? ($("nt-wd-new").value.trim() || name)
+ : sel;
const desc = $("nt-desc").value.trim();
const skill = $("nt-skill").value;
const model_profile = $("nt-model").value;
@@ -2878,7 +2859,6 @@ $("nt-go").onclick = async () => {
try {
const t = await api("POST", "/v1/tasks",
{ name, working_dir, description: desc, skill, model_profile });
- wdCombo.hide();
$("new-task-modal").classList.remove("show");
await loadTaskList();
selectTask(t.task_id);
@@ -2888,14 +2868,41 @@ $("nt-go").onclick = async () => {
}
};
-// 工作目录:供 combobox panel 和 hint 共用,缓存到 state.folders
+// 工作目录:拉数据 + 灌两个 select(顶部 filter-wd 和 modal nt-wd-sel)
async function loadFolderSuggestions() {
try {
const data = await api("GET", "/v1/folders");
state.folders = data.folders || [];
} catch (e) {
- state.folders = state.folders || []; // 失败保留旧值;首次失败也置空数组,combobox 显示"无已有目录"
+ state.folders = state.folders || [];
}
+ populateFolderSelects();
+}
+
+// 灌 filter-wd + nt-wd-sel options;保留当前选中值
+function populateFolderSelects() {
+ const folders = state.folders || [];
+ // 顶部 filter:第一项 "(全部目录)" sentinel
+ const filterSel = $("filter-wd");
+ const filterCur = filterSel.value;
+ const filterOpts = ['
'];
+ for (const f of folders) {
+ const tag = f.n_tasks ? `${f.n_tasks} 个任务` : `空目录`;
+ filterOpts.push(`
`);
+ }
+ filterSel.innerHTML = filterOpts.join("");
+ filterSel.value = filterCur; // 重渲后恢复选中
+ // modal wd:第一项 "+ 新建(跟随任务名)" sentinel(label 由 updateSentinelLabel 实时刷)
+ const wdSel = $("nt-wd-sel");
+ const wdCur = wdSel.value || "__new__";
+ const wdOpts = [`
`];
+ for (const f of folders) {
+ const tag = f.n_tasks ? `${f.n_tasks} 个任务` : `空目录`;
+ wdOpts.push(`
`);
+ }
+ wdSel.innerHTML = wdOpts.join("");
+ wdSel.value = wdCur;
+ updateSentinelLabel(); // 用最新的 name 刷 sentinel
}
// 智能体类型下拉:skill registry 服务器端静态,首次加载后缓存到 state.skills
@@ -2919,111 +2926,66 @@ async function loadSkillOptions() {
sel.value = ""; // hd-new 已清空,这里幂等再保一次
}
+// === modal wd select + 二级 input 联动 ===
+// select 选 "__new__" sentinel → 显示二级 input(默认值跟随 name);选已有目录 → 隐藏二级 input
+// wdManuallyEdited:用户改过二级 input 后置 true,name 不再覆盖;清空二级 input 重置 false
+
+function updateSentinelLabel() {
+ // sentinel 永远是 select 第一项,labels 实时含 name 让用户一眼知会建什么
+ const sel = $("nt-wd-sel");
+ const opt = sel.options[0];
+ if (!opt || opt.value !== "__new__") return;
+ const name = $("nt-name").value.trim();
+ opt.textContent = name ? `+ 新建「${name}」` : `+ 新建(跟随任务名)`;
+}
+
function updateWdHint() {
- const v = $("nt-wd-sel").value.trim();
const hint = $("nt-wd-hint");
- if (!v) {
- // wd 空 → 提交时会 fallback 到 name(见 nt-go);若 name 也空则不显示
- const fallback = $("nt-name").value.trim();
- hint.textContent = fallback ? `→ 用任务名「${fallback}」作目录` : "";
- return;
- }
- const hit = (state.folders || []).find(f => f.name === v);
- if (hit) {
- const n = hit.n_tasks || 0;
- hint.innerHTML = `
→ 复用已有目录 · ${n} 个任务`;
+ const sel = $("nt-wd-sel").value;
+ if (sel === "__new__") {
+ const v = $("nt-wd-new").value.trim();
+ const name = $("nt-name").value.trim();
+ const target = v || name;
+ if (!target) { hint.textContent = ""; return; }
+ // 用户手输的新名恰好命中已有目录 → 提示会复用而非新建
+ const collision = (state.folders || []).find(f => f.name === target);
+ if (collision) {
+ const n = collision.n_tasks || 0;
+ hint.innerHTML = `
! 已有同名目录,将复用 · ${n} 个任务`;
+ } else {
+ hint.innerHTML = `
→ 新建目录 ${escapeHtml(target)}`;
+ }
} else {
- hint.innerHTML = `
→ 新建目录 ${escapeHtml(v)}`;
+ const f = (state.folders || []).find(x => x.name === sel);
+ const n = f ? (f.n_tasks || 0) : 0;
+ hint.innerHTML = `
→ 复用已有目录 · ${n} 个任务`;
}
}
-// === folder combobox 工厂(自定义,代替 datalist —— datalist 在 input 非空时下拉被前缀过滤丢失) ===
-// 数据源统一走 state.folders(loadFolderSuggestions 维护);onPick(name) 是选中后的回调
-function makeFolderCombo({ input, panel, onPick }) {
- let filtered = [];
- let activeIdx = -1;
- function render() {
- const all = state.folders || [];
- const f = (input.value || "").trim().toLowerCase();
- filtered = all.filter(x => !f || x.name.toLowerCase().includes(f));
- if (filtered.length === 0) {
- panel.innerHTML = `
${all.length === 0 ? "无已有目录" : "无匹配 · 将新建"}
`;
- return;
- }
- panel.innerHTML = filtered.map((x, i) => {
- const tag = x.n_tasks ? `${x.n_tasks} 个任务` : `空目录`;
- return `
`
- + `${escapeHtml(x.name)}`
- + `${escapeHtml(tag)}`
- + `
`;
- }).join("");
- }
- function show() { activeIdx = -1; render(); panel.classList.add("show"); }
- function hide() { panel.classList.remove("show"); activeIdx = -1; }
- function pick(idx) {
- const f = filtered[idx];
- if (!f) return;
- input.value = f.name;
- hide();
- if (onPick) onPick(f.name);
- }
- // 内部:打字 → 实时过滤 + 保持显示(外部 input listener 仍可注册业务副作用,顺序无依赖)
- input.addEventListener("input", () => { activeIdx = -1; render(); panel.classList.add("show"); });
- input.addEventListener("focus", show);
- input.addEventListener("click", show);
- // blur 延迟关闭,让 panel item 的 mousedown 来得及触发(mousedown 也 preventDefault 双保险)
- input.addEventListener("blur", () => setTimeout(hide, 120));
- panel.addEventListener("mousedown", (e) => {
- const item = e.target.closest(".item");
- if (!item) return;
- e.preventDefault(); // 阻止 input 失焦提前关 panel
- pick(parseInt(item.dataset.idx));
- });
- // 键盘:↑↓ 移动 active,Enter 选中,Esc 关闭
- input.addEventListener("keydown", (e) => {
- if (!panel.classList.contains("show")) return;
- if (e.key === "ArrowDown") {
- e.preventDefault();
- if (filtered.length === 0) return;
- activeIdx = (activeIdx + 1) % filtered.length;
- render();
- } else if (e.key === "ArrowUp") {
- e.preventDefault();
- if (filtered.length === 0) return;
- activeIdx = activeIdx <= 0 ? filtered.length - 1 : activeIdx - 1;
- render();
- } else if (e.key === "Enter" && activeIdx >= 0) {
- e.preventDefault();
- pick(activeIdx);
- } else if (e.key === "Escape") {
- e.preventDefault();
- hide();
- }
- });
- return { show, hide, render };
-}
-
-// modal 新建任务的工作目录:选中后置 manual flag + 刷新 hint
-const wdCombo = makeFolderCombo({
- input: $("nt-wd-sel"),
- panel: $("nt-wd-panel"),
- onPick: () => { wdManuallyEdited = true; updateWdHint(); },
-});
-// 顶部 filter-wd:选中后立即触发筛选(打字仍走 scheduleFilter debounce)
-makeFolderCombo({
- input: $("filter-wd"),
- panel: $("filter-wd-panel"),
- onPick: () => loadTaskList(),
-});
-
-// name 改变 → 若 wd 未被手动改,同步;并刷新 hint
+// name 改变 → 更新 sentinel label;若未脱钩且当前是 sentinel,二级 input 跟随 name
$("nt-name").addEventListener("input", () => {
- if (!wdManuallyEdited) $("nt-wd-sel").value = $("nt-name").value;
+ updateSentinelLabel();
+ if (!wdManuallyEdited && $("nt-wd-sel").value === "__new__") {
+ $("nt-wd-new").value = $("nt-name").value;
+ }
updateWdHint();
});
-// wd 改变(用户打字) → 非空视为手动修改;清空 → 重置 flag 但保持空(避免 backspace 想换名字时被打断)
-$("nt-wd-sel").addEventListener("input", () => {
- wdManuallyEdited = $("nt-wd-sel").value.trim() !== "";
+
+// wd select 切换 → 切显示二级 input + 刷 hint
+$("nt-wd-sel").addEventListener("change", () => {
+ const v = $("nt-wd-sel").value;
+ if (v === "__new__") {
+ $("nt-wd-new").style.display = "";
+ if (!wdManuallyEdited) $("nt-wd-new").value = $("nt-name").value;
+ } else {
+ $("nt-wd-new").style.display = "none";
+ }
+ updateWdHint();
+});
+
+// 二级 input 改变 → 非空视为手动修改;清空重置 flag 但保持空(避免 backspace 想换名时被打断)
+$("nt-wd-new").addEventListener("input", () => {
+ wdManuallyEdited = $("nt-wd-new").value.trim() !== "";
updateWdHint();
});