Squashed commit of the following:
commit 2730269da717c78e90bc7d35ea6b2404f8e845f5
Author: nrslib <38722970+nrslib@users.noreply.github.com>
Date: Fri Feb 6 00:11:54 2026 +0900
takt: add-category-display-split
This commit is contained in:
parent
c3b11df7e0
commit
34a6a4bea2
4
package-lock.json
generated
4
package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "takt",
|
"name": "takt",
|
||||||
"version": "0.6.0-rc1",
|
"version": "0.6.0-rc2",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "takt",
|
"name": "takt",
|
||||||
"version": "0.6.0-rc1",
|
"version": "0.6.0-rc2",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@anthropic-ai/claude-agent-sdk": "^0.2.19",
|
"@anthropic-ai/claude-agent-sdk": "^0.2.19",
|
||||||
|
|||||||
@ -36,6 +36,7 @@ const pieceCategoriesState = vi.hoisted(() => ({
|
|||||||
categories: undefined as any,
|
categories: undefined as any,
|
||||||
showOthersCategory: undefined as boolean | undefined,
|
showOthersCategory: undefined as boolean | undefined,
|
||||||
othersCategoryName: undefined as string | undefined,
|
othersCategoryName: undefined as string | undefined,
|
||||||
|
builtinCategoryName: undefined as string | undefined,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../infra/config/global/globalConfig.js', async (importOriginal) => {
|
vi.mock('../infra/config/global/globalConfig.js', async (importOriginal) => {
|
||||||
@ -53,6 +54,7 @@ vi.mock('../infra/config/global/pieceCategories.js', async (importOriginal) => {
|
|||||||
getPieceCategoriesConfig: () => pieceCategoriesState.categories,
|
getPieceCategoriesConfig: () => pieceCategoriesState.categories,
|
||||||
getShowOthersCategory: () => pieceCategoriesState.showOthersCategory,
|
getShowOthersCategory: () => pieceCategoriesState.showOthersCategory,
|
||||||
getOthersCategoryName: () => pieceCategoriesState.othersCategoryName,
|
getOthersCategoryName: () => pieceCategoriesState.othersCategoryName,
|
||||||
|
getBuiltinCategoryName: () => pieceCategoriesState.builtinCategoryName,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -105,6 +107,7 @@ describe('piece category config loading', () => {
|
|||||||
pieceCategoriesState.categories = undefined;
|
pieceCategoriesState.categories = undefined;
|
||||||
pieceCategoriesState.showOthersCategory = undefined;
|
pieceCategoriesState.showOthersCategory = undefined;
|
||||||
pieceCategoriesState.othersCategoryName = undefined;
|
pieceCategoriesState.othersCategoryName = undefined;
|
||||||
|
pieceCategoriesState.builtinCategoryName = undefined;
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@ -126,6 +129,7 @@ others_category_name: "Others"
|
|||||||
expect(config!.pieceCategories).toEqual([
|
expect(config!.pieceCategories).toEqual([
|
||||||
{ name: 'Default', pieces: ['simple'], children: [] },
|
{ name: 'Default', pieces: ['simple'], children: [] },
|
||||||
]);
|
]);
|
||||||
|
expect(config!.hasCustomCategories).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should prefer project config over default when piece_categories is defined', () => {
|
it('should prefer project config over default when piece_categories is defined', () => {
|
||||||
@ -150,6 +154,7 @@ show_others_category: false
|
|||||||
{ name: 'Project', pieces: ['custom'], children: [] },
|
{ name: 'Project', pieces: ['custom'], children: [] },
|
||||||
]);
|
]);
|
||||||
expect(config!.showOthersCategory).toBe(false);
|
expect(config!.showOthersCategory).toBe(false);
|
||||||
|
expect(config!.hasCustomCategories).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should prefer user config over project config when piece_categories is defined', () => {
|
it('should prefer user config over project config when piece_categories is defined', () => {
|
||||||
@ -179,6 +184,7 @@ piece_categories:
|
|||||||
expect(config!.pieceCategories).toEqual([
|
expect(config!.pieceCategories).toEqual([
|
||||||
{ name: 'User', pieces: ['preferred'], children: [] },
|
{ name: 'User', pieces: ['preferred'], children: [] },
|
||||||
]);
|
]);
|
||||||
|
expect(config!.hasCustomCategories).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should ignore configs without piece_categories and fall back to default', () => {
|
it('should ignore configs without piece_categories and fall back to default', () => {
|
||||||
@ -223,6 +229,8 @@ describe('buildCategorizedPieces', () => {
|
|||||||
],
|
],
|
||||||
showOthersCategory: true,
|
showOthersCategory: true,
|
||||||
othersCategoryName: 'Others',
|
othersCategoryName: 'Others',
|
||||||
|
builtinCategoryName: 'Builtin',
|
||||||
|
hasCustomCategories: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
const categorized = buildCategorizedPieces(allPieces, config);
|
const categorized = buildCategorizedPieces(allPieces, config);
|
||||||
@ -236,6 +244,7 @@ describe('buildCategorizedPieces', () => {
|
|||||||
expect(categorized.missingPieces).toEqual([
|
expect(categorized.missingPieces).toEqual([
|
||||||
{ categoryPath: ['Cat'], pieceName: 'missing' },
|
{ categoryPath: ['Cat'], pieceName: 'missing' },
|
||||||
]);
|
]);
|
||||||
|
expect(categorized.builtinCategoryName).toBe('Builtin');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should skip empty categories', () => {
|
it('should skip empty categories', () => {
|
||||||
@ -248,6 +257,8 @@ describe('buildCategorizedPieces', () => {
|
|||||||
],
|
],
|
||||||
showOthersCategory: false,
|
showOthersCategory: false,
|
||||||
othersCategoryName: 'Others',
|
othersCategoryName: 'Others',
|
||||||
|
builtinCategoryName: 'Builtin',
|
||||||
|
hasCustomCategories: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
const categorized = buildCategorizedPieces(allPieces, config);
|
const categorized = buildCategorizedPieces(allPieces, config);
|
||||||
@ -280,3 +291,193 @@ describe('buildCategorizedPieces', () => {
|
|||||||
expect(paths).toEqual(['Parent / Child']);
|
expect(paths).toEqual(['Parent / Child']);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('buildCategorizedPieces with hasCustomCategories (auto builtin categorization)', () => {
|
||||||
|
let testDir: string;
|
||||||
|
let resourcesDir: string;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
testDir = join(tmpdir(), `takt-cat-config-${randomUUID()}`);
|
||||||
|
resourcesDir = join(testDir, 'resources');
|
||||||
|
|
||||||
|
mkdirSync(resourcesDir, { recursive: true });
|
||||||
|
pathsState.resourcesDir = resourcesDir;
|
||||||
|
|
||||||
|
pieceCategoriesState.categories = undefined;
|
||||||
|
pieceCategoriesState.showOthersCategory = undefined;
|
||||||
|
pieceCategoriesState.othersCategoryName = undefined;
|
||||||
|
pieceCategoriesState.builtinCategoryName = undefined;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
rmSync(testDir, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should auto-categorize uncategorized builtins when hasCustomCategories is true', () => {
|
||||||
|
// Set up default categories for auto-categorization
|
||||||
|
writeYaml(join(resourcesDir, 'default-categories.yaml'), `
|
||||||
|
piece_categories:
|
||||||
|
Standard:
|
||||||
|
pieces:
|
||||||
|
- default
|
||||||
|
- minimal
|
||||||
|
Advanced:
|
||||||
|
pieces:
|
||||||
|
- research
|
||||||
|
`);
|
||||||
|
|
||||||
|
const allPieces = createPieceMap([
|
||||||
|
{ name: 'my-piece', source: 'user' },
|
||||||
|
{ name: 'default', source: 'builtin' },
|
||||||
|
{ name: 'minimal', source: 'builtin' },
|
||||||
|
{ name: 'research', source: 'builtin' },
|
||||||
|
]);
|
||||||
|
|
||||||
|
// User only categorizes their own piece
|
||||||
|
const config = {
|
||||||
|
pieceCategories: [
|
||||||
|
{ name: 'My Pieces', pieces: ['my-piece'], children: [] },
|
||||||
|
],
|
||||||
|
showOthersCategory: false,
|
||||||
|
othersCategoryName: 'Others',
|
||||||
|
builtinCategoryName: 'Builtin',
|
||||||
|
hasCustomCategories: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const categorized = buildCategorizedPieces(allPieces, config);
|
||||||
|
|
||||||
|
// User pieces in categories
|
||||||
|
expect(categorized.categories).toEqual([
|
||||||
|
{ name: 'My Pieces', pieces: ['my-piece'], children: [] },
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Builtins auto-categorized using default category structure
|
||||||
|
expect(categorized.builtinCategories).toEqual([
|
||||||
|
{ name: 'Standard', pieces: ['default', 'minimal'], children: [] },
|
||||||
|
{ name: 'Advanced', pieces: ['research'], children: [] },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not duplicate builtins that are explicitly in user categories', () => {
|
||||||
|
writeYaml(join(resourcesDir, 'default-categories.yaml'), `
|
||||||
|
piece_categories:
|
||||||
|
Standard:
|
||||||
|
pieces:
|
||||||
|
- default
|
||||||
|
- minimal
|
||||||
|
`);
|
||||||
|
|
||||||
|
const allPieces = createPieceMap([
|
||||||
|
{ name: 'my-piece', source: 'user' },
|
||||||
|
{ name: 'default', source: 'builtin' },
|
||||||
|
{ name: 'minimal', source: 'builtin' },
|
||||||
|
]);
|
||||||
|
|
||||||
|
// User explicitly includes 'default' in their category
|
||||||
|
const config = {
|
||||||
|
pieceCategories: [
|
||||||
|
{ name: 'My Favorites', pieces: ['my-piece', 'default'], children: [] },
|
||||||
|
],
|
||||||
|
showOthersCategory: false,
|
||||||
|
othersCategoryName: 'Others',
|
||||||
|
builtinCategoryName: 'Builtin',
|
||||||
|
hasCustomCategories: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const categorized = buildCategorizedPieces(allPieces, config);
|
||||||
|
|
||||||
|
// 'default' is in user-defined builtin categories (from user's category config)
|
||||||
|
expect(categorized.builtinCategories).toEqual([
|
||||||
|
{ name: 'My Favorites', pieces: ['default'], children: [] },
|
||||||
|
// 'minimal' auto-categorized from default categories
|
||||||
|
{ name: 'Standard', pieces: ['minimal'], children: [] },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should ensure builtins are visible even with showOthersCategory: false', () => {
|
||||||
|
writeYaml(join(resourcesDir, 'default-categories.yaml'), `
|
||||||
|
piece_categories:
|
||||||
|
Standard:
|
||||||
|
pieces:
|
||||||
|
- default
|
||||||
|
`);
|
||||||
|
|
||||||
|
const allPieces = createPieceMap([
|
||||||
|
{ name: 'my-piece', source: 'user' },
|
||||||
|
{ name: 'default', source: 'builtin' },
|
||||||
|
{ name: 'extra-builtin', source: 'builtin' },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
pieceCategories: [
|
||||||
|
{ name: 'My Pieces', pieces: ['my-piece'], children: [] },
|
||||||
|
],
|
||||||
|
showOthersCategory: false,
|
||||||
|
othersCategoryName: 'Others',
|
||||||
|
builtinCategoryName: 'Builtin',
|
||||||
|
hasCustomCategories: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const categorized = buildCategorizedPieces(allPieces, config);
|
||||||
|
|
||||||
|
// Both builtins should be in builtinCategories, never hidden
|
||||||
|
expect(categorized.builtinCategories).toEqual([
|
||||||
|
{ name: 'Standard', pieces: ['default'], children: [] },
|
||||||
|
// extra-builtin not in default categories, so flat under Builtin
|
||||||
|
{ name: 'Builtin', pieces: ['extra-builtin'], children: [] },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use custom builtinCategoryName when configured', () => {
|
||||||
|
const allPieces = createPieceMap([
|
||||||
|
{ name: 'my-piece', source: 'user' },
|
||||||
|
{ name: 'default', source: 'builtin' },
|
||||||
|
]);
|
||||||
|
|
||||||
|
// No default categories file — builtins go to flat list
|
||||||
|
const config = {
|
||||||
|
pieceCategories: [
|
||||||
|
{ name: 'My Pieces', pieces: ['my-piece'], children: [] },
|
||||||
|
],
|
||||||
|
showOthersCategory: false,
|
||||||
|
othersCategoryName: 'Others',
|
||||||
|
builtinCategoryName: 'System Pieces',
|
||||||
|
hasCustomCategories: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const categorized = buildCategorizedPieces(allPieces, config);
|
||||||
|
|
||||||
|
expect(categorized.builtinCategoryName).toBe('System Pieces');
|
||||||
|
// Flat fallback uses the custom name
|
||||||
|
expect(categorized.builtinCategories).toEqual([
|
||||||
|
{ name: 'System Pieces', pieces: ['default'], children: [] },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fall back to flat Builtin category when default categories are unavailable', () => {
|
||||||
|
// No default-categories.yaml file
|
||||||
|
|
||||||
|
const allPieces = createPieceMap([
|
||||||
|
{ name: 'my-piece', source: 'user' },
|
||||||
|
{ name: 'default', source: 'builtin' },
|
||||||
|
{ name: 'minimal', source: 'builtin' },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
pieceCategories: [
|
||||||
|
{ name: 'My Pieces', pieces: ['my-piece'], children: [] },
|
||||||
|
],
|
||||||
|
showOthersCategory: false,
|
||||||
|
othersCategoryName: 'Others',
|
||||||
|
builtinCategoryName: 'Builtin',
|
||||||
|
hasCustomCategories: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const categorized = buildCategorizedPieces(allPieces, config);
|
||||||
|
|
||||||
|
// All builtins in a flat 'Builtin' category
|
||||||
|
expect(categorized.builtinCategories).toEqual([
|
||||||
|
{ name: 'Builtin', pieces: ['default', 'minimal'], children: [] },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@ -366,7 +366,7 @@ async function selectTopLevelPieceOption(
|
|||||||
// 4. Builtin pieces
|
// 4. Builtin pieces
|
||||||
if (builtinCount > 0) {
|
if (builtinCount > 0) {
|
||||||
options.push({
|
options.push({
|
||||||
label: `📂 Builtin/ (${builtinCount})`,
|
label: `📂 ${categorized.builtinCategoryName}/ (${builtinCount})`,
|
||||||
value: BUILTIN_SOURCE_VALUE,
|
value: BUILTIN_SOURCE_VALUE,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -32,6 +32,7 @@ export {
|
|||||||
setShowOthersCategory,
|
setShowOthersCategory,
|
||||||
getOthersCategoryName,
|
getOthersCategoryName,
|
||||||
setOthersCategoryName,
|
setOthersCategoryName,
|
||||||
|
getBuiltinCategoryName,
|
||||||
} from './pieceCategories.js';
|
} from './pieceCategories.js';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
|||||||
@ -15,6 +15,7 @@ interface PieceCategoriesFile {
|
|||||||
categories?: PieceCategoryConfigNode;
|
categories?: PieceCategoryConfigNode;
|
||||||
show_others_category?: boolean;
|
show_others_category?: boolean;
|
||||||
others_category_name?: string;
|
others_category_name?: string;
|
||||||
|
builtin_category_name?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDefaultPieceCategoriesPath(): string {
|
function getDefaultPieceCategoriesPath(): string {
|
||||||
@ -100,3 +101,10 @@ export function setOthersCategoryName(name: string): void {
|
|||||||
data.others_category_name = name;
|
data.others_category_name = name;
|
||||||
savePieceCategoriesFile(data);
|
savePieceCategoriesFile(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Get builtin category name */
|
||||||
|
export function getBuiltinCategoryName(): string | undefined {
|
||||||
|
const data = loadPieceCategoriesFile();
|
||||||
|
return data.builtin_category_name;
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import {
|
|||||||
getPieceCategoriesConfig,
|
getPieceCategoriesConfig,
|
||||||
getShowOthersCategory,
|
getShowOthersCategory,
|
||||||
getOthersCategoryName,
|
getOthersCategoryName,
|
||||||
|
getBuiltinCategoryName,
|
||||||
} from '../global/pieceCategories.js';
|
} from '../global/pieceCategories.js';
|
||||||
import { getLanguageResourcesDir } from '../../resources/index.js';
|
import { getLanguageResourcesDir } from '../../resources/index.js';
|
||||||
import { listBuiltinPieceNames } from './pieceResolver.js';
|
import { listBuiltinPieceNames } from './pieceResolver.js';
|
||||||
@ -33,11 +34,15 @@ export interface CategoryConfig {
|
|||||||
pieceCategories: PieceCategoryNode[];
|
pieceCategories: PieceCategoryNode[];
|
||||||
showOthersCategory: boolean;
|
showOthersCategory: boolean;
|
||||||
othersCategoryName: string;
|
othersCategoryName: string;
|
||||||
|
builtinCategoryName: string;
|
||||||
|
/** True when categories are from user or project config (not builtin defaults). Triggers auto-categorization of builtins. */
|
||||||
|
hasCustomCategories: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CategorizedPieces {
|
export interface CategorizedPieces {
|
||||||
categories: PieceCategoryNode[];
|
categories: PieceCategoryNode[];
|
||||||
builtinCategories: PieceCategoryNode[];
|
builtinCategories: PieceCategoryNode[];
|
||||||
|
builtinCategoryName: string;
|
||||||
allPieces: Map<string, PieceWithSource>;
|
allPieces: Map<string, PieceWithSource>;
|
||||||
missingPieces: MissingPiece[];
|
missingPieces: MissingPiece[];
|
||||||
}
|
}
|
||||||
@ -134,6 +139,8 @@ function parseCategoryConfig(raw: unknown, sourceLabel: string): CategoryConfig
|
|||||||
pieceCategories: parseCategoryTree(parsed.piece_categories, sourceLabel),
|
pieceCategories: parseCategoryTree(parsed.piece_categories, sourceLabel),
|
||||||
showOthersCategory,
|
showOthersCategory,
|
||||||
othersCategoryName,
|
othersCategoryName,
|
||||||
|
builtinCategoryName: 'Builtin',
|
||||||
|
hasCustomCategories: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -166,16 +173,19 @@ export function getPieceCategories(cwd: string): CategoryConfig | null {
|
|||||||
if (userCategoriesNode) {
|
if (userCategoriesNode) {
|
||||||
const showOthersCategory = getShowOthersCategory() ?? true;
|
const showOthersCategory = getShowOthersCategory() ?? true;
|
||||||
const othersCategoryName = getOthersCategoryName() ?? 'Others';
|
const othersCategoryName = getOthersCategoryName() ?? 'Others';
|
||||||
|
const builtinCategoryName = getBuiltinCategoryName() ?? 'Builtin';
|
||||||
return {
|
return {
|
||||||
pieceCategories: parseCategoryTree(userCategoriesNode, 'user config'),
|
pieceCategories: parseCategoryTree(userCategoriesNode, 'user config'),
|
||||||
showOthersCategory,
|
showOthersCategory,
|
||||||
othersCategoryName,
|
othersCategoryName,
|
||||||
|
builtinCategoryName,
|
||||||
|
hasCustomCategories: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const projectConfig = loadCategoryConfigFromPath(getProjectConfigPath(cwd), 'project config');
|
const projectConfig = loadCategoryConfigFromPath(getProjectConfigPath(cwd), 'project config');
|
||||||
if (projectConfig) {
|
if (projectConfig) {
|
||||||
return projectConfig;
|
return { ...projectConfig, hasCustomCategories: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
return loadDefaultCategories();
|
return loadDefaultCategories();
|
||||||
@ -211,12 +221,14 @@ function buildCategoryTreeForSource(
|
|||||||
allPieces: Map<string, PieceWithSource>,
|
allPieces: Map<string, PieceWithSource>,
|
||||||
sourceFilter: (source: PieceSource) => boolean,
|
sourceFilter: (source: PieceSource) => boolean,
|
||||||
categorized: Set<string>,
|
categorized: Set<string>,
|
||||||
|
allowedPieces?: Set<string>,
|
||||||
): PieceCategoryNode[] {
|
): PieceCategoryNode[] {
|
||||||
const result: PieceCategoryNode[] = [];
|
const result: PieceCategoryNode[] = [];
|
||||||
|
|
||||||
for (const node of categories) {
|
for (const node of categories) {
|
||||||
const pieces: string[] = [];
|
const pieces: string[] = [];
|
||||||
for (const pieceName of node.pieces) {
|
for (const pieceName of node.pieces) {
|
||||||
|
if (allowedPieces && !allowedPieces.has(pieceName)) continue;
|
||||||
const entry = allPieces.get(pieceName);
|
const entry = allPieces.get(pieceName);
|
||||||
if (!entry) continue;
|
if (!entry) continue;
|
||||||
if (!sourceFilter(entry.source)) continue;
|
if (!sourceFilter(entry.source)) continue;
|
||||||
@ -224,7 +236,7 @@ function buildCategoryTreeForSource(
|
|||||||
categorized.add(pieceName);
|
categorized.add(pieceName);
|
||||||
}
|
}
|
||||||
|
|
||||||
const children = buildCategoryTreeForSource(node.children, allPieces, sourceFilter, categorized);
|
const children = buildCategoryTreeForSource(node.children, allPieces, sourceFilter, categorized, allowedPieces);
|
||||||
if (pieces.length > 0 || children.length > 0) {
|
if (pieces.length > 0 || children.length > 0) {
|
||||||
result.push({ name: node.name, pieces, children });
|
result.push({ name: node.name, pieces, children });
|
||||||
}
|
}
|
||||||
@ -258,6 +270,58 @@ function appendOthersCategory(
|
|||||||
return [...categories, { name: othersCategoryName, pieces: uncategorized, children: [] }];
|
return [...categories, { name: othersCategoryName, pieces: uncategorized, children: [] }];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collect uncategorized builtin piece names.
|
||||||
|
*/
|
||||||
|
function collectUncategorizedBuiltins(
|
||||||
|
allPieces: Map<string, PieceWithSource>,
|
||||||
|
categorizedBuiltin: Set<string>,
|
||||||
|
): Set<string> {
|
||||||
|
const uncategorized = new Set<string>();
|
||||||
|
for (const [pieceName, entry] of allPieces.entries()) {
|
||||||
|
if (entry.source === 'builtin' && !categorizedBuiltin.has(pieceName)) {
|
||||||
|
uncategorized.add(pieceName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return uncategorized;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build builtin categories for uncategorized builtins using default category structure.
|
||||||
|
* Falls back to flat list if default categories are unavailable.
|
||||||
|
*/
|
||||||
|
function buildAutoBuiltinCategories(
|
||||||
|
allPieces: Map<string, PieceWithSource>,
|
||||||
|
uncategorizedBuiltins: Set<string>,
|
||||||
|
builtinCategoryName: string,
|
||||||
|
defaultConfig: CategoryConfig | null,
|
||||||
|
): PieceCategoryNode[] {
|
||||||
|
if (defaultConfig) {
|
||||||
|
const autoCategorized = new Set<string>();
|
||||||
|
const autoCategories = buildCategoryTreeForSource(
|
||||||
|
defaultConfig.pieceCategories,
|
||||||
|
allPieces,
|
||||||
|
(source) => source === 'builtin',
|
||||||
|
autoCategorized,
|
||||||
|
uncategorizedBuiltins,
|
||||||
|
);
|
||||||
|
// Any builtins still not categorized by default categories go into a flat list
|
||||||
|
const remaining: string[] = [];
|
||||||
|
for (const name of uncategorizedBuiltins) {
|
||||||
|
if (!autoCategorized.has(name)) {
|
||||||
|
remaining.push(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (remaining.length > 0) {
|
||||||
|
autoCategories.push({ name: builtinCategoryName, pieces: remaining, children: [] });
|
||||||
|
}
|
||||||
|
return autoCategories;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No default categories available: flat list
|
||||||
|
return [{ name: builtinCategoryName, pieces: Array.from(uncategorizedBuiltins), children: [] }];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build categorized pieces map from configuration.
|
* Build categorized pieces map from configuration.
|
||||||
*/
|
*/
|
||||||
@ -265,6 +329,8 @@ export function buildCategorizedPieces(
|
|||||||
allPieces: Map<string, PieceWithSource>,
|
allPieces: Map<string, PieceWithSource>,
|
||||||
config: CategoryConfig,
|
config: CategoryConfig,
|
||||||
): CategorizedPieces {
|
): CategorizedPieces {
|
||||||
|
const { builtinCategoryName } = config;
|
||||||
|
|
||||||
const ignoreMissing = new Set<string>();
|
const ignoreMissing = new Set<string>();
|
||||||
if (!getBuiltinPiecesEnabled()) {
|
if (!getBuiltinPiecesEnabled()) {
|
||||||
for (const name of listBuiltinPieceNames({ includeDisabled: true })) {
|
for (const name of listBuiltinPieceNames({ includeDisabled: true })) {
|
||||||
@ -301,6 +367,16 @@ export function buildCategorizedPieces(
|
|||||||
categorizedBuiltin,
|
categorizedBuiltin,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// When user defined categories, auto-categorize uncategorized builtins
|
||||||
|
if (config.hasCustomCategories) {
|
||||||
|
const uncategorizedBuiltins = collectUncategorizedBuiltins(allPieces, categorizedBuiltin);
|
||||||
|
if (uncategorizedBuiltins.size > 0) {
|
||||||
|
const defaultConfig = loadDefaultCategories();
|
||||||
|
const autoCategories = buildAutoBuiltinCategories(allPieces, uncategorizedBuiltins, builtinCategoryName, defaultConfig);
|
||||||
|
builtinCategories.push(...autoCategories);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const finalCategories = config.showOthersCategory
|
const finalCategories = config.showOthersCategory
|
||||||
? appendOthersCategory(
|
? appendOthersCategory(
|
||||||
categories,
|
categories,
|
||||||
@ -311,19 +387,27 @@ export function buildCategorizedPieces(
|
|||||||
)
|
)
|
||||||
: categories;
|
: categories;
|
||||||
|
|
||||||
const finalBuiltinCategories = config.showOthersCategory
|
// For user-defined configs, uncategorized builtins are already handled above,
|
||||||
? appendOthersCategory(
|
// so only apply Others for non-user-defined configs
|
||||||
builtinCategories,
|
let finalBuiltinCategories: PieceCategoryNode[];
|
||||||
allPieces,
|
if (config.hasCustomCategories) {
|
||||||
categorizedBuiltin,
|
finalBuiltinCategories = builtinCategories;
|
||||||
isBuiltin,
|
} else {
|
||||||
config.othersCategoryName,
|
finalBuiltinCategories = config.showOthersCategory
|
||||||
)
|
? appendOthersCategory(
|
||||||
: builtinCategories;
|
builtinCategories,
|
||||||
|
allPieces,
|
||||||
|
categorizedBuiltin,
|
||||||
|
isBuiltin,
|
||||||
|
config.othersCategoryName,
|
||||||
|
)
|
||||||
|
: builtinCategories;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
categories: finalCategories,
|
categories: finalCategories,
|
||||||
builtinCategories: finalBuiltinCategories,
|
builtinCategories: finalBuiltinCategories,
|
||||||
|
builtinCategoryName,
|
||||||
allPieces,
|
allPieces,
|
||||||
missingPieces,
|
missingPieces,
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user