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",
|
||||
"version": "0.6.0-rc1",
|
||||
"version": "0.6.0-rc2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "takt",
|
||||
"version": "0.6.0-rc1",
|
||||
"version": "0.6.0-rc2",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/claude-agent-sdk": "^0.2.19",
|
||||
|
||||
@ -36,6 +36,7 @@ const pieceCategoriesState = vi.hoisted(() => ({
|
||||
categories: undefined as any,
|
||||
showOthersCategory: undefined as boolean | undefined,
|
||||
othersCategoryName: undefined as string | undefined,
|
||||
builtinCategoryName: undefined as string | undefined,
|
||||
}));
|
||||
|
||||
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,
|
||||
getShowOthersCategory: () => pieceCategoriesState.showOthersCategory,
|
||||
getOthersCategoryName: () => pieceCategoriesState.othersCategoryName,
|
||||
getBuiltinCategoryName: () => pieceCategoriesState.builtinCategoryName,
|
||||
};
|
||||
});
|
||||
|
||||
@ -105,6 +107,7 @@ describe('piece category config loading', () => {
|
||||
pieceCategoriesState.categories = undefined;
|
||||
pieceCategoriesState.showOthersCategory = undefined;
|
||||
pieceCategoriesState.othersCategoryName = undefined;
|
||||
pieceCategoriesState.builtinCategoryName = undefined;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@ -126,6 +129,7 @@ others_category_name: "Others"
|
||||
expect(config!.pieceCategories).toEqual([
|
||||
{ name: 'Default', pieces: ['simple'], children: [] },
|
||||
]);
|
||||
expect(config!.hasCustomCategories).toBe(false);
|
||||
});
|
||||
|
||||
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: [] },
|
||||
]);
|
||||
expect(config!.showOthersCategory).toBe(false);
|
||||
expect(config!.hasCustomCategories).toBe(true);
|
||||
});
|
||||
|
||||
it('should prefer user config over project config when piece_categories is defined', () => {
|
||||
@ -179,6 +184,7 @@ piece_categories:
|
||||
expect(config!.pieceCategories).toEqual([
|
||||
{ name: 'User', pieces: ['preferred'], children: [] },
|
||||
]);
|
||||
expect(config!.hasCustomCategories).toBe(true);
|
||||
});
|
||||
|
||||
it('should ignore configs without piece_categories and fall back to default', () => {
|
||||
@ -223,6 +229,8 @@ describe('buildCategorizedPieces', () => {
|
||||
],
|
||||
showOthersCategory: true,
|
||||
othersCategoryName: 'Others',
|
||||
builtinCategoryName: 'Builtin',
|
||||
hasCustomCategories: false,
|
||||
};
|
||||
|
||||
const categorized = buildCategorizedPieces(allPieces, config);
|
||||
@ -236,6 +244,7 @@ describe('buildCategorizedPieces', () => {
|
||||
expect(categorized.missingPieces).toEqual([
|
||||
{ categoryPath: ['Cat'], pieceName: 'missing' },
|
||||
]);
|
||||
expect(categorized.builtinCategoryName).toBe('Builtin');
|
||||
});
|
||||
|
||||
it('should skip empty categories', () => {
|
||||
@ -248,6 +257,8 @@ describe('buildCategorizedPieces', () => {
|
||||
],
|
||||
showOthersCategory: false,
|
||||
othersCategoryName: 'Others',
|
||||
builtinCategoryName: 'Builtin',
|
||||
hasCustomCategories: false,
|
||||
};
|
||||
|
||||
const categorized = buildCategorizedPieces(allPieces, config);
|
||||
@ -280,3 +291,193 @@ describe('buildCategorizedPieces', () => {
|
||||
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
|
||||
if (builtinCount > 0) {
|
||||
options.push({
|
||||
label: `📂 Builtin/ (${builtinCount})`,
|
||||
label: `📂 ${categorized.builtinCategoryName}/ (${builtinCount})`,
|
||||
value: BUILTIN_SOURCE_VALUE,
|
||||
});
|
||||
}
|
||||
|
||||
@ -32,6 +32,7 @@ export {
|
||||
setShowOthersCategory,
|
||||
getOthersCategoryName,
|
||||
setOthersCategoryName,
|
||||
getBuiltinCategoryName,
|
||||
} from './pieceCategories.js';
|
||||
|
||||
export {
|
||||
|
||||
@ -15,6 +15,7 @@ interface PieceCategoriesFile {
|
||||
categories?: PieceCategoryConfigNode;
|
||||
show_others_category?: boolean;
|
||||
others_category_name?: string;
|
||||
builtin_category_name?: string;
|
||||
}
|
||||
|
||||
function getDefaultPieceCategoriesPath(): string {
|
||||
@ -100,3 +101,10 @@ export function setOthersCategoryName(name: string): void {
|
||||
data.others_category_name = name;
|
||||
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,
|
||||
getShowOthersCategory,
|
||||
getOthersCategoryName,
|
||||
getBuiltinCategoryName,
|
||||
} from '../global/pieceCategories.js';
|
||||
import { getLanguageResourcesDir } from '../../resources/index.js';
|
||||
import { listBuiltinPieceNames } from './pieceResolver.js';
|
||||
@ -33,11 +34,15 @@ export interface CategoryConfig {
|
||||
pieceCategories: PieceCategoryNode[];
|
||||
showOthersCategory: boolean;
|
||||
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 {
|
||||
categories: PieceCategoryNode[];
|
||||
builtinCategories: PieceCategoryNode[];
|
||||
builtinCategoryName: string;
|
||||
allPieces: Map<string, PieceWithSource>;
|
||||
missingPieces: MissingPiece[];
|
||||
}
|
||||
@ -134,6 +139,8 @@ function parseCategoryConfig(raw: unknown, sourceLabel: string): CategoryConfig
|
||||
pieceCategories: parseCategoryTree(parsed.piece_categories, sourceLabel),
|
||||
showOthersCategory,
|
||||
othersCategoryName,
|
||||
builtinCategoryName: 'Builtin',
|
||||
hasCustomCategories: false,
|
||||
};
|
||||
}
|
||||
|
||||
@ -166,16 +173,19 @@ export function getPieceCategories(cwd: string): CategoryConfig | null {
|
||||
if (userCategoriesNode) {
|
||||
const showOthersCategory = getShowOthersCategory() ?? true;
|
||||
const othersCategoryName = getOthersCategoryName() ?? 'Others';
|
||||
const builtinCategoryName = getBuiltinCategoryName() ?? 'Builtin';
|
||||
return {
|
||||
pieceCategories: parseCategoryTree(userCategoriesNode, 'user config'),
|
||||
showOthersCategory,
|
||||
othersCategoryName,
|
||||
builtinCategoryName,
|
||||
hasCustomCategories: true,
|
||||
};
|
||||
}
|
||||
|
||||
const projectConfig = loadCategoryConfigFromPath(getProjectConfigPath(cwd), 'project config');
|
||||
if (projectConfig) {
|
||||
return projectConfig;
|
||||
return { ...projectConfig, hasCustomCategories: true };
|
||||
}
|
||||
|
||||
return loadDefaultCategories();
|
||||
@ -211,12 +221,14 @@ function buildCategoryTreeForSource(
|
||||
allPieces: Map<string, PieceWithSource>,
|
||||
sourceFilter: (source: PieceSource) => boolean,
|
||||
categorized: Set<string>,
|
||||
allowedPieces?: Set<string>,
|
||||
): PieceCategoryNode[] {
|
||||
const result: PieceCategoryNode[] = [];
|
||||
|
||||
for (const node of categories) {
|
||||
const pieces: string[] = [];
|
||||
for (const pieceName of node.pieces) {
|
||||
if (allowedPieces && !allowedPieces.has(pieceName)) continue;
|
||||
const entry = allPieces.get(pieceName);
|
||||
if (!entry) continue;
|
||||
if (!sourceFilter(entry.source)) continue;
|
||||
@ -224,7 +236,7 @@ function buildCategoryTreeForSource(
|
||||
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) {
|
||||
result.push({ name: node.name, pieces, children });
|
||||
}
|
||||
@ -258,6 +270,58 @@ function appendOthersCategory(
|
||||
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.
|
||||
*/
|
||||
@ -265,6 +329,8 @@ export function buildCategorizedPieces(
|
||||
allPieces: Map<string, PieceWithSource>,
|
||||
config: CategoryConfig,
|
||||
): CategorizedPieces {
|
||||
const { builtinCategoryName } = config;
|
||||
|
||||
const ignoreMissing = new Set<string>();
|
||||
if (!getBuiltinPiecesEnabled()) {
|
||||
for (const name of listBuiltinPieceNames({ includeDisabled: true })) {
|
||||
@ -301,6 +367,16 @@ export function buildCategorizedPieces(
|
||||
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
|
||||
? appendOthersCategory(
|
||||
categories,
|
||||
@ -311,7 +387,13 @@ export function buildCategorizedPieces(
|
||||
)
|
||||
: categories;
|
||||
|
||||
const finalBuiltinCategories = config.showOthersCategory
|
||||
// For user-defined configs, uncategorized builtins are already handled above,
|
||||
// so only apply Others for non-user-defined configs
|
||||
let finalBuiltinCategories: PieceCategoryNode[];
|
||||
if (config.hasCustomCategories) {
|
||||
finalBuiltinCategories = builtinCategories;
|
||||
} else {
|
||||
finalBuiltinCategories = config.showOthersCategory
|
||||
? appendOthersCategory(
|
||||
builtinCategories,
|
||||
allPieces,
|
||||
@ -320,10 +402,12 @@ export function buildCategorizedPieces(
|
||||
config.othersCategoryName,
|
||||
)
|
||||
: builtinCategories;
|
||||
}
|
||||
|
||||
return {
|
||||
categories: finalCategories,
|
||||
builtinCategories: finalBuiltinCategories,
|
||||
builtinCategoryName,
|
||||
allPieces,
|
||||
missingPieces,
|
||||
};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user