diff --git a/CHANGELOG.md b/CHANGELOG.md
index d9cb6d3..df2a2fa 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,6 +13,8 @@
- 素材在右边这一栏里时,鼠标移过卡片不再自动跳进详细说明(之前很难操作),点卡片上的 ⓘ 才看。
- **整个自定义节点包交给 AI**:素材里点“节点”,再在来源那一排点某个自定义节点包,上面会出现“整包交给 AI”。不知道该选哪个节点时,把整个包给它,AI 自己从包里挑;选“只能用”时,这个包里的节点都允许,别的第三方和付费节点照样拦下。AI 用 `get_toolbox` 能看到包里有哪些节点,`find_nodes` 搜索时也把它们排在前面。
- A whole custom node pack can be picked (from its source in the nodes' second column): the AI gets its node types in get_toolbox, finds them first, and with "only" every node of the pack is allowed.
+- 小窗里的“来源”不再是一条要横着拖的长条,改成一个“来源:全部 ▾”按钮:点开是一张列表(来源多时上面有搜索框),点一个就选上并收起;Esc 或点别处也收起。
+- In the side panel the sources (or types) are one "Source: All ▾" button opening a list to pick from, with a search when it is long, instead of a strip to scroll sideways.
- 素材在右边这一栏(小窗)里时改成一行一个的列表:左边是图标或小封面,中间是名字和来源,右边是勾选;搜索框和“全部 / 已选 / 常用”并成一行。同样的高度能看到的条目多了好几倍;展开成大窗口还是原来的卡片。
- In the side panel the materials picker lists one line per item (badge or small cover, name, where it is from, the tick) and puts the search and the filters on one line; the large window keeps the cards.
- AI 工作台和素材窗口里的滚动条变细,平时看不见,鼠标移到能滚动的那一块上才显出来。
diff --git a/docs/guides/mcp.md b/docs/guides/mcp.md
index 25250af..b3b4632 100644
--- a/docs/guides/mcp.md
+++ b/docs/guides/mcp.md
@@ -143,7 +143,7 @@ claude mcp add --transport http anomalous http://127.0.0.1:8188/anomalous/mcp
**让 AI 在画布上搭工作流**:AI 可以先用 `find_nodes` 查这台 ComfyUI 有哪些节点、每个节点要接什么,然后用 `new_workflow` 新开一个标签页(空白,或者一次把整套工作流搭好并排好位置;你正开着的工作流不受影响),再用 `add_node`、`connect_nodes`、`set_values`、`remove_nodes` 加节点、连线、改参数、删节点。每一步你都能在画布上看到,按 Ctrl+Z 能撤回;AI 工作台那一步下面写着改了什么(包括哪根线从哪里连到哪里)。类型接不上、选项不存在时,画布不会动,AI 会收到能用的写法。
-**给 AI 的素材**:窗口右上角的 **✦ 素材** 默认在工作台右边那一栏打开(在别的页面打开时停靠在屏幕右边):不挡时间线和画布,上面搜索,种类是一排标签,卡片两列,点卡片在小窗里看详细说明(“返回列表”回去),下面挑好点交给 AI。右上角 **⤢** 展开成大窗口,大窗口标题栏的侧栏按钮再收回去;AMB 记得你上次用的是哪种。大窗口里:上面搜索所有素材,左边选种类,节点可以按来源筛(ComfyUI 自带、你装的每个第三方节点包、付费 API 节点),卡片上看得到节点的输入输出、搭配和工作流的封面、提示词原文;鼠标移到卡片上右边显示完整说明。提示词里也能挑搭配和工作流里的提示词。不知道该选哪个节点时,在来源那一排点某个自定义节点包,点上面出现的 **整包交给 AI**:AI 会从这个包里挑合适的节点用,“只能用”时这个包的节点都允许。勾选后点 **交给 AI**,选“优先用这些”或“只能用这些节点”(自带节点照常可用,别的第三方和付费节点会被拦下)。☆ 设为常用:没挑素材的任务自动带上常用的。AI 用 `get_toolbox` 看到你挑的东西的详细说明,用 `open_workflow` 在新标签页打开你存的工作流。工作流详情页的 **✦ 交给 AI** 会新开一个带着这个工作流的任务。
+**给 AI 的素材**:窗口右上角的 **✦ 素材** 默认在工作台右边那一栏打开(在别的页面打开时停靠在屏幕右边):不挡时间线和画布,上面搜索,种类是一排标签,卡片两列,点卡片在小窗里看详细说明(“返回列表”回去),下面挑好点交给 AI。右上角 **⤢** 展开成大窗口,大窗口标题栏的侧栏按钮再收回去;AMB 记得你上次用的是哪种。大窗口里:上面搜索所有素材,左边选种类,节点可以按来源筛(ComfyUI 自带、你装的每个第三方节点包、付费 API 节点),卡片上看得到节点的输入输出、搭配和工作流的封面、提示词原文;鼠标移到卡片上右边显示完整说明。提示词里也能挑搭配和工作流里的提示词。不知道该选哪个节点时,在来源里点某个自定义节点包(小窗里点“来源:全部 ▾”打开列表,可以搜索),点上面出现的 **整包交给 AI**:AI 会从这个包里挑合适的节点用,“只能用”时这个包的节点都允许。勾选后点 **交给 AI**,选“优先用这些”或“只能用这些节点”(自带节点照常可用,别的第三方和付费节点会被拦下)。☆ 设为常用:没挑素材的任务自动带上常用的。AI 用 `get_toolbox` 看到你挑的东西的详细说明,用 `open_workflow` 在新标签页打开你存的工作流。工作流详情页的 **✦ 交给 AI** 会新开一个带着这个工作流的任务。
**运行进度**:AI 运行工作流后,时间线上那一步下面有进度条(排队时前面还有几个;运行时哪个节点、第几步、节点做完几个)。进度条右边是百分比,旁边是红色的 **■ 停止**:一点就停这一次运行(还在排队的移出队列,正在跑的立刻中断,只动这次运行),AI 也会收到“你停止了运行”,不会自己再跑一遍。任务工具栏的运行状态按钮旁边也有一个小 ■。进度条下面写着当前节点 **约剩多久**、每步几秒、**显存剩多少**,再下面一行是 ComfyUI **控制台**最新的那一行(比如 `Model Initializing ...`、`Requested to load …`),点开能看最近几行;这些 AI 也同样看得到。出图刚排上队,AMB 会读这次的工作流算出每个采样器要在多大的图上采样:如果是放大模型放大后又整张重新采样、尺寸超过这张显卡的舒适范围、或者要解码特别大的图,会出现黄色提醒,告诉你可能很慢或爆显存,以及怎么改(只想放大就用纯模型超分,或者先缩小、减步数、分块解码);AI 收到同样的提醒,会被要求先问你要不要停(它也能用 `stop_run` 停自己的运行)。AI 用 `run_status` 问同样的进度,或者等到跑完再继续,不会凭时间猜;运行期间 AI 调用别的工具时,回复里的 `your_run` 也带着一句当前进度,跑完后告诉它一次结果,所以它不用自己去翻输出文件夹或者看屏幕。
diff --git a/web/modules/locales.js b/web/modules/locales.js
index 397efb5..7b22dff 100644
--- a/web/modules/locales.js
+++ b/web/modules/locales.js
@@ -1599,6 +1599,8 @@ export const i18n = {
aiTbLiked: "常用",
aiTbSources: "来源",
aiTbNarrow: "分类",
+ aiTbPickSource: "换一个来源或分类",
+ aiTbFindSource: "搜索{what}…",
aiTbEverything: "全部",
aiToolboxKind_packs: "节点包",
aiTbPackName: "{name}(整包)",
@@ -4754,6 +4756,8 @@ export const i18n = {
aiTbLiked: "Favourites",
aiTbSources: "Sources",
aiTbNarrow: "Narrow",
+ aiTbPickSource: "Pick another source or type",
+ aiTbFindSource: "Find a {what}…",
aiTbEverything: "All",
aiToolboxKind_packs: "Node packs",
aiTbPackName: "{name} (whole pack)",
diff --git a/web/modules/ui_ai_icons.js b/web/modules/ui_ai_icons.js
index 81c4fbb..f16c0ca 100644
--- a/web/modules/ui_ai_icons.js
+++ b/web/modules/ui_ai_icons.js
@@ -36,6 +36,7 @@ const PATHS = {
chat: '',
arrowRight: '',
chevronLeft: '',
+ chevronDown: '',
chevronRight: '',
pulse: '',
stop: '',
diff --git a/web/modules/ui_ai_toolbox.js b/web/modules/ui_ai_toolbox.js
index 1915750..cf8523f 100644
--- a/web/modules/ui_ai_toolbox.js
+++ b/web/modules/ui_ai_toolbox.js
@@ -5,7 +5,8 @@
* search over everything, the kinds on the left (node types, combos, workflows,
* prompts, models) with how many are picked, a second column to narrow by source (each custom
* node pack with its badge, ComfyUI's own, paid API nodes), by where a prompt comes from or by
- * model type (a custom node pack can be picked whole there: every node type in it), a grid of
+ * model type (a custom node pack can be picked whole there: every node type in it; in the side
+ * panel that column is one "Source: All ▾" button opening a list to search), a grid of
* cards (ui_ai_toolbox_cards.js) drawn as it scrolls, the card in focus
* in full on the right, and the picks below with how strictly the AI keeps to them. Node types
* are the page's own (no waiting); the rest is read ahead and kept (ai_toolbox_data.js). The
@@ -293,9 +294,69 @@ export async function openToolbox(task, changed = () => {}) {
if (facet && state.picks.packs.includes(facet)) node.classList.add('has-pack');
return node;
};
- side.replaceChildren(el('div', 'anomalous-tb-facets-title', t(state.kind === 'nodes' ? 'aiTbSources' : 'aiTbNarrow')),
- row(null, t('aiTbEverything'), beforeFacet.length),
- ...list.map(facet => row(facet.key, facet.label, facet.count, facet.source ? sourceBadge(facet.source, 22) : null)));
+ const title = t(state.kind === 'nodes' ? 'aiTbSources' : 'aiTbNarrow');
+ const rows = () => [row(null, t('aiTbEverything'), beforeFacet.length),
+ ...list.map(facet => row(facet.key, facet.label, facet.count, facet.source ? sourceBadge(facet.source, 22) : null))];
+ if (!body.classList.contains('is-side')) {
+ side.replaceChildren(el('div', 'anomalous-tb-facets-title', title), ...rows());
+ return;
+ }
+ // The side panel: one button saying what is shown, opening the list to pick from (and search).
+ const current = list.find(facet => facet.key === state.facet);
+ const open = button(`anomalous-tb-facet-open${current && state.picks.packs.includes(current.key) ? ' has-pack' : ''}`, [
+ current?.source ? sourceBadge(current.source, 20) : icon('list', 15),
+ el('span', 'anomalous-tb-facet-open-label', `${title}:${current?.label || t('aiTbEverything')}`),
+ el('span', 'anomalous-tb-facet-count', String(current?.count ?? beforeFacet.length)), icon('chevronDown', 14)],
+ () => facetMenu(title, rows), t('aiTbPickSource'));
+ open.setAttribute('aria-haspopup', 'listbox');
+ side.replaceChildren(open);
+ }
+
+ let menu = null;
+ /** The side panel's list of sources (or types) over the cards: a search when it is long; a pick or Esc closes it. */
+ function facetMenu(title, rows) {
+ if (menu?.isConnected) return void closeMenu();
+ menu = el('div', 'anomalous-tb-facet-menu');
+ menu.dataset.anomalousEscape = '1'; // Esc closes this list, not the browser
+ const items = rows();
+ const list = el('div', 'anomalous-tb-facet-menu-list');
+ list.setAttribute('role', 'listbox');
+ list.append(...items);
+ if (items.length > 8) {
+ const find = el('input', 'anomalous-tb-facet-menu-find');
+ find.type = 'search';
+ find.placeholder = t('aiTbFindSource', { what: title });
+ find.addEventListener('input', () => {
+ const words = find.value.trim().toLowerCase();
+ for (const item of items) item.hidden = Boolean(words) && !item.title.toLowerCase().includes(words);
+ });
+ menu.append(find);
+ requestAnimationFrame(() => find.focus());
+ }
+ menu.append(list);
+ menu.addEventListener('click', (event) => { if (event.target.closest('.anomalous-tb-facet')) closeMenu(); });
+ const top = side.offsetTop + side.offsetHeight;
+ menu.style.top = `${top}px`;
+ menu.style.maxHeight = `${Math.max(160, main.clientHeight - top - 8)}px`;
+ main.append(menu);
+ setTimeout(() => document.addEventListener('pointerdown', outside, true));
+ document.addEventListener('keydown', escape);
+ }
+
+ function outside(event) {
+ if (!menu?.contains(event.target) && !event.target.closest?.('.anomalous-tb-facet-open')) closeMenu();
+ }
+
+ // After the browser's own Esc handler has seen the list's mark (it then keeps the browser open).
+ function escape(event) {
+ if (event.key === 'Escape') setTimeout(closeMenu);
+ }
+
+ function closeMenu() {
+ menu?.remove();
+ menu = null;
+ document.removeEventListener('pointerdown', outside, true);
+ document.removeEventListener('keydown', escape);
}
/** While one custom node pack is shown: its name, how many node types, and picking it whole. */
@@ -313,6 +374,7 @@ export async function openToolbox(task, changed = () => {}) {
let observer = null;
function drawGrid() {
+ closeMenu();
observer?.disconnect();
cards.clear();
const { items, beforeFacet } = shown();
diff --git a/web/styles.css b/web/styles.css
index bef2936..f5c08e9 100644
--- a/web/styles.css
+++ b/web/styles.css
@@ -41,6 +41,6 @@
@import url("./styles/40-ai-side.css?v=20261024-card");
@import url("./styles/41-ai-results.css?v=20261023-tags");
@import url("./styles/42-ai-flow.css?v=20261024-runmap");
-@import url("./styles/43-ai-toolbox.css?v=20261024-packs");
+@import url("./styles/43-ai-toolbox.css?v=20261024-facetmenu");
@import url("./styles/44-ai-status.css?v=20261024-stop");
@import url("./styles/45-ai-side-views.css?v=20261024-bars");
diff --git a/web/styles/43-ai-toolbox.css b/web/styles/43-ai-toolbox.css
index 8011bbd..06176e7 100644
--- a/web/styles/43-ai-toolbox.css
+++ b/web/styles/43-ai-toolbox.css
@@ -464,7 +464,46 @@ body.anomalous-tb-docked #anomalous-container.anomalous-maximized .anomalous-ai
.anomalous-tb.is-side .anomalous-tb-kind-matches { display: none; }
.anomalous-tb.is-side .anomalous-tb-kind.is-on::before { display: none; }
.anomalous-tb.is-side .anomalous-tb-rail, .anomalous-tb.is-side .anomalous-tb-facets { padding: 4px; }
-.anomalous-tb.is-side .anomalous-tb-facet { flex: none; max-width: 170px; min-height: 26px; padding-block: 0; font-size: 12px; }
+/* Sources (or types) as one button and a list under it, not a strip to scroll sideways. */
+.anomalous-tb.is-side .anomalous-tb-facets { overflow: visible; }
+.anomalous-tb-facet-open {
+ display: flex;
+ flex: 1;
+ align-items: center;
+ gap: 8px;
+ min-width: 0;
+ height: 32px;
+ padding: 0 10px 0 8px;
+ border: 1px solid var(--amb-border);
+ border-radius: 8px;
+ background: rgba(var(--amb-ink-rgb), 0.03);
+ color: var(--amb-text-main);
+ font: inherit;
+ font-size: 12.5px;
+ cursor: pointer;
+}
+.anomalous-tb-facet-open:hover { border-color: var(--amb-border-strong, var(--amb-border)); background: rgba(var(--amb-ink-rgb), 0.07); }
+.anomalous-tb-facet-open.has-pack { border-color: var(--amb-link); }
+.anomalous-tb-facet-open-label { flex: 1; min-width: 0; overflow: hidden; text-align: left; text-overflow: ellipsis; white-space: nowrap; }
+.anomalous-tb-facet-menu {
+ position: absolute;
+ z-index: 4;
+ right: 6px;
+ left: 6px;
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ padding: 6px;
+ border: 1px solid var(--amb-border-strong, var(--amb-border));
+ border-radius: 10px;
+ background: var(--amb-bg-panel);
+ box-shadow: 0 14px 40px rgba(0, 0, 0, 0.4);
+}
+.anomalous-tb-facet-menu-find { flex: none; height: 30px; padding: 0 10px; border: 1px solid var(--amb-border); border-radius: 7px; background: var(--amb-bg-input, transparent); color: var(--amb-text-main); font: inherit; font-size: 12.5px; outline: 0; }
+.anomalous-tb-facet-menu-find:focus { border-color: var(--amb-link); }
+.anomalous-tb-facet-menu-list { display: flex; flex-direction: column; gap: 1px; min-height: 0; overflow-y: auto; }
+.anomalous-tb-facet-menu .anomalous-tb-facet { flex: none; width: 100%; min-height: 32px; padding: 0 8px; border: 0; border-radius: 7px; background: none; color: var(--amb-text-soft); font: inherit; font-size: 12.5px; cursor: pointer; }
+.anomalous-tb-facet-menu .anomalous-tb-facet[hidden] { display: none; }
.anomalous-tb.is-side .anomalous-tb-grid-head { display: none; }
.anomalous-tb.is-side .anomalous-tb-scroller { padding: 4px; }
.anomalous-tb.is-side .anomalous-tb-grid { grid-template-columns: minmax(0, 1fr); gap: 2px; }