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>
400 lines
12 KiB
TypeScript
400 lines
12 KiB
TypeScript
// @vitest-environment jsdom
|
|
|
|
import { cleanup, fireEvent, render, screen } from '@testing-library/react';
|
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
import type {
|
|
InputFieldSpec,
|
|
InstalledPluginRecord,
|
|
McpServerConfig,
|
|
PluginSourceKind,
|
|
SkillSummary,
|
|
TrustTier,
|
|
} from '@open-design/contracts';
|
|
import { HomeHero } from '../../src/components/HomeHero';
|
|
|
|
function makePlugin(
|
|
id: string,
|
|
title: string,
|
|
sourceKind: PluginSourceKind = 'bundled',
|
|
trust: TrustTier = 'bundled',
|
|
): InstalledPluginRecord {
|
|
return {
|
|
id,
|
|
title,
|
|
version: '1.0.0',
|
|
sourceKind,
|
|
source: '/tmp',
|
|
trust,
|
|
capabilitiesGranted: ['prompt:inject'],
|
|
manifest: {
|
|
name: id,
|
|
version: '1.0.0',
|
|
title,
|
|
description: 'A plugin fixture',
|
|
tags: ['fixture'],
|
|
},
|
|
fsPath: '/tmp',
|
|
installedAt: 0,
|
|
updatedAt: 0,
|
|
};
|
|
}
|
|
|
|
function makeSkill(id: string, name: string): SkillSummary {
|
|
return {
|
|
id,
|
|
name,
|
|
description: 'A skill fixture',
|
|
triggers: ['fixture'],
|
|
mode: 'prototype',
|
|
previewType: 'html',
|
|
designSystemRequired: false,
|
|
defaultFor: [],
|
|
upstream: null,
|
|
hasBody: true,
|
|
examplePrompt: `Use ${name}`,
|
|
aggregatesExamples: false,
|
|
};
|
|
}
|
|
|
|
function makeMcp(id: string, label: string): McpServerConfig {
|
|
return {
|
|
id,
|
|
label,
|
|
transport: 'stdio',
|
|
enabled: true,
|
|
command: 'npx',
|
|
};
|
|
}
|
|
|
|
afterEach(() => {
|
|
cleanup();
|
|
});
|
|
|
|
describe('HomeHero plugin picker', () => {
|
|
it('opens plugin search from an @ token across community and my plugins', () => {
|
|
const onPromptChange = vi.fn();
|
|
const onPickPlugin = vi.fn();
|
|
render(
|
|
<HomeHero
|
|
prompt="Make @sam"
|
|
onPromptChange={onPromptChange}
|
|
onSubmit={() => undefined}
|
|
activePluginTitle={null}
|
|
activeChipId={null}
|
|
onClearActivePlugin={() => undefined}
|
|
pluginOptions={[
|
|
makePlugin('sample-plugin', 'Sample Plugin'),
|
|
makePlugin('sample-user-plugin', 'Sample User Plugin', 'github', 'restricted'),
|
|
]}
|
|
pluginsLoading={false}
|
|
pendingPluginId={null}
|
|
pendingChipId={null}
|
|
onPickPlugin={onPickPlugin}
|
|
onPickChip={() => undefined}
|
|
contextItemCount={0}
|
|
error={null}
|
|
/>,
|
|
);
|
|
|
|
expect(screen.getByTestId('home-hero-plugin-picker')).toBeTruthy();
|
|
expect(screen.getByText('Official')).toBeTruthy();
|
|
expect(screen.getByText('My plugin')).toBeTruthy();
|
|
fireEvent.mouseDown(screen.getByRole('option', { name: /sample user plugin/i }));
|
|
|
|
expect(onPickPlugin).toHaveBeenCalledWith(
|
|
expect.objectContaining({ id: 'sample-user-plugin' }),
|
|
'Make @Sample User Plugin',
|
|
);
|
|
});
|
|
|
|
it('renders selected @ plugins inside the prompt and opens their details', () => {
|
|
const onOpenPluginDetails = vi.fn();
|
|
const sample = makePlugin('sample-plugin', 'Sample Plugin');
|
|
const helper = makePlugin('helper-plugin', 'Helper Plugin');
|
|
|
|
render(
|
|
<HomeHero
|
|
prompt="Use @Sample Plugin with @Helper Plugin"
|
|
onPromptChange={() => undefined}
|
|
onSubmit={() => undefined}
|
|
activePluginTitle={null}
|
|
activeChipId={null}
|
|
onClearActivePlugin={() => undefined}
|
|
selectedPluginContexts={[sample, helper]}
|
|
onOpenPluginDetails={onOpenPluginDetails}
|
|
pluginOptions={[]}
|
|
pluginsLoading={false}
|
|
pendingPluginId={null}
|
|
pendingChipId={null}
|
|
onPickPlugin={() => undefined}
|
|
onPickChip={() => undefined}
|
|
contextItemCount={2}
|
|
error={null}
|
|
/>,
|
|
);
|
|
|
|
fireEvent.click(screen.getByTestId('home-hero-prompt-plugin-sample-plugin'));
|
|
expect(onOpenPluginDetails).toHaveBeenCalledWith(sample);
|
|
expect(screen.getByTestId('home-hero-prompt-plugin-helper-plugin')).toBeTruthy();
|
|
});
|
|
|
|
it('opens the context picker for a bare @ token even before results arrive', () => {
|
|
render(
|
|
<HomeHero
|
|
prompt="@"
|
|
onPromptChange={() => undefined}
|
|
onSubmit={() => undefined}
|
|
activePluginTitle={null}
|
|
activeChipId={null}
|
|
onClearActivePlugin={() => undefined}
|
|
pluginOptions={[]}
|
|
pluginsLoading={false}
|
|
skillOptions={[]}
|
|
skillsLoading={false}
|
|
mcpOptions={[]}
|
|
mcpLoading={false}
|
|
pendingPluginId={null}
|
|
pendingChipId={null}
|
|
onPickPlugin={() => undefined}
|
|
onPickChip={() => undefined}
|
|
contextItemCount={0}
|
|
error={null}
|
|
/>,
|
|
);
|
|
|
|
expect(screen.getByTestId('home-hero-plugin-picker')).toBeTruthy();
|
|
expect(screen.getByRole('tab', { name: /plugins/i })).toBeTruthy();
|
|
expect(screen.getByRole('tab', { name: /skills/i })).toBeTruthy();
|
|
expect(screen.getByRole('tab', { name: /mcp/i })).toBeTruthy();
|
|
expect(screen.getByRole('tab', { name: /connectors/i })).toBeTruthy();
|
|
expect(screen.getByText('Search plugins, skills, MCP servers, and connectors.')).toBeTruthy();
|
|
});
|
|
|
|
it('can pick skills and MCP servers from the home @ picker', () => {
|
|
const onPickSkill = vi.fn();
|
|
const onPickMcp = vi.fn();
|
|
const skill = makeSkill('prototype-lab', 'Prototype Lab');
|
|
const mcp = makeMcp('linear', 'Linear');
|
|
const { rerender } = render(
|
|
<HomeHero
|
|
prompt="Make @proto"
|
|
onPromptChange={() => undefined}
|
|
onSubmit={() => undefined}
|
|
activePluginTitle={null}
|
|
activeChipId={null}
|
|
onClearActivePlugin={() => undefined}
|
|
pluginOptions={[]}
|
|
pluginsLoading={false}
|
|
skillOptions={[skill]}
|
|
skillsLoading={false}
|
|
mcpOptions={[mcp]}
|
|
mcpLoading={false}
|
|
pendingPluginId={null}
|
|
pendingChipId={null}
|
|
onPickPlugin={() => undefined}
|
|
onPickSkill={onPickSkill}
|
|
onPickMcp={onPickMcp}
|
|
onPickChip={() => undefined}
|
|
contextItemCount={0}
|
|
error={null}
|
|
/>,
|
|
);
|
|
|
|
fireEvent.mouseDown(screen.getByRole('option', { name: /prototype lab/i }));
|
|
expect(onPickSkill).toHaveBeenCalledWith(skill, 'Make @Prototype Lab');
|
|
|
|
rerender(
|
|
<HomeHero
|
|
prompt="@lin"
|
|
onPromptChange={() => undefined}
|
|
onSubmit={() => undefined}
|
|
activePluginTitle={null}
|
|
activeChipId={null}
|
|
onClearActivePlugin={() => undefined}
|
|
pluginOptions={[]}
|
|
pluginsLoading={false}
|
|
skillOptions={[skill]}
|
|
skillsLoading={false}
|
|
mcpOptions={[mcp]}
|
|
mcpLoading={false}
|
|
pendingPluginId={null}
|
|
pendingChipId={null}
|
|
onPickPlugin={() => undefined}
|
|
onPickSkill={onPickSkill}
|
|
onPickMcp={onPickMcp}
|
|
onPickChip={() => undefined}
|
|
contextItemCount={0}
|
|
error={null}
|
|
/>,
|
|
);
|
|
|
|
fireEvent.mouseDown(screen.getByRole('option', { name: /linear/i }));
|
|
expect(onPickMcp).toHaveBeenCalledWith(mcp, '@Linear');
|
|
});
|
|
|
|
it('does not submit while an IME composition is confirming text with Enter', () => {
|
|
const onSubmit = vi.fn();
|
|
render(
|
|
<HomeHero
|
|
prompt="做一个中文官网"
|
|
onPromptChange={() => undefined}
|
|
onSubmit={onSubmit}
|
|
activePluginTitle={null}
|
|
activeChipId={null}
|
|
onClearActivePlugin={() => undefined}
|
|
pluginOptions={[]}
|
|
pluginsLoading={false}
|
|
pendingPluginId={null}
|
|
pendingChipId={null}
|
|
onPickPlugin={() => undefined}
|
|
onPickChip={() => undefined}
|
|
contextItemCount={0}
|
|
error={null}
|
|
/>,
|
|
);
|
|
|
|
const input = screen.getByTestId('home-hero-input');
|
|
fireEvent.compositionStart(input);
|
|
fireEvent.keyDown(input, { key: 'Enter', code: 'Enter' });
|
|
expect(onSubmit).not.toHaveBeenCalled();
|
|
|
|
fireEvent.compositionEnd(input);
|
|
fireEvent.keyDown(input, { key: 'Enter', code: 'Enter' });
|
|
expect(onSubmit).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('does not pick a plugin while an IME composition is active', () => {
|
|
const onPickPlugin = vi.fn();
|
|
const onSubmit = vi.fn();
|
|
render(
|
|
<HomeHero
|
|
prompt="Make @sam"
|
|
onPromptChange={() => undefined}
|
|
onSubmit={onSubmit}
|
|
activePluginTitle={null}
|
|
activeChipId={null}
|
|
onClearActivePlugin={() => undefined}
|
|
pluginOptions={[makePlugin('sample-plugin', 'Sample Plugin')]}
|
|
pluginsLoading={false}
|
|
pendingPluginId={null}
|
|
pendingChipId={null}
|
|
onPickPlugin={onPickPlugin}
|
|
onPickChip={() => undefined}
|
|
contextItemCount={0}
|
|
error={null}
|
|
/>,
|
|
);
|
|
|
|
const input = screen.getByTestId('home-hero-input');
|
|
fireEvent.compositionStart(input);
|
|
fireEvent.keyDown(input, { key: 'Enter', code: 'Enter' });
|
|
|
|
expect(onPickPlugin).not.toHaveBeenCalled();
|
|
expect(onSubmit).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('highlights rendered plugin input values inside the prompt surface', () => {
|
|
const fields: InputFieldSpec[] = [
|
|
{
|
|
name: 'source',
|
|
label: 'Import source',
|
|
type: 'select',
|
|
options: ['folder', 'zip', 'github', 'marketplace'],
|
|
default: 'marketplace',
|
|
},
|
|
];
|
|
const prompt =
|
|
'Create a compact import receipt for community-import-smoke-test installed from marketplace.';
|
|
|
|
const { rerender } = render(
|
|
<HomeHero
|
|
prompt={prompt}
|
|
onPromptChange={() => undefined}
|
|
onSubmit={() => undefined}
|
|
activePluginTitle="Community Import Smoke Test"
|
|
activeChipId={null}
|
|
onClearActivePlugin={() => undefined}
|
|
pluginInputFields={fields}
|
|
pluginInputValues={{ source: 'marketplace' }}
|
|
pluginInputTemplate="Create a compact import receipt for community-import-smoke-test installed from {{source}}."
|
|
pluginOptions={[]}
|
|
pluginsLoading={false}
|
|
pendingPluginId={null}
|
|
pendingChipId={null}
|
|
onPickPlugin={() => undefined}
|
|
onPickChip={() => undefined}
|
|
contextItemCount={0}
|
|
error={null}
|
|
/>,
|
|
);
|
|
|
|
// The inline pill is a read-only span so its width tracks the
|
|
// textarea text exactly. (See HomeHero.tsx for why <input>/<select>
|
|
// at this position caused the overlay/textarea caret drift.)
|
|
const slot = screen.getByTestId('home-hero-prompt-slot-source');
|
|
expect(slot.tagName).toBe('SPAN');
|
|
expect(slot.textContent).toBe('marketplace');
|
|
expect(slot.getAttribute('data-filled')).toBe('true');
|
|
// The structured inputs form below the textarea is suppressed
|
|
// when every plugin input is already referenced in the template
|
|
// — otherwise the form would render a second, identical labelled
|
|
// input for every slot pill shown inline, making the chat box
|
|
// look like it had grown a second composer.
|
|
expect(screen.queryByTestId('plugin-inputs-form')).toBeNull();
|
|
|
|
rerender(
|
|
<HomeHero
|
|
prompt={`${prompt} Extra user edit.`}
|
|
onPromptChange={() => undefined}
|
|
onSubmit={() => undefined}
|
|
activePluginTitle="Community Import Smoke Test"
|
|
activeChipId={null}
|
|
onClearActivePlugin={() => undefined}
|
|
pluginInputFields={fields}
|
|
pluginInputValues={{ source: 'marketplace' }}
|
|
pluginInputTemplate="Create a compact import receipt for community-import-smoke-test installed from {{source}}."
|
|
pluginOptions={[]}
|
|
pluginsLoading={false}
|
|
pendingPluginId={null}
|
|
pendingChipId={null}
|
|
onPickPlugin={() => undefined}
|
|
onPickChip={() => undefined}
|
|
contextItemCount={0}
|
|
error={null}
|
|
/>,
|
|
);
|
|
|
|
expect(screen.queryByTestId('home-hero-prompt-slot-source')).toBeNull();
|
|
});
|
|
|
|
it('opens active plugin details from the active plugin chip', () => {
|
|
const onOpenPluginDetails = vi.fn();
|
|
const active = makePlugin('prototype-plugin', 'Prototype Plugin');
|
|
render(
|
|
<HomeHero
|
|
prompt="Build a prototype"
|
|
onPromptChange={() => undefined}
|
|
onSubmit={() => undefined}
|
|
activePluginTitle="Prototype"
|
|
activePluginRecord={active}
|
|
activeChipId="prototype"
|
|
onClearActivePlugin={() => undefined}
|
|
onOpenPluginDetails={onOpenPluginDetails}
|
|
pluginOptions={[]}
|
|
pluginsLoading={false}
|
|
pendingPluginId={null}
|
|
pendingChipId={null}
|
|
onPickPlugin={() => undefined}
|
|
onPickChip={() => undefined}
|
|
contextItemCount={0}
|
|
error={null}
|
|
/>,
|
|
);
|
|
|
|
fireEvent.click(screen.getByTitle('Plugin: Prototype Plugin'));
|
|
expect(onOpenPluginDetails).toHaveBeenCalledWith(active);
|
|
const activeChipText = screen.getByTestId('home-hero-active-plugin').textContent;
|
|
expect(activeChipText).toContain('Prototype');
|
|
expect(activeChipText).not.toContain('Plugin');
|
|
});
|
|
});
|