diff --git a/PROGRESS.md b/PROGRESS.md index 6861d0d..7dc5815 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -23,6 +23,7 @@ ### 2026-05-21 +- **新建任务弹窗 combobox 从 datalist 改自定义 dropdown(modal + 顶部 filter-wd 双处)**:接上一条迭代 —— `` 在 wd 因联动有了值之后,浏览器按前缀过滤把下拉过滤没了,用户只能点右侧三角看完整列表,体验比原 `` 改成 ``,删 `+ 新建目录…` sentinel + 二级 `nt-wd-new` 输入框;加 `wdManuallyEdited` flag —— name 输入时若 flag=false 自动同步到 wd(programmatic 改 value 不触发 wd input 事件不会假阳性),wd 非空输入置 flag=true 脱钩,wd 清空重置 flag=false 但保持空(避免 backspace 想换名字时被立刻填回打断);submit 保留 `working_dir || name` fallback 兜底空值。`loadFolderSuggestions` 不再渲染 select options,只灌共享 datalist + 缓存到 `state.folders` 供 hint 比对"命中已有/新建"。label 文案 `(可选,留空 → 用任务名...)` → `(默认跟随任务名;可输入新名或选已有目录复用)`,更直观。 - **system prompt 注入 task 预选 skill 提示**:`core/agent_builder.py::_build_system_prompt` 加 `task_skill` 参数,非空时在"工作目录与 task 上下文"段加一行 `- **task 预选 skill**: \`\` — 用户创建时声明的主 skill`;空字符串走老路径,prompt 字节级一致。LLM 拿到这条事实 + `general_v1.md:17-23` 已有的"对应 skill 领域先 load_skill" 规则自然组合 → 主动 load。否决"直接把完整 SKILL.md 预注入 prompt"方案 —— 那会把 `tasks.skill` 从 metadata 升格成 binding,需要同步改 DESIGN.md / 想清楚 PATCH 改 skill 的语义,投入产出比不划算;轻量提示保渐进披露三层架构不动。 - **imagegen skill 加 ⛔ 调 tool 前必须贴 prompt 给用户 + BLOCKING 等确认硬约束**:用户反馈之前流程"模糊就问"不够,清楚的描述也可能模型脑里和用户脑里对不上,事后看图才发现白烧 ¥0.22。改:① 顶部流程一句话加"⛔ 把 prompt 完整贴给用户看 + 问改不改 → 用户明确确认后 → 调 seedream"步骤;② 加「调 tool 前的强制门(铁律)」段定义回复分类(可以/OK/画吧/嗯 算确认;改 X → 重贴重等;沉默/追问别的 → 继续等;模棱两可 → 追问到明确);③ 加「调 tool 前再过一道」段给具体贴 prompt 的对话格式(代码块 + 参数清单 + 预计花费 + 一句"开烧?改什么?");④ 调用范式段加"前置条件:已拿到明确确认才调";⑤ 反模式加两条(没贴就调 / 模棱两可当确认)。本质是把"模型脑内装配"摊到对话层让用户最后过一眼,装配 ≠ 授权调用。:用户反馈 skill 缺图片比例引导。原 SKILL 里 size 表写"比例只能正方形"是基于 doubao.yaml + tool 参数描述只列三个正方形例子的间接推断,无验证。改:① 诊断五维 → 六维,加"比例/尺寸"(ppt 16:9 / 海报 9:16 / 头像 1:1 / 公众号 2.35:1 / 书籍 3:4);② 一次性追问范式加比例项,上下文推断里给"做 ppt/海报/公众号/学术示意"四种用途的默认比例;③ size 参数表重写成"按用途选比例,再选分辨率",列常见 size 参考值 + 明确"非方形是按比例算的参考值,豆包是否原生支持需首次小调用验证";④ 失败解药表加比例错(改 size 不动 prompt)+ API 报错回退默认两条;⑤ 反模式加"不问比例就默认走 yaml 1:1"。承认 unknown:豆包 5.0 实际支持哪些非方形 size 没验证,首次用错就回退默认 + 让用户协商,不臆造。:两根因 —— ① `general_v1.md` 「媒体生成工具」段把 `seedream` 写成一级直觉(列了"画/出/来张"等关键词 + 直接调 tool 的 how-to),压过 skill discovery block 的微弱声音;② imagegen description 关键词覆盖窄(没有"画/绘制/艺术图/图片"等朴素词)。修法:system prompt 那段改成"调 seedream 前**必须先 `load_skill('imagegen')`**",细节判断全移到 skill 里,只留 ¥0.22 计费 + 不装饰生成 + 不连发三条兜底硬约束;imagegen description 扩 17 个触发词(画/绘制/出图/来张/艺术图/写实图/场景图...)。两层联动:一级 prompt 指引到 skill,二级 description 提匹配概率。 diff --git a/web/static/dev.html b/web/static/dev.html index 39317e8..617d97d 100644 --- a/web/static/dev.html +++ b/web/static/dev.html @@ -525,6 +525,24 @@ #new-task-modal label { display: block; margin-top: 8px; font-size: 12px; color: var(--muted); } #new-task-modal .err { color: var(--accent); font-size: 12px; margin-top: 8px; min-height: 1em; } #new-task-modal .actions { margin-top: 14px; display: flex; gap: 8px; justify-content: flex-end; } + /* 自定义 combobox(替代原生 datalist —— datalist 在 input 非空时下拉被前缀过滤丢失) */ + .combo { position: relative; } + .combo-panel { + position: absolute; top: calc(100% + 2px); left: 0; right: 0; + background: #fff; border: 1px solid var(--border); border-radius: 6px; + box-shadow: 0 6px 16px rgba(0,0,0,0.12); + max-height: 220px; overflow-y: auto; z-index: 10; + display: none; + } + .combo-panel.show { display: block; } + .combo-panel .item { + display: flex; justify-content: space-between; align-items: baseline; + padding: 6px 10px; cursor: pointer; gap: 12px; font-size: 13px; + } + .combo-panel .item:hover, + .combo-panel .item.active { background: var(--hover); } + .combo-panel .item .tag { color: var(--muted); font-size: 11px; flex-shrink: 0; } + .combo-panel .empty { padding: 8px 10px; color: var(--muted); font-size: 12px; text-align: center; } /* ───── file preview modal ───── */ #file-preview-modal { @@ -696,7 +714,10 @@
- +
+ +
+
排序 @@ -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();