-
+ {/* Hidden on ultra-narrow (<360px) viewports — the 320px header is
+ already at capacity (see the 320x700 component test). */}
+
+
+
diff --git a/packages/app/src/components/ui/command-palette.tsx b/packages/app/src/components/ui/command-palette.tsx
index 5757fdf85..ff463687c 100644
--- a/packages/app/src/components/ui/command-palette.tsx
+++ b/packages/app/src/components/ui/command-palette.tsx
@@ -233,9 +233,9 @@ export function CommandPalette() {
const clampedIndex = Math.min(activeIndex, Math.max(0, flatEntries.length - 1));
const scrollRowIntoView = (index: number) => {
- listRef.current
- ?.querySelector(`[data-palette-index="${index}"]`)
- ?.scrollIntoView({ block: 'nearest' });
+ const row = listRef.current?.querySelector(`[data-palette-index="${index}"]`);
+ // scrollIntoView is missing from some DOM test environments.
+ if (row && typeof row.scrollIntoView === 'function') row.scrollIntoView({ block: 'nearest' });
};
const moveActive = (delta: number) => {
From 6fa7e56c3f4c759bb362c4240656c2ac5e8e48da Mon Sep 17 00:00:00 2001
From: functionstackx <47992694+functionstackx@users.noreply.github.com>
Date: Fri, 28 Aug 2026 00:10:10 +0000
Subject: [PATCH 3/4] =?UTF-8?q?fix:=20address=20Bugbot=20review=20?=
=?UTF-8?q?=E2=80=94=20IME=20Enter,=20locale=20query=20params,=20shortcut?=
=?UTF-8?q?=20close=20reset?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Ignore Enter while an IME composition is being confirmed (isComposing /
keyCode 229), so committing CJK input no longer runs the selection
- Locale action now keeps the current query string and uses the same
commit-retry push as the header language toggle
- Closing via Cmd/Ctrl+K now routes through handleOpenChange so the next
open starts with a cleared query
中文:修复 Bugbot 审查发现的三个问题——输入法(IME)确认组合输入时不再
触发选中项;语言切换操作保留当前查询参数并复用页眉语言切换的导航重试
逻辑;通过快捷键关闭面板时同样清空搜索词。
---
.../components/ui/command-palette.test.tsx | 35 ++++++++++++++
.../app/src/components/ui/command-palette.tsx | 46 +++++++++++++------
2 files changed, 66 insertions(+), 15 deletions(-)
diff --git a/packages/app/src/components/ui/command-palette.test.tsx b/packages/app/src/components/ui/command-palette.test.tsx
index 5734d0a76..fbc98bcce 100644
--- a/packages/app/src/components/ui/command-palette.test.tsx
+++ b/packages/app/src/components/ui/command-palette.test.tsx
@@ -115,6 +115,41 @@ describe('CommandPalette', () => {
expect(active).not.toBeNull();
});
+ it('ignores Enter while an IME composition is being confirmed', () => {
+ render();
+ openViaTrigger();
+ setQuery('kimi k3');
+ const input = document.body.querySelector(
+ '[data-testid="command-palette-input"]',
+ ) as HTMLInputElement;
+ act(() => {
+ input.dispatchEvent(
+ new KeyboardEvent('keydown', { key: 'Enter', isComposing: true, bubbles: true }),
+ );
+ });
+ expect(push).not.toHaveBeenCalled();
+ // The palette stays open, still showing the query.
+ expect(document.body.querySelector('[data-testid="command-palette"]')).not.toBeNull();
+ });
+
+ it('clears the query when closed via the keyboard shortcut', () => {
+ render();
+ openViaTrigger();
+ setQuery('kimi');
+ // Close and reopen via Ctrl+K — the old filter must not persist.
+ act(() => {
+ document.dispatchEvent(new KeyboardEvent('keydown', { key: 'k', ctrlKey: true }));
+ });
+ expect(document.body.querySelector('[data-testid="command-palette"]')).toBeNull();
+ act(() => {
+ document.dispatchEvent(new KeyboardEvent('keydown', { key: 'k', ctrlKey: true }));
+ });
+ const input = document.body.querySelector(
+ '[data-testid="command-palette-input"]',
+ ) as HTMLInputElement;
+ expect(input.value).toBe('');
+ });
+
it('shows an empty state for unmatched queries', () => {
render();
openViaTrigger();
diff --git a/packages/app/src/components/ui/command-palette.tsx b/packages/app/src/components/ui/command-palette.tsx
index ff463687c..8b0889d43 100644
--- a/packages/app/src/components/ui/command-palette.tsx
+++ b/packages/app/src/components/ui/command-palette.tsx
@@ -22,6 +22,7 @@ import {
import { hasZhSibling, switchLocalePath, zhPath } from '@/lib/i18n';
import { pushInApp } from '@/lib/client-navigation';
import { useClientPathname } from '@/hooks/useClientPathname';
+import { useClientSearch } from '@/hooks/useClientSearch';
import { useLocale } from '@/lib/use-locale';
import { cn } from '@/lib/utils';
import { matchesSearch } from '@/lib/search-match';
@@ -103,6 +104,7 @@ export function CommandPalette() {
// Live pathname: per-model dashboard routes rewrite the URL outside the
// Next router, which usePathname alone would miss (same as LanguageToggle).
const pathname = useClientPathname(routerPathname);
+ const search = useClientSearch();
const locale = useLocale();
const t = STRINGS[locale];
const { setTheme, theme } = useTheme();
@@ -123,28 +125,37 @@ export function CommandPalette() {
track('command_palette_opened', { source });
}, []);
+ const handleOpenChange = React.useCallback((nextOpen: boolean) => {
+ setOpen(nextOpen);
+ if (!nextOpen) {
+ setQuery('');
+ setActiveIndex(0);
+ }
+ }, []);
+
+ // Mirror of `open` for the document-level shortcut listener, so toggling
+ // goes through handleOpenChange (which resets the query on close).
+ const openRef = React.useRef(false);
+ React.useEffect(() => {
+ openRef.current = open;
+ }, [open]);
+
// Global ⌘K / Ctrl+K shortcut. Toggles, so a second press closes.
React.useEffect(() => {
const onKeyDown = (event: KeyboardEvent) => {
if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === 'k' && !event.altKey) {
event.preventDefault();
- setOpen((prev) => {
- if (!prev) track('command_palette_opened', { source: 'shortcut' });
- return !prev;
- });
+ if (openRef.current) {
+ handleOpenChange(false);
+ } else {
+ handleOpenChange(true);
+ track('command_palette_opened', { source: 'shortcut' });
+ }
}
};
document.addEventListener('keydown', onKeyDown);
return () => document.removeEventListener('keydown', onKeyDown);
- }, []);
-
- const handleOpenChange = React.useCallback((nextOpen: boolean) => {
- setOpen(nextOpen);
- if (!nextOpen) {
- setQuery('');
- setActiveIndex(0);
- }
- }, []);
+ }, [handleOpenChange]);
const navItems = React.useMemo(() => buildPaletteNavItems(locale), [locale]);
@@ -178,7 +189,9 @@ export function CommandPalette() {
keywords: t.switchLocaleKeywords,
icon: LanguagesIcon,
run: () => {
- router.push(switchLocalePath(pathname));
+ // Same contract as the header language toggle: keep the current
+ // query string (dashboard filters) and use the commit-retry push.
+ pushInApp(router, switchLocalePath(pathname) + search);
},
},
{
@@ -191,7 +204,7 @@ export function CommandPalette() {
},
},
],
- [t, theme, setTheme, router, pathname],
+ [t, theme, setTheme, router, pathname, search],
);
const sections = React.useMemo
(() => {
@@ -261,6 +274,9 @@ export function CommandPalette() {
setActiveIndex(flatEntries.length - 1);
scrollRowIntoView(flatEntries.length - 1);
} else if (event.key === 'Enter') {
+ // Ignore the Enter that confirms an IME composition (CJK input),
+ // otherwise committing Chinese text would also run the selection.
+ if (event.nativeEvent.isComposing || event.keyCode === 229) return;
event.preventDefault();
flatEntries[clampedIndex]?.select();
}
From 275bec50f59077f3b05932e87535a4dac9463212 Mon Sep 17 00:00:00 2001
From: functionstackx <47992694+functionstackx@users.noreply.github.com>
Date: Fri, 28 Aug 2026 00:45:04 +0000
Subject: [PATCH 4/4] =?UTF-8?q?fix:=20address=20Bugbot=20round=202=20?=
=?UTF-8?q?=E2=80=94=20same-page=20no-op,=20unofficialruns,=20dashboard=20?=
=?UTF-8?q?keywords?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Selecting the current page in the palette is now a no-op instead of a
refetch that wipes dashboard filters (matches header link behavior)
- Dashboard tab jumps carry the unofficialruns param, same as TabNav
- Dashboard destinations are searchable via the header label the user
actually sees: "dashboard" and 仪表板 in both locales
中文:修复 Bugbot 第二轮审查——在面板中选择当前页面不再重新加载(避免
清空仪表板筛选条件);仪表板标签跳转保留 unofficialruns 参数;仪表板
条目可通过页眉实际显示的「Dashboard/仪表板」关键词搜索到。
---
.../components/ui/command-palette.test.tsx | 12 +++++++++++-
.../app/src/components/ui/command-palette.tsx | 19 +++++++++++++++++--
.../app/src/lib/command-palette-items.test.ts | 11 +++++++++++
packages/app/src/lib/command-palette-items.ts | 4 +++-
4 files changed, 42 insertions(+), 4 deletions(-)
diff --git a/packages/app/src/components/ui/command-palette.test.tsx b/packages/app/src/components/ui/command-palette.test.tsx
index fbc98bcce..52962d5c0 100644
--- a/packages/app/src/components/ui/command-palette.test.tsx
+++ b/packages/app/src/components/ui/command-palette.test.tsx
@@ -159,7 +159,7 @@ describe('CommandPalette', () => {
});
it('navigates to the /zh sibling and renders Chinese labels on /zh pages', () => {
- mockPathname = '/zh';
+ mockPathname = '/zh/glossary';
render();
openViaTrigger();
const input = document.body.querySelector(
@@ -170,4 +170,14 @@ describe('CommandPalette', () => {
pressOnInput('Enter');
expect(push).toHaveBeenCalledWith('/zh');
});
+
+ it('treats selecting the current page as a no-op', () => {
+ // Re-pushing the same route would only wipe live dashboard filters.
+ mockPathname = '/zh';
+ render();
+ openViaTrigger();
+ setQuery('首页');
+ pressOnInput('Enter');
+ expect(push).not.toHaveBeenCalled();
+ });
});
diff --git a/packages/app/src/components/ui/command-palette.tsx b/packages/app/src/components/ui/command-palette.tsx
index 8b0889d43..de56a3f07 100644
--- a/packages/app/src/components/ui/command-palette.tsx
+++ b/packages/app/src/components/ui/command-palette.tsx
@@ -159,14 +159,29 @@ export function CommandPalette() {
const navItems = React.useMemo(() => buildPaletteNavItems(locale), [locale]);
+ // Dashboard tab jumps carry the unofficialruns param, same as TabNav.
+ const unofficialIds = React.useMemo(() => {
+ for (const [key, value] of new URLSearchParams(search)) {
+ if (/^unofficialruns?$/iu.test(key) && value) return value;
+ }
+ return '';
+ }, [search]);
+
const selectNav = React.useCallback(
(item: PaletteNavItem) => {
const target = locale === 'zh' && hasZhSibling(item.href) ? zhPath(item.href) : item.href;
track('command_palette_selected', { id: item.id, query });
handleOpenChange(false);
- pushInApp(router, target);
+ // Selecting the current page is a no-op — a refetch would only wipe the
+ // dashboard filters (header links behave the same way).
+ if (target === pathname) return;
+ const href =
+ item.group === 'dashboard' && unofficialIds
+ ? `${target}?unofficialruns=${unofficialIds}`
+ : target;
+ pushInApp(router, href);
},
- [locale, query, router, handleOpenChange],
+ [locale, query, router, handleOpenChange, pathname, unofficialIds],
);
const actionItems = React.useMemo(
diff --git a/packages/app/src/lib/command-palette-items.test.ts b/packages/app/src/lib/command-palette-items.test.ts
index 9792e0008..94ef354ea 100644
--- a/packages/app/src/lib/command-palette-items.test.ts
+++ b/packages/app/src/lib/command-palette-items.test.ts
@@ -49,6 +49,17 @@ describe('buildPaletteNavItems', () => {
}
});
+ it('dashboard tabs are searchable via the header label in both locales', () => {
+ // The header calls this section "Dashboard" / 「仪表板」 — the words users
+ // actually see must hit every dashboard destination.
+ for (const items of [en, zh]) {
+ for (const item of items.filter((i) => i.group === 'dashboard')) {
+ expect(item.keywords).toContain('dashboard');
+ expect(item.keywords).toContain('仪表板');
+ }
+ }
+ });
+
it('every href with a zh sibling resolves; dashboard/page hrefs are mirrored', () => {
// All palette destinations should exist in the English tree; the mirrored
// ones (all of them today) must round-trip through hasZhSibling.
diff --git a/packages/app/src/lib/command-palette-items.ts b/packages/app/src/lib/command-palette-items.ts
index 532008ffa..b2a77d6a6 100644
--- a/packages/app/src/lib/command-palette-items.ts
+++ b/packages/app/src/lib/command-palette-items.ts
@@ -101,7 +101,9 @@ export function buildPaletteNavItems(locale: Locale): PaletteNavItem[] {
group: 'dashboard',
href: route.path,
label: locale === 'zh' ? TAB_LABELS_ZH[key] : TAB_LABELS_EN[key],
- keywords: locale === 'zh' ? TAB_LABELS_EN[key] : TAB_LABELS_ZH[key],
+ // Include the header's "Dashboard" label (both locales) so the words
+ // users actually see in the nav also hit these destinations.
+ keywords: `${locale === 'zh' ? TAB_LABELS_EN[key] : TAB_LABELS_ZH[key]} dashboard 仪表板`,
};
});