diff --git a/.agents/docs/components-package.md b/.agents/docs/components-package.md
index f09dc60a59..4d4ebfcf13 100644
--- a/.agents/docs/components-package.md
+++ b/.agents/docs/components-package.md
@@ -262,6 +262,12 @@ does not name a control. The Preferences switches use this association with the
existing translated copy. See the
[decision and browser regression](../notes/implemented/bug-fix/2026-10-01-preferences-switch-names.md).
+Machine-list filters compose `Menu.Content width="compact"` and
+`Menu.CheckboxItem indicatorSide="end"`. The trigger names this two-option menu,
+so the popup contains only the independent filters. The shared checkable row
+keeps keyboard and multi-select behavior; its existing start-aligned indicator
+remains the default for other menus.
+
`--input` is the theme's raw `input.background` and doubles as a muted chip/composer
slab, so in a light theme it can sit below the page color and read as a disabled field.
`--input-field` is derived in `lib/vscode-theme/vscode-theme-css.ts` as the lighter of
diff --git a/.agents/notes/assets/machine-filter-menu-density/checked.png b/.agents/notes/assets/machine-filter-menu-density/checked.png
new file mode 100644
index 0000000000..94d2f525a1
Binary files /dev/null and b/.agents/notes/assets/machine-filter-menu-density/checked.png differ
diff --git a/.agents/notes/assets/machine-filter-menu-density/unchecked.png b/.agents/notes/assets/machine-filter-menu-density/unchecked.png
new file mode 100644
index 0000000000..6ee3a04afe
Binary files /dev/null and b/.agents/notes/assets/machine-filter-menu-density/unchecked.png differ
diff --git a/.agents/notes/implemented/bug-fix/2026-10-20-machine-filter-menu-density.md b/.agents/notes/implemented/bug-fix/2026-10-20-machine-filter-menu-density.md
new file mode 100644
index 0000000000..40ef52d9a9
--- /dev/null
+++ b/.agents/notes/implemented/bug-fix/2026-10-20-machine-filter-menu-density.md
@@ -0,0 +1,49 @@
+# Compact machine filter menu
+
+Status: implemented
+Translation: current
+
+PR: [#1743](https://github.com/LodyAI/Lody/pull/1743)
+
+[中文](2026-10-20-machine-filter-menu-density.zh.md)
+
+## Abstract
+
+The machine filter popup reserved a leading tick column even when both filters
+were off, leaving the short labels indented inside an oversized menu. It now
+uses the existing compact width and puts selection ticks after the labels.
+Removing the redundant heading and separator leaves just the two filter rows.
+Online and My machines remain independent checkboxes, with keyboard navigation
+and an open menu while changing either option.
+
+## Decision
+
+The [menu family](../feature/2026-09-11-ui-menu-primitives.md) keeps the mark box
+reserved so labels cannot shift when a tick appears. `Menu.CheckboxItem` gains
+an optional `indicatorSide="end"`, matching the radio row's existing placement
+option. It moves that box after the label; the default remains at the start.
+The product uses this option with `width="compact"` (180px minimum), without
+changing shared menu dimensions or row styles. Longer labels can still expand
+beyond the minimum. The trigger retains its translated accessible name.
+
+The UI gallery's compact example demonstrates trailing ticks. The owning menu
+suite exercises both placements with pointer and keyboard activation, and the
+settings browser suite checks the real machine filter and resulting list.
+
+## Verification
+
+The menu and gallery suites pass all 52 tests. English and Chinese browser cases
+pass: the menu is 180px wide and 64px tall, labels begin within 14px of its edge,
+and checking does not shift them. The real list changes from four machines to
+two online machines, then one online owned machine; keyboard toggling and
+reopening preserve the selected filters. Both package type checks and targeted
+typed lint pass. Before submission, `pnpm format` and the full `pnpm check`
+also passed, including workspace type checks, lint, tests, and boundary checks.
+Documentation checking retains the existing six missing Kimi/Pi submodule links
+in the bilingual subagent-events note; no new documentation errors were added.
+
+## Screenshots
+
+
+
+
diff --git a/.agents/notes/implemented/bug-fix/2026-10-20-machine-filter-menu-density.zh.md b/.agents/notes/implemented/bug-fix/2026-10-20-machine-filter-menu-density.zh.md
new file mode 100644
index 0000000000..b52eeba412
--- /dev/null
+++ b/.agents/notes/implemented/bug-fix/2026-10-20-machine-filter-menu-density.zh.md
@@ -0,0 +1,42 @@
+# 紧凑的机器过滤菜单
+
+Status: implemented
+Translation: current
+
+PR: [#1743](https://github.com/LodyAI/Lody/pull/1743)
+
+[English](2026-10-20-machine-filter-menu-density.md)
+
+## 摘要
+
+机器过滤菜单在两个条件都未选中时仍保留前方勾选列,短标签因此缩进,菜单也显得过大。
+现在使用现有的紧凑宽度,将勾选标记放到文字后方。
+去掉重复标题和分隔线后,菜单只保留两行筛选条件。
+“在线”和“我的机器”仍可独立选择,保留键盘导航,切换条件时菜单保持打开。
+
+## 决定
+
+[菜单组件族](../feature/2026-09-11-ui-menu-primitives.md) 保留标记的空间,避免勾选出现时
+文字移动。`Menu.CheckboxItem` 新增可选的 `indicatorSide="end"`,与已有单选行的
+位置选项保持一致,将标记区域移到文字后方;默认仍在前方。
+产品组合该选项与 `width="compact"`(最小宽度 180px),没有修改共享菜单的尺寸
+或行样式。更长标签仍可撑大菜单。触发按钮保留已有翻译的可访问名称。
+
+UI 图库中的紧凑示例展示右侧勾选。所属菜单测试覆盖两种位置的鼠标和键盘激活,
+设置页面浏览器测试检查真实机器过滤菜单及过滤后的列表。
+
+## 验证
+
+菜单与图库测试共 52 项全部通过。中英文浏览器案例通过:菜单宽 180px、高 64px,
+标签起点距离菜单边缘小于 14px,勾选不会使文字移动。真实列表从四台机器变为两台
+在线机器,再变为一台在线且属于自己的机器;键盘切换和重新打开菜单均保留筛选状态。
+两个包的类型检查与针对修改文件的类型感知 lint 均通过。提交前的 `pnpm format`
+和完整 `pnpm check` 也已通过,包含工作区类型检查、lint、测试与边界检查。
+文档检查仍有 subagent-events 双语笔记中原有的六个 Kimi/Pi 子模块缺失链接,
+没有新增文档错误。
+
+## 截图
+
+
+
+
diff --git a/packages/components/src/components/settings/machine-tab-list.tsx b/packages/components/src/components/settings/machine-tab-list.tsx
index 8e714d9fe9..6a7b02986a 100644
--- a/packages/components/src/components/settings/machine-tab-list.tsx
+++ b/packages/components/src/components/settings/machine-tab-list.tsx
@@ -163,11 +163,7 @@ export function MachineListFilterButton({
}
/>
-
-
- {t('settings.agent.machineTabs.filter.label', 'Filter machines')}
-
-
+
void;
}) {
return (
- onSelect()}>
+ onSelect()}
+ >
{label}
);
diff --git a/packages/components/tests/e2e/desktop-settings-layout.spec.ts b/packages/components/tests/e2e/desktop-settings-layout.spec.ts
index 9b2fbed16c..8b6dcad789 100644
--- a/packages/components/tests/e2e/desktop-settings-layout.spec.ts
+++ b/packages/components/tests/e2e/desktop-settings-layout.spec.ts
@@ -381,3 +381,53 @@ for (const locale of ['en', 'zh_CN']) {
).toHaveCount(0);
});
}
+
+for (const { locale, triggerName, onlineName, mineName } of [
+ { locale: 'en', triggerName: 'Filter machines', onlineName: 'Online', mineName: 'My machines' },
+ { locale: 'zh_CN', triggerName: '过滤机器', onlineName: '在线', mineName: '我的机器' },
+]) {
+ test(`machine filters keep labels unindented in a compact menu (${locale})`, async ({ page }) => {
+ await page.setViewportSize({ width: 900, height: 600 });
+ await page.goto(
+ `/iframe.html?id=settings-machinetablist--default&viewMode=story&globals=theme:dark;locale:${locale}`
+ );
+ const trigger = page.getByRole('button', { name: triggerName, exact: true });
+ const machines = page.locator('button[data-settings-machine-id]');
+ await expect(machines).toHaveCount(4);
+ await trigger.click();
+ const menu = page.getByRole('menu');
+ const online = menu.getByRole('menuitemcheckbox', { name: onlineName, exact: true });
+ const mine = menu.getByRole('menuitemcheckbox', { name: mineName, exact: true });
+ await expect(online).toBeVisible();
+ await expect(mine).toHaveAttribute('aria-checked', 'false');
+ const box = await menu.boundingBox();
+ expect(box!.width).toBeLessThanOrEqual(182);
+ expect(box!.height).toBeLessThanOrEqual(72);
+ const labelLeft = await online.evaluate(
+ (row) => row.firstElementChild!.getBoundingClientRect().left
+ );
+ expect(labelLeft - box!.x).toBeLessThanOrEqual(14);
+ await online.click();
+ await expect(online).toHaveAttribute('aria-checked', 'true');
+ await expect(machines).toHaveCount(2);
+ await expect(online.locator(':scope > span:last-child svg')).toBeVisible();
+ expect(
+ await online.evaluate((row) => row.firstElementChild!.getBoundingClientRect().left)
+ ).toBe(labelLeft);
+ await mine.click();
+ await expect(mine).toHaveAttribute('aria-checked', 'true');
+ await expect(machines).toHaveCount(1);
+ await page.keyboard.press('ArrowUp');
+ await expect(online).toBeFocused();
+ await page.keyboard.press('Space');
+ await expect(online).toHaveAttribute('aria-checked', 'false');
+ await expect(machines).toHaveCount(2);
+ await page.keyboard.press('Escape');
+ await expect(menu).toBeHidden();
+ await trigger.click();
+ await expect(mine).toHaveAttribute('aria-checked', 'true');
+ await mine.click();
+ await expect(machines).toHaveCount(4);
+ await expect(menu).toBeVisible();
+ });
+}
diff --git a/packages/ui/README.md b/packages/ui/README.md
index 545ed7081a..5ab54c269b 100644
--- a/packages/ui/README.md
+++ b/packages/ui/README.md
@@ -204,6 +204,10 @@ had there, through Base UI's `render`:
`width="compact"` for a short two-choice menu; the regular width remains the
default. Width is a primitive variant, not a caller class override.
+`Menu.CheckboxItem indicatorSide="end"` places the tick after the label for
+compact filters. It reserves the trailing mark box in both states, keeping the
+label aligned without a leading indent. The default remains `start`.
+
`ContextMenu` and `Menubar` restate only the way in — a right click or a long
press, and a bar of names — and re-export `Menu`'s rows rather than rebuilding
them, so a command looks and behaves the same wherever a person meets it. A row
diff --git a/packages/ui/src/gallery/gallery.tsx b/packages/ui/src/gallery/gallery.tsx
index 8fc3dbca59..c5bc279ef5 100644
--- a/packages/ui/src/gallery/gallery.tsx
+++ b/packages/ui/src/gallery/gallery.tsx
@@ -1844,8 +1844,10 @@ function CompactMenu() {
}>Two choices
- Local
- Worktree
+
+ Online
+
+ My machines
);
diff --git a/packages/ui/src/menu/menu.tsx b/packages/ui/src/menu/menu.tsx
index ba161e9a71..9f3748a3c8 100644
--- a/packages/ui/src/menu/menu.tsx
+++ b/packages/ui/src/menu/menu.tsx
@@ -93,7 +93,10 @@ export interface MenuLinkItemProps
}
export interface MenuCheckboxItemProps
- extends Omit, Omit {}
+ extends Omit, Omit {
+ /** Place the tick after the label to leave the start of short filter rows unindented. */
+ indicatorSide?: 'start' | 'end';
+}
export interface MenuRadioItemProps
extends Omit, MenuRowProps {
@@ -342,10 +345,24 @@ export const MenuLinkItem = forwardRef(fun
* Base UI unmounts the tick while the row is unticked, so an icon sharing that
* box would slide sideways every time the row was toggled. A row that needs a
* glyph as well puts it in `endContent`. The box itself is always reserved, so
- * the list does not reflow as the tick appears.
+ * the list does not reflow as the tick appears. Short filters can move this box
+ * to the end with `indicatorSide="end"`, leaving the label unindented.
*/
export const MenuCheckboxItem = forwardRef(
- function MenuCheckboxItem({ className, children, shortcut, endContent, ...rest }, ref) {
+ function MenuCheckboxItem(
+ { className, children, shortcut, endContent, indicatorSide = 'start', ...rest },
+ ref
+ ) {
+ const mark = (
+
+ }
+ >
+
+
+
+ );
return (
rowClassName(state, 'neutral', className)}
>
- }
- >
-
-
-
- }
+ leading={indicatorSide === 'start' ? mark : undefined}
shortcut={shortcut}
- endContent={endContent}
+ endContent={
+ indicatorSide === 'end' ? (
+ <>
+ {endContent}
+ {mark}
+ >
+ ) : (
+ endContent
+ )
+ }
>
{children}
diff --git a/packages/ui/test/menu.test.tsx b/packages/ui/test/menu.test.tsx
index 7f11f13d22..1e66a70146 100644
--- a/packages/ui/test/menu.test.tsx
+++ b/packages/ui/test/menu.test.tsx
@@ -275,29 +275,39 @@ describe('Menu row', () => {
});
describe('Menu checkbox and radio rows', () => {
- test('a checkbox row announces its state and toggles without closing the menu', async () => {
- function Toggles() {
- const [notify, setNotify] = useState(false);
- return (
-
-
- Notify me
-
-
- );
+ test.each(['start', 'end'] as const)(
+ 'a checkbox row with its mark at %s toggles by pointer and keyboard without closing',
+ async (indicatorSide) => {
+ function Toggles() {
+ const [notify, setNotify] = useState(false);
+ return (
+
+
+ Notify me
+
+
+ );
+ }
+ mounted = await mount();
+ await click(trigger());
+ const row = () => rows('menuitemcheckbox')[0];
+ expect(row().getAttribute('aria-checked')).toBe('false');
+
+ await click(row());
+
+ expect(row().getAttribute('aria-checked')).toBe('true');
+ // A setting is toggled, not chosen: the menu stays up so a second one can
+ // be toggled without reopening it.
+ expect(trigger().getAttribute('aria-expanded')).toBe('true');
+ await press('Enter');
+ expect(row().getAttribute('aria-checked')).toBe('false');
+ expect(trigger().getAttribute('aria-expanded')).toBe('true');
}
- mounted = await mount();
- await click(trigger());
- const row = () => rows('menuitemcheckbox')[0];
- expect(row().getAttribute('aria-checked')).toBe('false');
-
- await click(row());
-
- expect(row().getAttribute('aria-checked')).toBe('true');
- // A setting is toggled, not chosen: the menu stays up so a second one can
- // be toggled without reopening it.
- expect(trigger().getAttribute('aria-expanded')).toBe('true');
- });
+ );
test('a radio group holds one value and marks the row that has it', async () => {
function Sort() {