// 新建任务弹框:可选任务名 / 可搜索工作目录组合框(复用“开始一段新对话”交互)/ // 描述 / 智能体(skill)/ 模型 select,提交 POST /v1/tasks。 // 顶层自绑 hd-new-custom 打开、nt-go 提交、各 input 联动;唯一对外导出 loadFolderSuggestions //(供 main enterApp 初始化顶部 filter-wd、files 新建/复制/移动后刷新目录列表)。 import { $ } from "./dom.js"; import { state } from "./state.js"; import { api } from "./api.js"; import { escapeHtml } from "./format.js"; import { logout } from "./auth.js"; import { loadModels, loadTaskList, selectTask } from "./chat.js"; // ───── new task ───── const LS_RECENT_WORKING_DIR = "zcbot.recent-working-dir"; let customWorkingDir = ""; let customDirNewMode = false; let customDirActiveIndex = -1; $("hd-new-custom").onclick = async () => { $("nt-name").value = ""; customWorkingDir = ""; customDirNewMode = false; $("nt-wd-picker").value = ""; $("nt-wd-picker").dataset.searching = ""; setCustomFolderOpen(false); $("nt-desc").value = ""; $("nt-skill").value = ""; $("nt-err").textContent = ""; $("new-task-modal").classList.add("show"); await Promise.all([loadFolderSuggestions(), loadSkillOptions(), loadModels()]); renderCustomFolderOptions(); syncCustomDirHint(); populateModelSelect(); $("nt-name").focus(); }; function populateModelSelect() { const sel = $("nt-model"); const models = state.models || []; if (models.length === 0) { sel.innerHTML = ``; return; } sel.innerHTML = models.map(m => `` ).join(""); } $("nt-cancel").onclick = () => $("new-task-modal").classList.remove("show"); $("nt-go").onclick = async () => { const name = $("nt-name").value.trim(); const working_dir = currentCustomWorkingDir(); const desc = $("nt-desc").value.trim(); const skill = $("nt-skill").value; const model_profile = $("nt-model").value; $("nt-err").textContent = ""; if (!working_dir) { $("nt-err").textContent = "请选择已有工作目录或创建新目录"; return; } try { const body = { working_dir, description: desc, skill, model_profile }; if (name) body.name = name; const t = await api("POST", "/v1/tasks", body); localStorage.setItem(LS_RECENT_WORKING_DIR, working_dir); $("new-task-modal").classList.remove("show"); await loadTaskList(); selectTask(t.task_id); } catch (e) { if (e.status === 401) { logout(); return; } $("nt-err").textContent = e.message; } }; // 工作目录:拉数据 + 灌顶部 filter-wd,并刷新自定义创建组合框。 export async function loadFolderSuggestions() { try { const data = await api("GET", "/v1/folders"); state.folders = data.folders || []; } catch (e) { state.folders = state.folders || []; } populateFolderSelects(); renderCustomFolderOptions(); } // 灌顶部 filter-wd;自定义创建弹框使用与快速新对话一致的组合框。 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; // 重渲后恢复选中 } // 智能体类型下拉:skill registry 服务器端静态,首次加载后缓存到 state.skills async function loadSkillOptions() { const sel = $("nt-skill"); if (!state.skills) { try { const data = await api("GET", "/v1/skills"); state.skills = data.skills || []; } catch (e) { state.skills = []; // 静默兜底,select 仍保留"(默认)"项 } } // 渲染:第一项固定为"默认"(空 value),其后逐 skill 一项 const opts = ['']; for (const s of state.skills) { const label = `${s.name}${s.description ? " — " + s.description : ""}`; opts.push(``); } sel.innerHTML = opts.join(""); sel.value = ""; // hd-new 已清空,这里幂等再保一次 } // === 自定义创建目录组合框(与“开始一段新对话”一致)=== // UI 新契约:工作目录始终必填,任务名选填;不再用任务名自动补目录。 function currentCustomWorkingDir() { return customWorkingDir.trim(); } function customDirIsNew() { return !!currentCustomWorkingDir() && customDirNewMode; } function syncCustomDirHint() { const target = currentCustomWorkingDir(); $("nt-wd-hint").textContent = target ? customDirIsNew() ? `将新建工作目录「${target}」` : `文件将保存到「${target}」` : "可选择已有目录,也可以新建目录。"; } function renderCustomFolderOptions() { const input = $("nt-wd-picker"); const list = $("nt-wd-options"); if (!input || !list) return; const query = input.dataset.searching === "1" ? input.value.trim().toLocaleLowerCase() : ""; const recent = localStorage.getItem(LS_RECENT_WORKING_DIR) || ""; const folders = [...(state.folders || [])]; folders.sort((a, b) => Number(b.name === recent) - Number(a.name === recent)); const visible = query ? folders.filter(f => f.name.toLocaleLowerCase().includes(query)) : folders; const exact = query ? folders.some(f => f.name.toLocaleLowerCase() === query) : false; const selected = customDirIsNew() ? "" : currentCustomWorkingDir(); const existingHtml = visible.map((f, i) => { const tag = f.n_tasks ? `${f.n_tasks} 个任务` : "空目录"; const recentTag = f.name === recent ? `最近使用` : ""; const chosen = f.name === selected ? " selected" : ""; return ``; }).join(""); const createHtml = query && !exact ? `` : `