Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ export default {
await page.getByRole('menuitem', { name: await ui.t('ccAgent.sidebar.filterCreatorsAny'), exact: true }).click();
await page.waitForFunction(() => document.querySelectorAll('[data-task]').length === 7);
await page.locator('[data-task="remote"] [data-testid="bot-follow-mark"]').waitFor();
await page.getByRole('menuitem', { name: await ui.t('ccAgent.sidebar.filterCreatorsNone'), exact: true }).click();
await page.waitForFunction(() => document.querySelectorAll('[data-task]').length === 1);
assert.equal(await rows.getAttribute('data-task'), 'owner');
await ui.capture('hide-all');
const local = page.getByRole('menuitemcheckbox', { name: 'Dash', exact: true });
const remote = page.getByRole('menuitemcheckbox', { name: 'Lizi MacBook Pro', exact: true });
await local.click();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ function props(groupDevice: boolean): ProjectsSectionProps {
unclassified: [], projects: [], dialogues: [], allKnownProjects: [], allProjectKeysForOrder: ['local:known-project'],
bots: [{ botId: 'demo', displayName: 'Demo Bot', avatar: '', avatarColor: '', sessions, latestActivityAt: sessions[0].updatedAt }],
filter: {
creators: 'all', creatorOptions: [], toggleCreator: vi.fn(), setCreatorsAll: vi.fn(),
creators: 'all', creatorOptions: [], toggleCreator: vi.fn(), setCreatorsAll: vi.fn(), setCreatorsNone: vi.fn(),
groupBy: 'project', groupDialogue: true, groupDevice, sortBy: 'recency',
projectOrder: 'activity', manualProjectOrder: [], projects: 'all', status: 'active', isFilterActive: false,
projectsAsSet: null, isSessionContentFiltered: false, vendor: 'all', lastActivity: 'all', manualPinnedOrder: [],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ function props(groupDevice: boolean): ProjectsSectionProps {
allProjectKeysForOrder: [],
bots: [],
filter: {
creators: 'all', creatorOptions: [], toggleCreator: vi.fn(), setCreatorsAll: vi.fn(),
creators: 'all', creatorOptions: [], toggleCreator: vi.fn(), setCreatorsAll: vi.fn(), setCreatorsNone: vi.fn(),
groupBy: 'project',
groupDialogue: true,
groupDevice,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -901,6 +901,9 @@ describe('creator filter selection and owner persistence', () => {
persistCreators(['a', 'b'], OWNER_ID);
expect(loadCreators(OWNER_ID)).toEqual(['a', 'b']);
expect(loadCreators('owner-b')).toBe('all');
persistCreators('none', OWNER_ID);
expect(loadCreators(OWNER_ID)).toBe('none');
expect(nextCreatorsAfterToggle('none', 'a')).toEqual(['a']);
persistCreators('all', OWNER_ID);
expect(loadCreators(OWNER_ID)).toBe('all');
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,7 @@ it('offers one Cindy across computers without merging a custom namesake or unrel
const tasks = [session({ id: 'local-child' }), session({ id: 'remote-child', deviceLinkDeviceId: 'laptop', status: 'archived' }),
session({ id: 'custom-child', deviceLinkDeviceId: 'laptop' }), session()];
expect(tasks.filter(task => matchesTaskCreators(task, selected, result.current.index))).toEqual(tasks.slice(0, 2));
expect(tasks.filter(task => matchesTaskCreators(task, 'none', result.current.index))).toEqual([tasks[3]]);
h.remote = [...h.remote, { ...bot('new-cindy', 'Cindy', { templateId: 'cindy' }), deviceId: 'new-host', delegatedSessionIds: ['new-child'] }];
rerender();
expect(result.current.options).toHaveLength(3);
Expand Down Expand Up @@ -284,6 +285,8 @@ it.each(['routine', 'channel'])('uses exact local/remote ownership for %s avatar
expect(matchesTaskCreators(remote, [localKey], result.current)).toBe(false);
expect(matchesTaskCreators(remote, [remoteKey], result.current)).toBe(true);
expect(matchesTaskCreators(local, [localKey, remoteKey], result.current)).toBe(true);
expect(matchesTaskCreators(local, 'none', result.current)).toBe(false);
expect(matchesTaskCreators(remote, 'none', new Map())).toBe(false);
});

// Unit / review4390134785: existing coverage starts with a real roster, missing
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -214,13 +214,14 @@ function taskCreatorSessionKey(id: string, deviceId?: string): string {

export function matchesTaskCreators(
session: Session,
selected: 'all' | readonly string[],
selected: 'all' | 'none' | readonly string[],
index: ReadonlyMap<string, ReadonlySet<string>>,
): boolean {
if (selected === 'all') return true;
const creators = isTeammateTaskSession(session) && session.companion
? index.get(`creator:${taskBotKey({ id: session.companion.id, deviceId: session.deviceLinkDeviceId })}`)
: index.get(taskCreatorSessionKey(session.id, session.deviceLinkDeviceId));
if (selected === 'none') return !isTeammateTaskSession(session) && !creators?.size;
return selected.some(key => creators?.has(key));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ const log = createLogger('SidebarFilterCore');
export const STATUS_KEY = 'cc-agent.sidebar.filter.status';
export const PROJECTS_KEY = 'cc-agent.sidebar.filter.projects';
export const CREATORS_KEY = 'cc-agent.sidebar.filter.creators';
export type FilterCreators = 'all' | string[];
export type FilterCreators = 'all' | 'none' | string[];

export const VENDOR_KEY = 'cc-agent.sidebar.filter.vendor';
export const GROUP_BY_KEY = 'cc-agent.sidebar.filter.groupBy';
Expand Down Expand Up @@ -151,6 +151,7 @@ export function loadCreators(ownerId: string | null): FilterCreators {
if (!raw) return 'all';
try {
const value: unknown = JSON.parse(raw);
if (value === 'none') return 'none';
if (!Array.isArray(value)) return 'all';
const keys = [...new Set(value.filter((key): key is string => typeof key === 'string' && key.length > 0))];
return keys.length ? keys : 'all';
Expand All @@ -166,15 +167,15 @@ export function normalizeTaskCreators(
selected: FilterCreators,
options: readonly { filterKey?: string; filterMemberKeys?: readonly string[] }[],
): FilterCreators {
if (selected === 'all') return selected;
if (selected === 'all' || selected === 'none') return selected;
const aliases = new Map(options.flatMap(bot => bot.filterKey
? (bot.filterMemberKeys ?? []).map(member => [member, bot.filterKey!] as const) : []));
const next = [...new Set(selected.map(key => aliases.get(key) ?? key))];
return next.length === selected.length && next.every((key, i) => key === selected[i]) ? selected : next;
}

export function nextCreatorsAfterToggle(previous: FilterCreators, key: string): FilterCreators {
if (previous === 'all') return [key];
if (previous === 'all' || previous === 'none') return [key];
const next = previous.includes(key) ? previous.filter(value => value !== key) : [...previous, key];
return next.length ? next : 'all';
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ export interface UseSidebarFilterReturn {
creatorOptions: readonly TaskBotIdentity[];
toggleCreator: (key: string) => void;
setCreatorsAll: () => void;
setCreatorsNone: () => void;
/** Project 维度。'all' = 全部;string[] = 仅显示其中的 normalized workingDir。 */
projects: FilterProjects;
/** projects === 'all' 时返回 null,方便 render 阶段做"是否在集合内"判定。 */
Expand Down Expand Up @@ -254,6 +255,10 @@ export function useSidebarFilter(
return next;
});
}, [ownerId, normalizeCreators]);
const setCreatorsNone = useCallback(() => {
setCreators('none');
persistCreators('none', ownerId);
}, [ownerId]);
const setCreatorsAll = useCallback(() => {
setCreators('all');
persistCreators('all', ownerId);
Expand Down Expand Up @@ -645,6 +650,7 @@ export function useSidebarFilter(
creatorOptions,
toggleCreator,
setCreatorsAll,
setCreatorsNone,
status,
projects,
projectsAsSet,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,7 @@ export function SidebarFilterPopover({
creatorOptions,
toggleCreator,
setCreatorsAll,
setCreatorsNone,
isFilterActive,
vendor,
lastActivity,
Expand Down Expand Up @@ -526,6 +527,7 @@ export function SidebarFilterPopover({
: t('ccAgent.sidebar.filterSelectedProjects', { count: projects.length });

const creatorValue = creators === 'all' ? t('ccAgent.sidebar.filterCreatorsAny')
: creators === 'none' ? t('ccAgent.sidebar.filterCreatorsNone')
: creators.length === 1
? (creatorOptions.find(bot => taskCreatorFilterKey(bot) === creators[0])?.name
?? t('ccAgent.sidebar.filterSelectedCreators', { count: creators.length }))
Expand Down Expand Up @@ -813,20 +815,22 @@ export function SidebarFilterPopover({
<MenuSubRow label={t('ccAgent.sidebar.filterCreatorsHeading')} value={creatorValue} Icon={Users}>
<SelectMenuItem label={t('ccAgent.sidebar.filterCreatorsAny')} selected={creators === 'all'}
onSelect={setCreatorsAll} Icon={Users} />
<SelectMenuItem label={t('ccAgent.sidebar.filterCreatorsNone')} selected={creators === 'none'}
onSelect={setCreatorsNone} Icon={Users} />
{creatorOptions.length > 0 && <DropdownMenuSeparator />}
<div className="max-h-[256px] overflow-y-auto">
{creatorOptions.map(bot => {
const key = taskCreatorFilterKey(bot);
return (
<DropdownMenuItem key={key} className={cn(MENU_ITEM_CLASS, bot.deviceName && 'h-auto min-h-8 items-start')}
role="menuitemcheckbox" aria-label={[bot.name, bot.deviceName].filter(Boolean).join(' ')} aria-checked={creators !== 'all' && creators.includes(key)}
role="menuitemcheckbox" aria-label={[bot.name, bot.deviceName].filter(Boolean).join(' ')} aria-checked={Array.isArray(creators) && creators.includes(key)}
onSelect={event => { event.preventDefault(); toggleCreator(key); }}>
<BotAvatar bot={bot} size="xs" />
<span className="min-w-0 flex-1">
<span className="block truncate" title={bot.name}>{bot.name}</span>
{bot.deviceName && <span className="block truncate text-xs text-[var(--cmd-palette-item-meta)]" title={bot.deviceName}>{bot.deviceName}</span>}
</span>
{creators !== 'all' && creators.includes(key) &&
{Array.isArray(creators) && creators.includes(key) &&
<Check size={15} className={cn('shrink-0 text-[var(--msg-assistant-text)]', bot.deviceName && 'mt-0.5')} />}
</DropdownMenuItem>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ function makeFilter(overrides: Partial<UseSidebarFilterReturn> = {}): UseSidebar
creatorOptions: [],
toggleCreator: vi.fn(),
setCreatorsAll: vi.fn(),
setCreatorsNone: vi.fn(),
status: 'active',
projects: 'all',
projectsAsSet: null,
Expand Down Expand Up @@ -214,6 +215,8 @@ it('keeps the creator submenu open for multiple selections and exposes an all re
fireEvent.click(within(submenu).getByRole('menuitemcheckbox', { name: 'Dash Laptop' }));
expect(filter.toggleCreator).toHaveBeenCalledWith('["remote","a"]');
expect(screen.getAllByRole('menu')).toHaveLength(3);
fireEvent.click(within(submenu).getByRole('menuitem', { name: '不限来源' }));
fireEvent.click(within(submenu).getByRole('menuitem', { name: '全部' }));
expect(filter.setCreatorsAll).toHaveBeenCalledOnce();
fireEvent.click(within(submenu).getByRole('menuitem', { name: '都不显示' }));
expect(filter.setCreatorsNone).toHaveBeenCalledOnce();
});
3 changes: 2 additions & 1 deletion apps/desktop/src/renderer/i18n/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -13767,7 +13767,8 @@
"custom": "Drag project rows with the mouse to reorder. Sessions inside still follow the sort above."
},
"filterCreatorsHeading": "Teammates",
"filterCreatorsAny": "Any source",
"filterCreatorsAny": "All",
"filterCreatorsNone": "Hide all",
"filterSelectedCreators": "{{count}} selected",
"filterProjectsHeading": "Projects",
"filterAllProjects": "All projects",
Expand Down
3 changes: 2 additions & 1 deletion apps/desktop/src/renderer/i18n/locales/ja/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -13712,7 +13712,8 @@
"custom": "マウスでプロジェクト行をドラッグして並び替え。中のセッションは上の並び替えに従う"
},
"filterCreatorsHeading": "チームメイト",
"filterCreatorsAny": "作成元を問わない",
"filterCreatorsAny": "すべて",
"filterCreatorsNone": "すべて非表示",
"filterSelectedCreators": "{{count}} 人を選択",
"filterProjectsHeading": "プロジェクト",
"filterAllProjects": "すべてのプロジェクト",
Expand Down
3 changes: 2 additions & 1 deletion apps/desktop/src/renderer/i18n/locales/ko/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -13712,7 +13712,8 @@
"custom": "마우스로 프로젝트 행을 끌어 순서를 바꿉니다. 안의 세션은 위 정렬을 따릅니다."
},
"filterCreatorsHeading": "팀원",
"filterCreatorsAny": "생성자 제한 없음",
"filterCreatorsAny": "전체",
"filterCreatorsNone": "모두 숨기기",
"filterSelectedCreators": "{{count}}명 선택",
"filterProjectsHeading": "프로젝트",
"filterAllProjects": "모든 프로젝트",
Expand Down
3 changes: 2 additions & 1 deletion apps/desktop/src/renderer/i18n/locales/zh-CN/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -13712,7 +13712,8 @@
"custom": "用鼠标拖动项目行调整顺序,组内任务仍按上面的排序"
},
"filterCreatorsHeading": "伙伴",
"filterCreatorsAny": "不限来源",
"filterCreatorsAny": "全部",
"filterCreatorsNone": "都不显示",
"filterSelectedCreators": "已选 {{count}} 位",
"filterProjectsHeading": "项目",
"filterAllProjects": "所有项目",
Expand Down
3 changes: 2 additions & 1 deletion apps/desktop/src/renderer/i18n/locales/zh-TW/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -13712,7 +13712,8 @@
"custom": "用滑鼠拖動專案列調整順序,組內任務仍按上面的排序"
},
"filterCreatorsHeading": "夥伴",
"filterCreatorsAny": "不限來源",
"filterCreatorsAny": "全部",
"filterCreatorsNone": "都不顯示",
"filterSelectedCreators": "已選 {{count}} 位",
"filterProjectsHeading": "專案",
"filterAllProjects": "所有專案",
Expand Down
6 changes: 6 additions & 0 deletions apps/mobile/src/__tests__/homeChromeMenus.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,12 @@ describe("home chrome menus", () => {
state = { ...state, ...homeDisplayActionPatch("filter.teammate:cindy", state) };
state = { ...state, ...homeDisplayActionPatch("filter.teammate:filo", state) };
expect(state.teammateFilter).toBe("all");
state = { ...state, ...homeDisplayActionPatch("filter.teammates.none", state) };
expect(state.teammateFilter).toBe("none");
const hidden = buildHomeDisplayPullDownActions({ state, teammates: options, projects: [], dialogueCount: 0, t });
expect(find(hidden, "filter.teammates.none")?.state).toBe("on");
expect(find(hidden, "filter.teammate:cindy")?.state).toBe("off");
expect(homeDisplayActionPatch("filter.teammate:cindy", state)).toEqual({ teammateFilter: ["cindy"] });
expect(homeDisplayActionPatch("filter.teammates.all", { ...state, teammateFilter: ["cindy"] })).toEqual({ teammateFilter: "all" });
});

Expand Down
5 changes: 5 additions & 0 deletions apps/mobile/src/__tests__/homeDisplaySettings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,11 @@ describe('home content filters (desktop 筛选 parity)', () => {
expect(result.projects.map(row => row.key)).toEqual(['p2']);
expect(result.pinned).toBe(fixture.pinned);
expect(activeContentFilterCount(filters)).toBe(1);
const hidden = applyHomeContentFilters(fixture, { ...filters, teammates: 'none' }, NOW);
expect(hidden.chats.map(row => row.session.id)).toEqual(fixture.chats.filter(row => row.session.id !== 'chat-cc').map(row => row.session.id));
expect(hidden.projects.map(row => row.key)).toEqual(['p1']);
expect(hidden.pinned).toBe(fixture.pinned);
expect(filterSharedHomeRows([{ id: 'unknown' }, { id: 'known', item: item('chat-cc') }], { ...filters, teammates: 'none' }, NOW).map(row => row.id)).toEqual(['unknown']);
expect(filterSharedHomeRows([{ id: 'unknown' }, { id: 'known', item: item('chat-cc') }], filters, NOW).map(row => row.id)).toEqual(['known']);
});

Expand Down
3 changes: 3 additions & 0 deletions apps/mobile/src/__tests__/homeViewPreferenceStore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ describe('homeViewPreferenceStore', () => {
await saveHomeViewPreferences({ teammateFilter: { owner: OWNER, value: ['cindy', 'filo'] }, projectFilter: { owner: OWNER, value: ['project'] } });
expect((await readHomeViewPreferences(OWNER)).teammateFilter).toEqual(['cindy', 'filo']);
expect((await readHomeViewPreferences('another-owner')).teammateFilter).toBe('all');
await saveHomeViewPreferences({ teammateFilter: { owner: OWNER, value: 'none' } });
expect((await readHomeViewPreferences(OWNER)).teammateFilter).toBe('none');
expect((await readHomeViewPreferences('another-owner')).teammateFilter).toBe('all');
await saveHomeViewPreferences({ teammateFilter: { owner: OWNER, value: 'all' } });
expect(await readHomeViewPreferences(OWNER)).toMatchObject({ teammateFilter: 'all', projectFilter: ['project'] });
});
Expand Down
3 changes: 2 additions & 1 deletion apps/mobile/src/i18n/locales/en/devices.json
Original file line number Diff line number Diff line change
Expand Up @@ -419,7 +419,8 @@
"summarySeparator": ", ",
"filterEmptyCopy": "Adjust or reset Filters in Display settings.",
"filterTeammatesHeading": "Teammates",
"filterAnySource": "Any source",
"filterAnySource": "All",
"filterHideAllTeammates": "Hide all",
"filterSelectedTeammates": "{{count}} selected"
},
"search": {
Expand Down
3 changes: 2 additions & 1 deletion apps/mobile/src/i18n/locales/ja/devices.json
Original file line number Diff line number Diff line change
Expand Up @@ -419,7 +419,8 @@
"summarySeparator": "、",
"filterEmptyCopy": "表示設定の「フィルター」で調整またはリセットできます。",
"filterTeammatesHeading": "パートナー",
"filterAnySource": "すべての作成元",
"filterAnySource": "すべて",
"filterHideAllTeammates": "すべて非表示",
"filterSelectedTeammates": "{{count}} 人を選択"
},
"search": {
Expand Down
3 changes: 2 additions & 1 deletion apps/mobile/src/i18n/locales/ko/devices.json
Original file line number Diff line number Diff line change
Expand Up @@ -419,7 +419,8 @@
"summarySeparator": ", ",
"filterEmptyCopy": "표시 설정의 필터에서 조정하거나 재설정할 수 있습니다.",
"filterTeammatesHeading": "파트너",
"filterAnySource": "모든 출처",
"filterAnySource": "전체",
"filterHideAllTeammates": "모두 숨기기",
"filterSelectedTeammates": "{{count}}명 선택"
},
"search": {
Expand Down
3 changes: 2 additions & 1 deletion apps/mobile/src/i18n/locales/zh-CN/devices.json
Original file line number Diff line number Diff line change
Expand Up @@ -419,7 +419,8 @@
"summarySeparator": "、",
"filterEmptyCopy": "可在显示设置的「筛选」中调整或重置。",
"filterTeammatesHeading": "伙伴",
"filterAnySource": "不限来源",
"filterAnySource": "全部",
"filterHideAllTeammates": "都不显示",
"filterSelectedTeammates": "已选 {{count}} 位"
},
"search": {
Expand Down
3 changes: 2 additions & 1 deletion apps/mobile/src/i18n/locales/zh-TW/devices.json
Original file line number Diff line number Diff line change
Expand Up @@ -419,7 +419,8 @@
"summarySeparator": "、",
"filterEmptyCopy": "可在顯示設定的「篩選」中調整或重設。",
"filterTeammatesHeading": "夥伴",
"filterAnySource": "不限來源",
"filterAnySource": "全部",
"filterHideAllTeammates": "都不顯示",
"filterSelectedTeammates": "已選 {{count}} 位"
},
"search": {
Expand Down
Loading
Loading