mirror of
https://github.com/nexu-io/open-design.git
synced 2026-06-01 03:14:35 +07:00
* feat(daemon): add project working directory management and editor hand-off functionality - Introduced new flags for project commands to manage working directories, including `--working-dir` and `--dir`. - Implemented API routes for listing available editors and opening projects in selected editors. - Added a hand-off button in the ChatPane header to facilitate opening project folders in local applications. - Enhanced the HomeHero component to include working directory and design system settings, improving user experience in project creation. - Created HomeHeroSettingsChips component for inline management of working directory and design system selection. * feat(chat): implement voice transcription proxy and enhance UI components - Added a new API route for voice transcription using OpenAI's `/audio/transcriptions` endpoint, allowing users to send audio blobs directly for transcription. - Integrated multer for handling audio file uploads in memory, ensuring efficient processing without disk storage. - Updated the HomeHero component to include example prompt suggestions for plugins, enhancing user interaction. - Introduced the EditorIcon component to visually represent different editors in the hand-off menu, improving the user experience. - Refined the HandoffButton component to utilize the new EditorIcon, providing a more cohesive interface for selecting editors. - Enhanced CSS styles for various components to improve layout and responsiveness, including adjustments to tab and button sizes for better usability. * style(workspace-shell): enhance layout and overflow handling - Updated CSS for .workspace-shell to ensure full viewport width and height, with proper overflow management. - Adjusted grid layout to prevent content overflow and maintain responsiveness. - Modified styles for .workspace-tabs-chrome to improve width handling and prevent overflow issues. * refactor(chat): remove voice transcription proxy and related components - Deleted the voice transcription proxy implementation, including the associated API route and multer configuration. - Removed the MicButton component from the ChatComposer and HomeHero components to streamline the UI. - Updated HomeHero to include example suggestions without the voice input functionality. - Adjusted CSS styles for various components to maintain layout consistency after the removal of the MicButton. * feat(daemon): implement minting of HMAC tokens for working directory management - Added a new function `mintImportTokenFromCurrentSecret` to generate HMAC tokens bound to a specified base directory, enhancing security for working directory operations. - Updated the `desktop-auth.ts` file to include the new token minting functionality, which returns structured errors when the desktop auth secret is cleared. - Introduced new IPC message types for minting import tokens in the sidecar protocol, allowing seamless integration with the daemon's working directory management. - Enhanced the `WorkingDirPill` component to utilize the new token minting flow for secure directory selection in desktop builds. - Updated CSS styles for the HomeHero component to accommodate new example suggestion features and maintain layout consistency. * fix(HomeView): import HOME_HERO_CHIPS constant for improved chip management - Updated the HomeView component to import the HOME_HERO_CHIPS constant from the chips module, enhancing the management of hero chips within the component. * feat(daemon): implement mintImportTokenViaSidecar for secure working directory management - Introduced the `mintImportTokenViaSidecar` function to facilitate the minting of HMAC tokens for desktop-import operations via the daemon's sidecar IPC. This allows CLI commands to bypass authentication when the desktop-auth gate is active. - Updated the CLI to utilize the new token minting function when setting the working directory, ensuring secure access to trust-gated API endpoints. - Enhanced the sidecar server to handle minting requests and return structured error messages for improved user feedback. - Added tests to validate the new token minting functionality and its integration with the working directory management process. - Refactored related components to support the new token flow, improving overall security and user experience. * feat(HomeHero): enhance UI components and styles for improved user experience - Updated HomeHero component to replace active dot indicators with Plug icons for better visual representation of active plugins. - Adjusted CSS styles for various elements, including padding and dimensions, to enhance layout consistency and responsiveness. - Introduced new styles for active type icons and improved hover effects for buttons. - Updated HomeHeroSettingsChips to change button titles and icons for clarity. - Added tests to ensure proper rendering and functionality of updated components. * feat(ProjectDesignSystemPicker): enhance design system selection with preview functionality - Updated the ProjectDesignSystemPicker component to include a preview feature for design systems, allowing users to see a preview of the selected design system. - Implemented hover functionality to update the preview based on the hovered design system. - Added fullscreen preview capability for a more immersive experience. - Enhanced CSS styles for the design system picker to improve layout and responsiveness. - Introduced tests to validate the new preview functionality and ensure proper interaction within the component. * feat: refactor project metadata handling and enhance design system picker - Updated the default scenario plugin ID retrieval to use project metadata, improving the logic for determining the appropriate plugin based on project intent. - Enhanced the ProjectDesignSystemPicker and related components to support localized design system summaries and categories, improving user experience. - Introduced new translations for working directory and design system picker components, ensuring better accessibility and usability across different locales. - Added a new 'live-artifact' project type to the HomeHero chips, expanding the functionality for users creating refreshable artifacts. - Updated tests to validate the new project metadata handling and design system picker functionalities. * feat: enhance localization and styling for design system components - Added French translations for working directory and design system picker components, improving accessibility for French-speaking users. - Updated CSS styles for the pet task item to ensure consistent padding and layout. - Introduced a new test suite for HomeHeroSettingsChips to validate localization and design system selection functionality. - Enhanced ProjectDesignSystemPicker tests to ensure proper localization and interaction with design system categories. * fix: update .gitignore to include all claude-sessions directories and remove specific session files - Modified .gitignore to ensure all claude-sessions directories are ignored by using a wildcard pattern. - Deleted two specific claude-sessions markdown files to clean up unnecessary session data. * fix: repair home automation ci regressions * fix: stabilize artifact consistency e2e * Remove folder picker changes from PR 2400 --------- Co-authored-by: pftom <1043269994@qq.com> Co-authored-by: qiongyu1999 <2694684348@qq.com>
330 lines
13 KiB
TypeScript
330 lines
13 KiB
TypeScript
// Facet derivation contract for the plugins-home filter row. The
|
|
// home section is driven by a single curated workflow axis (Import /
|
|
// Create / Export / Refine / Extend) plus scoped subcategories inside
|
|
// the active lane. These tests lock the per-record category extraction,
|
|
// the catalog build (preserves curated order, drops empty buckets), and
|
|
// the selection-based filtering so the manifest fields the catalog
|
|
// depends on don't silently drift.
|
|
|
|
import { describe, expect, it } from 'vitest';
|
|
import type { InstalledPluginRecord } from '@open-design/contracts';
|
|
import {
|
|
applyFacetSelection,
|
|
buildFacetCatalog,
|
|
extractCategories,
|
|
extractSubcategories,
|
|
isFeaturedPlugin,
|
|
resolveDefaultSelection,
|
|
} from '../../src/components/plugins-home/facets';
|
|
|
|
function fixture(overrides: {
|
|
id: string;
|
|
title?: string;
|
|
tags?: string[];
|
|
od?: Record<string, unknown>;
|
|
}): InstalledPluginRecord {
|
|
return {
|
|
id: overrides.id,
|
|
title: overrides.title ?? overrides.id,
|
|
version: '0.1.0',
|
|
sourceKind: 'bundled',
|
|
source: '/tmp',
|
|
trust: 'bundled',
|
|
capabilitiesGranted: ['prompt:inject'],
|
|
manifest: {
|
|
name: overrides.id,
|
|
version: '0.1.0',
|
|
...(overrides.tags ? { tags: overrides.tags } : {}),
|
|
...(overrides.od ? { od: overrides.od } : {}),
|
|
},
|
|
fsPath: '/tmp',
|
|
installedAt: 0,
|
|
updatedAt: 0,
|
|
};
|
|
}
|
|
|
|
describe('extractCategories', () => {
|
|
it('maps generation modes to the single Create lane', () => {
|
|
expect(extractCategories(fixture({ id: 'a', od: { mode: 'deck' } }))).toEqual(['create']);
|
|
expect(extractCategories(fixture({ id: 'b', od: { mode: 'prototype' } }))).toEqual(['create']);
|
|
expect(extractCategories(fixture({ id: 'c', od: { mode: 'design-system' } }))).toEqual(['create']);
|
|
expect(extractCategories(fixture({ id: 'd', od: { mode: 'image' } }))).toEqual(['create']);
|
|
expect(extractCategories(fixture({ id: 'e', od: { mode: 'video' } }))).toEqual(['create']);
|
|
expect(extractCategories(fixture({ id: 'f', od: { mode: 'audio' } }))).toEqual(['create']);
|
|
});
|
|
|
|
it('maps workflow scenario plugins to a single semantic lane', () => {
|
|
expect(
|
|
extractCategories(fixture({ id: 'figma', od: { taskKind: 'figma-migration', mode: 'scenario' } })),
|
|
).toEqual(['import']);
|
|
expect(
|
|
extractCategories(fixture({ id: 'folder', od: { taskKind: 'code-migration', mode: 'scenario' } })),
|
|
).toEqual(['import']);
|
|
expect(
|
|
extractCategories(fixture({ id: 'new', od: { taskKind: 'new-generation', mode: 'scenario' } })),
|
|
).toEqual(['create']);
|
|
expect(
|
|
extractCategories(fixture({ id: 'react-export', tags: ['export', 'react'], od: { mode: 'export' } })),
|
|
).toEqual(['export']);
|
|
expect(
|
|
extractCategories(fixture({ id: 'pptx-export', tags: ['html-to-pptx'], od: { mode: 'utility' } })),
|
|
).toEqual(['export']);
|
|
expect(
|
|
extractCategories(fixture({ id: 'vercel-deploy', tags: ['deploy', 'vercel'], od: { mode: 'utility' } })),
|
|
).toEqual(['deploy']);
|
|
expect(
|
|
extractCategories(fixture({ id: 'slack-share', tags: ['share', 'slack'], od: { mode: 'utility' } })),
|
|
).toEqual(['share']);
|
|
expect(
|
|
extractCategories(fixture({ id: 'tune', od: { taskKind: 'tune-collab', mode: 'scenario' } })),
|
|
).toEqual(['refine']);
|
|
expect(
|
|
extractCategories(fixture({ id: 'author', tags: ['plugin-authoring'], od: { taskKind: 'new-generation', mode: 'scenario' } })),
|
|
).toEqual(['extend']);
|
|
});
|
|
|
|
it('keeps concrete create types under Create instead of duplicating child tabs', () => {
|
|
const f = extractCategories(
|
|
fixture({ id: 'a', tags: ['hyperframes', 'cinematic'], od: { mode: 'video' } }),
|
|
);
|
|
expect(f).toEqual(['create']);
|
|
});
|
|
|
|
it('returns no curated categories for plugins outside the shortlist', () => {
|
|
expect(extractCategories(fixture({ id: 'a', od: { mode: 'utility' } }))).toEqual([]);
|
|
expect(extractCategories(fixture({ id: 'b', od: { mode: 'template' } }))).toEqual([]);
|
|
expect(extractCategories(fixture({ id: 'c', od: { mode: 'scenario' } }))).toEqual([]);
|
|
expect(extractCategories(fixture({ id: 'd', od: {} }))).toEqual([]);
|
|
});
|
|
|
|
it('normalises mode casing / formatting via slugify before matching', () => {
|
|
expect(extractCategories(fixture({ id: 'a', od: { mode: 'Design System' } }))).toEqual(['create']);
|
|
expect(extractCategories(fixture({ id: 'b', od: { mode: 'design_system' } }))).toEqual(['create']);
|
|
});
|
|
});
|
|
|
|
describe('extractSubcategories', () => {
|
|
it('maps Create plugins to concrete accumulated buckets', () => {
|
|
expect(extractSubcategories(fixture({ id: 'a', od: { mode: 'prototype' } }))).toEqual(['prototype']);
|
|
expect(extractSubcategories(fixture({ id: 'b', od: { mode: 'deck' } }))).toEqual(['deck']);
|
|
expect(extractSubcategories(fixture({ id: 'c', od: { mode: 'design-system' } }))).toEqual(['design-system']);
|
|
expect(extractSubcategories(fixture({ id: 'd', tags: ['hyperframes'], od: { mode: 'video' } }))).toEqual(['hyperframes']);
|
|
expect(extractSubcategories(fixture({ id: 'e', od: { mode: 'image' } }))).toEqual(['image']);
|
|
});
|
|
|
|
it('maps Import and Export plugins to lane-scoped child buckets', () => {
|
|
expect(
|
|
extractSubcategories(fixture({ id: 'figma', od: { taskKind: 'figma-migration', mode: 'scenario' } })),
|
|
).toEqual(['from-figma']);
|
|
expect(
|
|
extractSubcategories(fixture({ id: 'folder', od: { taskKind: 'code-migration', mode: 'scenario' } })),
|
|
).toEqual(['from-code']);
|
|
expect(
|
|
extractSubcategories(fixture({ id: 'next-export', tags: ['export', 'nextjs', 'react'], od: { mode: 'export' } })),
|
|
).toEqual(['nextjs']);
|
|
expect(
|
|
extractSubcategories(fixture({ id: 'react-export', tags: ['export', 'react'], od: { mode: 'export' } })),
|
|
).toEqual(['reactjs']);
|
|
expect(
|
|
extractSubcategories(fixture({ id: 'vue-export', tags: ['export', 'vuejs'], od: { mode: 'export' } })),
|
|
).toEqual(['vuejs']);
|
|
expect(
|
|
extractSubcategories(fixture({ id: 'svelte-export', tags: ['export', 'sveltejs'], od: { mode: 'export' } })),
|
|
).toEqual(['sveltejs']);
|
|
expect(
|
|
extractSubcategories(fixture({ id: 'pptx-export', tags: ['html-to-pptx'], od: { mode: 'utility' } })),
|
|
).toEqual(['pptx']);
|
|
expect(
|
|
extractSubcategories(fixture({ id: 'pdf-export', tags: ['pdf-guide'], od: { mode: 'utility' } })),
|
|
).toEqual(['pdf']);
|
|
});
|
|
});
|
|
|
|
describe('buildFacetCatalog', () => {
|
|
it('produces a single category axis with curated order preserved and empty buckets dropped', () => {
|
|
const plugins = [
|
|
fixture({ id: 'source', od: { taskKind: 'figma-migration', mode: 'scenario' } }),
|
|
fixture({ id: 'a', od: { mode: 'design-system' } }),
|
|
fixture({ id: 'b', od: { mode: 'design-system' } }),
|
|
fixture({ id: 'c', od: { mode: 'deck' } }),
|
|
fixture({ id: 'd', od: { mode: 'image' } }),
|
|
fixture({ id: 'e', od: { mode: 'video' } }),
|
|
fixture({ id: 'f', tags: ['hyperframes'], od: { mode: 'video' } }),
|
|
fixture({ id: 'react-export', tags: ['export', 'react'], od: { mode: 'export' } }),
|
|
fixture({ id: 'next-export', tags: ['export', 'nextjs', 'react'], od: { mode: 'export' } }),
|
|
fixture({ id: 'vue-export', tags: ['export', 'vuejs'], od: { mode: 'export' } }),
|
|
fixture({ id: 'svelte-export', tags: ['export', 'sveltejs'], od: { mode: 'export' } }),
|
|
fixture({ id: 'pptx-export', tags: ['html-to-pptx'], od: { mode: 'utility' } }),
|
|
fixture({ id: 'pdf-export', tags: ['pdf-guide'], od: { mode: 'utility' } }),
|
|
fixture({ id: 'tune', od: { taskKind: 'tune-collab', mode: 'scenario' } }),
|
|
fixture({ id: 'author', tags: ['plugin-authoring'], od: { taskKind: 'new-generation', mode: 'scenario' } }),
|
|
// Plugins outside the shortlist do not surface as filter pills.
|
|
fixture({ id: 'g', od: { mode: 'utility' } }),
|
|
];
|
|
const catalog = buildFacetCatalog(plugins);
|
|
expect(catalog.category.map((o) => o.slug)).toEqual([
|
|
'import',
|
|
'create',
|
|
'export',
|
|
'share',
|
|
'deploy',
|
|
'refine',
|
|
'extend',
|
|
]);
|
|
expect(catalog.category.find((o) => o.slug === 'create')?.count).toBe(6);
|
|
expect(catalog.category.find((o) => o.slug === 'import')?.count).toBe(1);
|
|
expect(catalog.category.find((o) => o.slug === 'export')?.count).toBe(6);
|
|
expect(catalog.category.find((o) => o.slug === 'share')?.count).toBe(0);
|
|
expect(catalog.category.find((o) => o.slug === 'deploy')?.count).toBe(0);
|
|
expect(catalog.category.find((o) => o.slug === 'refine')?.count).toBe(1);
|
|
expect(catalog.category.find((o) => o.slug === 'extend')?.count).toBe(1);
|
|
expect((catalog.subcategory.create ?? []).map((o) => o.slug)).toEqual([
|
|
'prototype',
|
|
'deck',
|
|
'live-artifact',
|
|
'design-system',
|
|
'hyperframes',
|
|
'image',
|
|
'video',
|
|
'audio',
|
|
]);
|
|
expect((catalog.subcategory.import ?? []).map((o) => o.slug)).toEqual([
|
|
'from-figma',
|
|
'from-github',
|
|
'from-code',
|
|
'from-url',
|
|
'from-screenshot',
|
|
'from-pdf',
|
|
'from-pptx',
|
|
'from-framer',
|
|
'from-webflow',
|
|
]);
|
|
expect((catalog.subcategory.export ?? []).map((o) => o.slug)).toEqual([
|
|
'pptx',
|
|
'pdf',
|
|
'html',
|
|
'zip',
|
|
'markdown',
|
|
'figma',
|
|
'nextjs',
|
|
'reactjs',
|
|
'vuejs',
|
|
'sveltejs',
|
|
'astro',
|
|
'angular',
|
|
'tailwind',
|
|
]);
|
|
expect((catalog.subcategory.deploy ?? []).map((o) => o.slug)).toEqual([
|
|
'vercel',
|
|
'cloudflare',
|
|
'netlify',
|
|
'github-pages',
|
|
'fly-io',
|
|
'render',
|
|
'docker',
|
|
]);
|
|
});
|
|
|
|
it('keeps planned category and subcategory axes when no plugin matches', () => {
|
|
const catalog = buildFacetCatalog([
|
|
fixture({ id: 'a', od: { mode: 'utility' } }),
|
|
fixture({ id: 'b', od: { mode: 'template' } }),
|
|
]);
|
|
expect(catalog.category.map((o) => [o.slug, o.count])).toEqual([
|
|
['import', 0],
|
|
['create', 0],
|
|
['export', 0],
|
|
['share', 0],
|
|
['deploy', 0],
|
|
['refine', 0],
|
|
['extend', 0],
|
|
]);
|
|
expect(catalog.subcategory.deploy?.find((o) => o.slug === 'vercel')?.count).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe('applyFacetSelection', () => {
|
|
const plugins = [
|
|
fixture({ id: 'a', od: { mode: 'design-system' } }),
|
|
fixture({ id: 'b', od: { mode: 'prototype' } }),
|
|
fixture({ id: 'c', od: { mode: 'image' } }),
|
|
fixture({ id: 'd', od: { mode: 'video' } }),
|
|
fixture({ id: 'e', tags: ['hyperframes'], od: { mode: 'video' } }),
|
|
fixture({ id: 'f', tags: ['export', 'react'], od: { mode: 'export' } }),
|
|
fixture({ id: 'h', tags: ['html-to-pptx'], od: { mode: 'utility' } }),
|
|
fixture({ id: 'g', od: { taskKind: 'code-migration', mode: 'scenario' } }),
|
|
];
|
|
|
|
it('returns everything when no category is selected', () => {
|
|
expect(
|
|
applyFacetSelection(plugins, { category: null, subcategory: null }).map((p) => p.id),
|
|
).toEqual(['a', 'b', 'c', 'd', 'e', 'f', 'h', 'g']);
|
|
});
|
|
|
|
it('filters by the selected category slug', () => {
|
|
expect(
|
|
applyFacetSelection(plugins, { category: 'create', subcategory: null }).map((p) => p.id),
|
|
).toEqual(['a', 'b', 'c', 'd', 'e']);
|
|
expect(
|
|
applyFacetSelection(plugins, { category: 'export', subcategory: null }).map((p) => p.id),
|
|
).toEqual(['f', 'h']);
|
|
expect(
|
|
applyFacetSelection(plugins, { category: 'import', subcategory: null }).map((p) => p.id),
|
|
).toEqual(['g']);
|
|
});
|
|
|
|
it('filters by the selected subcategory inside the selected category', () => {
|
|
expect(
|
|
applyFacetSelection(plugins, { category: 'create', subcategory: 'design-system' }).map((p) => p.id),
|
|
).toEqual(['a']);
|
|
expect(
|
|
applyFacetSelection(plugins, { category: 'create', subcategory: 'hyperframes' }).map((p) => p.id),
|
|
).toEqual(['e']);
|
|
expect(
|
|
applyFacetSelection(plugins, { category: 'export', subcategory: 'reactjs' }).map((p) => p.id),
|
|
).toEqual(['f']);
|
|
expect(
|
|
applyFacetSelection(plugins, { category: 'export', subcategory: 'pptx' }).map((p) => p.id),
|
|
).toEqual(['h']);
|
|
});
|
|
|
|
it('returns an empty list when no plugin matches the selected category', () => {
|
|
expect(
|
|
applyFacetSelection(plugins, { category: 'refine', subcategory: null }).map((p) => p.id),
|
|
).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe('isFeaturedPlugin', () => {
|
|
it('returns true for boolean featured picks and numeric curator ranks', () => {
|
|
expect(isFeaturedPlugin(fixture({ id: 'a', od: { featured: true } }))).toBe(true);
|
|
expect(isFeaturedPlugin(fixture({ id: 'ranked', od: { featured: 4 } }))).toBe(true);
|
|
expect(isFeaturedPlugin(fixture({ id: 'b', od: { featured: 'true' } }))).toBe(false);
|
|
expect(isFeaturedPlugin(fixture({ id: 'c' }))).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('resolveDefaultSelection', () => {
|
|
it('defaults the home catalog to Create > Slides when that bucket exists', () => {
|
|
const catalog = buildFacetCatalog([
|
|
fixture({ id: 'slides', od: { mode: 'deck' } }),
|
|
fixture({ id: 'prototype', od: { mode: 'prototype' } }),
|
|
]);
|
|
|
|
expect(resolveDefaultSelection(catalog)).toEqual({
|
|
category: 'create',
|
|
subcategory: 'deck',
|
|
});
|
|
});
|
|
|
|
it('falls back to the Create lane when Slides is unavailable', () => {
|
|
const catalog = buildFacetCatalog([
|
|
fixture({ id: 'prototype', od: { mode: 'prototype' } }),
|
|
]);
|
|
|
|
expect(resolveDefaultSelection(catalog)).toEqual({
|
|
category: 'create',
|
|
subcategory: null,
|
|
});
|
|
});
|
|
});
|