排序
@@ -785,8 +806,10 @@
-
-
+
@@ -1359,9 +1382,9 @@ const _taskScrollObserver = new IntersectionObserver((entries) => {
}
}, { root: $("pane-left"), rootMargin: "200px 0px" });
_taskScrollObserver.observe($("task-sentinel"));
-// 工作目录输入框打开 enterApp 时拉一次 datalist(modal 也复用同一 list)
+// 顶部 filter-wd combobox 首次 focus 拉一次 folders;modal 打开会重拉(数据可能变)
async function ensureFoldersLoaded() {
- if ($("folders-datalist").children.length === 0) await loadFolderSuggestions();
+ if (!state.folders) await loadFolderSuggestions();
}
$("filter-wd").addEventListener("focus", ensureFoldersLoaded);
@@ -2825,6 +2848,7 @@ $("hd-new").onclick = async () => {
$("nt-err").textContent = "";
$("nt-wd-hint").textContent = "";
wdManuallyEdited = false;
+ wdCombo.hide();
$("new-task-modal").classList.add("show");
await Promise.all([loadFolderSuggestions(), loadSkillOptions(), loadModels()]);
populateModelSelect();
@@ -2841,7 +2865,7 @@ function populateModelSelect() {
`
`
).join("");
}
-$("nt-cancel").onclick = () => $("new-task-modal").classList.remove("show");
+$("nt-cancel").onclick = () => { wdCombo.hide(); $("new-task-modal").classList.remove("show"); };
$("nt-go").onclick = async () => {
const name = $("nt-name").value.trim();
// wd 走联动 + fallback:正常情况已跟随 name;若用户清空了 wd 也兜底用 name
@@ -2854,6 +2878,7 @@ $("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);
@@ -2863,22 +2888,14 @@ $("nt-go").onclick = async () => {
}
};
-// 工作目录:打开 modal 时拉一次,灌共享的 datalist(顶部筛选 filter-wd 和 modal nt-wd-sel 都用它)
-// 同时缓存到 state.folders 供 hint 判断"命中已有"/"新建"
+// 工作目录:供 combobox panel 和 hint 共用,缓存到 state.folders
async function loadFolderSuggestions() {
- let folders = [];
try {
const data = await api("GET", "/v1/folders");
- folders = data.folders || [];
+ state.folders = data.folders || [];
} catch (e) {
- // 静默 — datalist 为空,combobox 仍可手输
+ state.folders = state.folders || []; // 失败保留旧值;首次失败也置空数组,combobox 显示"无已有目录"
}
- state.folders = folders;
- const dl = $("folders-datalist");
- dl.innerHTML = folders.map((f) => {
- const tag = f.n_tasks ? `${f.n_tasks} 个任务` : `空目录`;
- return `
`;
- }).join("");
}
// 智能体类型下拉:skill registry 服务器端静态,首次加载后缓存到 state.skills
@@ -2919,12 +2936,92 @@ function updateWdHint() {
hint.innerHTML = `
→ 新建目录 ${escapeHtml(v)}`;
}
}
+
+// === 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
$("nt-name").addEventListener("input", () => {
if (!wdManuallyEdited) $("nt-wd-sel").value = $("nt-name").value;
updateWdHint();
});
-// wd 改变 → 非空视为手动修改;清空视为"恢复跟随",仅重置 flag(不立刻填回,避免打断输入)
+// wd 改变(用户打字) → 非空视为手动修改;清空 → 重置 flag 但保持空(避免 backspace 想换名字时被打断)
$("nt-wd-sel").addEventListener("input", () => {
wdManuallyEdited = $("nt-wd-sel").value.trim() !== "";
updateWdHint();