Initial import: open-design source for helix-mind.ai distribution
Some checks failed
ci / Validate workspace (push) Successful in 12m32s
landing-page-ci / Validate landing page (push) Successful in 9m41s
landing-page-deploy / Deploy landing page (push) Failing after 5m23s
github-metrics / Generate repository metrics SVG (push) Failing after 2m6s
refresh-contributors-wall / Refresh contributors wall cache bust (push) Failing after 12s

This repository contains the open-design daemon CLI source code, built
and packaged at https://helix-mind.ai/cli/open-design/latest.tgz for use
by the HelixMind /design slash command.

Licenses: Apache-2.0 (root) + MIT (skills/*)
This commit is contained in:
marco
2026-05-06 20:50:24 +02:00
commit 5dd70b5016
1336 changed files with 287186 additions and 0 deletions

1158
e2e/ui/app.test.ts Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,239 @@
import { expect, test } from '@playwright/test';
import type { Page } from '@playwright/test';
const STORAGE_KEY = 'open-design:config';
const CONNECTORS = [
{
id: 'github',
name: 'GitHub',
provider: 'composio',
category: 'Developer tools',
description: 'Read repository issues and pull requests.',
status: 'available',
auth: { provider: 'composio', configured: true },
tools: [
{
name: 'list_issues',
title: 'List issues',
description: 'List recent issues from a repository.',
safety: {
sideEffect: 'read',
approval: 'auto',
reason: 'Read-only issue lookup.',
},
refreshEligible: true,
},
],
},
{
id: 'slack',
name: 'Slack',
provider: 'composio',
category: 'Communication',
description: 'Search channels and messages.',
status: 'connected',
accountLabel: 'design-team',
auth: { provider: 'composio', configured: true },
tools: [],
},
];
const IMAGE_TEMPLATE = {
id: 'editorial-poster',
surface: 'image',
title: 'Editorial Poster',
summary: 'A punchy launch poster for a product announcement.',
category: 'Marketing',
tags: ['poster', 'launch'],
model: 'gpt-image-1',
aspect: '4:5',
source: {
repo: 'open-design/test-prompts',
license: 'MIT',
author: 'Open Design QA',
},
};
test.beforeEach(async ({ page }) => {
await page.addInitScript((key) => {
window.localStorage.setItem(
key,
JSON.stringify({
mode: 'daemon',
apiKey: '',
baseUrl: 'https://api.anthropic.com',
model: 'claude-sonnet-4-5',
agentId: 'mock',
skillId: null,
designSystemId: null,
onboardingCompleted: true,
agentModels: {},
}),
);
}, STORAGE_KEY);
await page.route('**/api/agents', async (route) => {
await route.fulfill({
json: {
agents: [
{
id: 'mock',
name: 'Mock Agent',
bin: 'mock-agent',
available: true,
version: 'test',
models: [{ id: 'default', label: 'Default' }],
},
],
},
});
});
});
test('prompt template retry preserves the edited body in project metadata', async ({ page }) => {
let detailRequests = 0;
await page.route('**/api/prompt-templates', async (route) => {
await route.fulfill({ json: { promptTemplates: [IMAGE_TEMPLATE] } });
});
await page.route('**/api/prompt-templates/image/editorial-poster', async (route) => {
detailRequests += 1;
if (detailRequests === 1) {
await route.fulfill({ status: 500, body: 'template unavailable' });
return;
}
await route.fulfill({
json: {
promptTemplate: {
...IMAGE_TEMPLATE,
prompt: 'Original poster prompt with dramatic type and product photography.',
},
},
});
});
await page.goto('/');
await page.getByTestId('new-project-tab-image').click();
await page.getByTestId('new-project-name').fill('Prompt template retry metadata');
await page.getByTestId('prompt-template-trigger').click();
await page.getByTestId('prompt-template-search').fill('poster');
await page.getByRole('option', { name: /Editorial Poster/i }).click();
await expect(page.getByTestId('prompt-template-error')).toBeVisible();
await page.getByTestId('prompt-template-retry').click();
await expect(page.getByTestId('prompt-template-error')).toHaveCount(0);
await expect(page.getByTestId('prompt-template-body')).toContainText('Original poster prompt');
await page.getByTestId('prompt-template-body').fill('');
await expect(page.getByTestId('prompt-template-empty-hint')).toBeVisible();
await page.getByTestId('prompt-template-body').fill(
'Edited QA prompt: bold poster, one hero product, crisp headline.',
);
await page.getByTestId('create-project').click();
const project = await fetchCurrentProject(page);
expect(project.metadata?.promptTemplate).toMatchObject({
id: 'editorial-poster',
surface: 'image',
title: 'Editorial Poster',
prompt: 'Edited QA prompt: bold poster, one hero product, crisp headline.',
});
});
test('live artifact empty connector CTA opens the gated connector setup path', async ({ page }) => {
await routeConnectors(page, []);
await page.goto('/');
await page.getByTestId('new-project-tab-live-artifact').click();
await expect(page.getByTestId('new-project-connectors')).toBeVisible();
await page.getByTestId('new-project-connectors-empty').click();
await expect(page.getByTestId('entry-tab-connectors')).toHaveAttribute('aria-selected', 'true');
await expect(page.getByTestId('connector-gate')).toBeVisible();
await page.getByTestId('connector-gate-action').click();
const settingsDialog = page.getByRole('dialog');
await expect(settingsDialog).toBeVisible();
await expect(settingsDialog.getByRole('heading', { name: 'Connectors' })).toBeVisible();
await expect(settingsDialog.getByPlaceholder('Paste Composio API key')).toBeVisible();
});
test('connectors search supports empty results and keyboard-closeable details', async ({ page }) => {
await routeConnectors(page, CONNECTORS);
await page.goto('/');
await page.getByTestId('entry-tab-connectors').click();
await expect(page.getByTestId('connector-grid-wrap')).toBeVisible();
const search = page.getByTestId('connectors-search-input');
await search.fill('git');
await expect(connectorCard(page, 'github')).toBeVisible();
await expect(connectorCard(page, 'slack')).toHaveCount(0);
await search.fill('missing connector');
await expect(page.getByTestId('connectors-empty')).toBeVisible();
await search.press('Escape');
await expect(page.getByTestId('connectors-empty')).toHaveCount(0);
await expect(connectorCard(page, 'github')).toBeVisible();
await expect(connectorCard(page, 'slack')).toBeVisible();
await connectorCard(page, 'github').click();
await expect(page.getByTestId('connector-drawer')).toBeVisible();
await expect(page.getByTestId('connector-drawer')).toContainText('List issues');
await page.keyboard.press('Escape');
await expect(page.getByTestId('connector-drawer')).toHaveCount(0);
});
async function routeConnectors(page: Page, connectors: typeof CONNECTORS) {
await page.route('**/api/connectors', async (route) => {
await route.fulfill({ json: { connectors } });
});
await page.route('**/api/connectors/status', async (route) => {
const statuses = Object.fromEntries(
connectors.map((connector) => [
connector.id,
{
status: connector.status,
accountLabel: connector.accountLabel,
},
]),
);
await route.fulfill({ json: { statuses } });
});
await page.route('**/api/connectors/discovery*', async (route) => {
await route.fulfill({
json: {
connectors,
meta: { provider: 'composio' },
},
});
});
}
function connectorCard(page: Page, id: string) {
return page.locator(`article.connector-card[data-connector-id="${id}"]`);
}
async function fetchCurrentProject(page: Page) {
await expect(page).toHaveURL(/\/projects\/[^/]+/);
const url = new URL(page.url());
const [, projectId] = url.pathname.match(/\/projects\/([^/]+)/) ?? [];
expect(projectId).toBeTruthy();
const response = await page.request.get(`/api/projects/${projectId}`);
expect(response.ok()).toBeTruthy();
const body = (await response.json()) as {
project: {
metadata?: {
promptTemplate?: {
id: string;
surface: string;
title: string;
prompt: string;
};
};
};
};
return body.project;
}

View File

@@ -0,0 +1,544 @@
import { expect, test } from '@playwright/test';
import type { Locator, Page } from '@playwright/test';
const STORAGE_KEY = 'open-design:config';
const DESIGN_SYSTEMS = [
{
id: 'nexu-soft-tech',
title: 'Nexu Soft Tech',
category: 'Product',
summary: 'Warm utility system for product interfaces.',
swatches: ['#F7F4EE', '#D6CBBF', '#1F2937', '#D97757'],
},
{
id: 'editorial-noir',
title: 'Editorial Noir',
category: 'Editorial',
summary: 'High-contrast editorial system with expressive type.',
swatches: ['#111111', '#F6EFE6', '#C44536', '#F2C14E'],
},
{
id: 'data-mist',
title: 'Data Mist',
category: 'Analytics',
summary: 'Calm dashboard system for dense data products.',
swatches: ['#EAF4F4', '#5EAAA8', '#05668D', '#0B132B'],
},
];
const TAB_SKILLS = [
skillSummary('prototype-skill', 'Prototype Skill', 'prototype', 'web', ['prototype']),
skillSummary('live-artifact', 'live-artifact', 'prototype', 'web', []),
skillSummary('deck-skill', 'Deck Skill', 'deck', 'web', ['deck']),
skillSummary('image-skill', 'Image Skill', 'image', 'image', ['image']),
];
test.beforeEach(async ({ page }) => {
await page.addInitScript((key) => {
window.localStorage.setItem(
key,
JSON.stringify({
mode: 'daemon',
apiKey: '',
baseUrl: 'https://api.anthropic.com',
model: 'claude-sonnet-4-5',
agentId: 'mock',
skillId: null,
designSystemId: null,
onboardingCompleted: true,
agentModels: {},
}),
);
}, STORAGE_KEY);
await page.route('**/api/agents', async (route) => {
await route.fulfill({
json: {
agents: [
{
id: 'mock',
name: 'Mock Agent',
bin: 'mock-agent',
available: true,
version: 'test',
models: [{ id: 'default', label: 'Default' }],
},
],
},
});
});
});
test('new project tabs switch visible form sections and preserve drafts', async ({ page }) => {
await page.route('**/api/skills', async (route) => {
await route.fulfill({ json: { skills: TAB_SKILLS } });
});
await page.route('**/api/connectors', async (route) => {
await route.fulfill({ json: { connectors: [] } });
});
await page.route('**/api/connectors/status', async (route) => {
await route.fulfill({ json: { statuses: {} } });
});
await page.goto('/');
await expect(page.getByTestId('new-project-tab-prototype')).toHaveAttribute('aria-selected', 'true');
await expect(page.locator('.newproj-title')).toContainText('New prototype');
await expect(page.getByTestId('design-system-trigger')).toBeVisible();
await expect(page.getByText('Fidelity', { exact: true })).toBeVisible();
await page.getByTestId('new-project-name').fill('Prototype draft survives');
await page.getByTestId('new-project-tab-live-artifact').click();
await expect(page.getByTestId('new-project-tab-live-artifact')).toHaveAttribute('aria-selected', 'true');
await expect(page.locator('.newproj-title')).toContainText('New live artifact');
await expect(page.locator('.newproj-title')).toContainText('Beta');
await expect(page.getByTestId('design-system-picker')).toHaveCount(0);
await expect(page.getByTestId('new-project-connectors')).toBeVisible();
await expect(page.getByTestId('create-project')).toContainText('Create live artifact');
await page.getByTestId('new-project-tab-deck').click();
await expect(page.getByTestId('new-project-tab-deck')).toHaveAttribute('aria-selected', 'true');
await expect(page.locator('.newproj-title')).toContainText('New slide deck');
await expect(page.getByTestId('design-system-trigger')).toBeVisible();
await expect(page.getByText('Use speaker notes')).toBeVisible();
await expect(page.getByTestId('new-project-connectors')).toHaveCount(0);
await page.getByTestId('new-project-tab-prototype').click();
await expect(page.getByTestId('new-project-tab-prototype')).toHaveAttribute('aria-selected', 'true');
await expect(page.locator('.newproj-title')).toContainText('New prototype');
await expect(page.getByTestId('new-project-name')).toHaveValue('Prototype draft survives');
await page.getByRole('button', { name: 'Scroll project types right' }).click();
await page.getByTestId('new-project-tab-image').click();
await expect(page.getByTestId('new-project-tab-image')).toHaveAttribute('aria-selected', 'true');
await expect(page.locator('.newproj-title')).toContainText('New image');
await expect(page.getByTestId('design-system-picker')).toHaveCount(0);
await expect(page.getByText('Model', { exact: true })).toBeVisible();
await expect(page.getByText('Aspect', { exact: true })).toBeVisible();
});
test('design system multi-select stores primary and inspiration metadata', async ({ page }) => {
await page.route('**/api/design-systems', async (route) => {
await route.fulfill({ json: { designSystems: DESIGN_SYSTEMS } });
});
await page.goto('/');
await page.getByTestId('new-project-tab-prototype').click();
await page.getByTestId('new-project-name').fill('Design system multi select metadata');
await page.getByTestId('design-system-trigger').click();
await page.getByRole('tab', { name: /multi/i }).click();
await page.getByRole('option', { name: /Nexu Soft Tech/i }).click();
await page.getByRole('option', { name: /Editorial Noir/i }).click();
await page.getByRole('option', { name: /Data Mist/i }).click();
await expect(page.getByTestId('design-system-trigger')).toContainText('Nexu Soft Tech');
await expect(page.getByTestId('design-system-trigger')).toContainText('+2');
await page.keyboard.press('Escape');
await page.getByTestId('create-project').click();
await expectWorkspaceReady(page);
const project = await fetchCurrentProject(page);
expect(project.designSystemId).toBe('nexu-soft-tech');
expect(project.metadata?.inspirationDesignSystemIds).toEqual([
'editorial-noir',
'data-mist',
]);
});
test('design system picker searches and switches the single selected system', async ({ page }) => {
await page.route('**/api/design-systems', async (route) => {
await route.fulfill({ json: { designSystems: DESIGN_SYSTEMS } });
});
await page.goto('/');
await page.getByTestId('new-project-tab-prototype').click();
await page.getByTestId('new-project-name').fill('Design system single switch flow');
await expect(page.getByTestId('design-system-trigger')).toBeVisible();
await page.getByTestId('design-system-trigger').click();
await page.getByTestId('design-system-search').fill('mist');
await expect(page.getByRole('option', { name: /Data Mist/i })).toBeVisible();
await expect(page.getByRole('option', { name: /Nexu Soft Tech/i })).toHaveCount(0);
await page.getByRole('option', { name: /Data Mist/i }).click();
await expect(page.getByTestId('design-system-trigger')).toContainText('Data Mist');
await expect(page.getByTestId('design-system-trigger')).toContainText('Analytics');
await page.getByTestId('create-project').click();
await expectWorkspaceReady(page);
const project = await fetchCurrentProject(page);
expect(project.designSystemId).toBe('data-mist');
expect(project.metadata?.inspirationDesignSystemIds).toBeUndefined();
});
test('project title rename persists after reload and ignores blank titles', async ({ page }) => {
await page.goto('/');
await createProject(page, 'Original rename title');
await expectWorkspaceReady(page);
const title = page.getByTestId('project-title');
await renameProjectTitle(page, title, 'Renamed persistent title');
await expect(title).toContainText('Renamed persistent title');
await page.reload();
await expectWorkspaceReady(page);
await expect(page.getByTestId('project-title')).toContainText('Renamed persistent title');
await renameProjectTitle(page, page.getByTestId('project-title'), ' ');
await page.reload();
await expectWorkspaceReady(page);
await expect(page.getByTestId('project-title')).toContainText('Renamed persistent title');
const project = await fetchCurrentProject(page);
expect(project.name).toBe('Renamed persistent title');
});
test('canceling design file deletion keeps the file and open tab', async ({ page }) => {
await page.goto('/');
await createProject(page, 'Design file delete cancel flow');
await expectWorkspaceReady(page);
const uploadedName = await uploadTinyPng(page, 'delete-cancel.png');
const fileTab = tabBySuffix(page, uploadedName);
await expect(fileTab).toHaveAttribute('aria-selected', 'true');
page.once('dialog', async (dialog) => {
expect(dialog.message()).toContain('delete-cancel.png');
await dialog.dismiss();
});
await page.getByTestId('design-files-tab').click();
await rowByFileName(page, uploadedName).hover();
await menuByFileName(page, uploadedName).click();
await page.getByTestId(`design-file-delete-${uploadedName}`).click();
await expect(rowByFileName(page, uploadedName)).toBeVisible();
await expect(fileTab).toBeVisible();
const { projectId } = getProjectContextFromUrl(page);
const files = await listProjectFiles(page, projectId);
expect(files.map((file) => file.name)).toContain(uploadedName);
});
test('home design card deletion supports cancel and confirm flows', async ({ page }) => {
const projectName = `Home delete design flow ${Date.now()}`;
await page.goto('/');
await createProject(page, projectName);
await expectWorkspaceReady(page);
const { projectId } = getProjectContextFromUrl(page);
await page.getByRole('button', { name: /back to projects/i }).click();
await expect(page.getByTestId('new-project-panel')).toBeVisible();
const designCard = homeDesignCard(page, projectName);
await expect(designCard).toBeVisible();
page.once('dialog', async (dialog) => {
expect(dialog.message()).toContain(projectName);
await dialog.dismiss();
});
await designCard.hover();
await designCard.getByRole('button', { name: new RegExp(`delete project ${escapeRegExp(projectName)}`, 'i') }).click();
await expect(designCard).toBeVisible();
page.once('dialog', async (dialog) => {
expect(dialog.message()).toContain(projectName);
await dialog.accept();
});
await designCard.hover();
await designCard.getByRole('button', { name: new RegExp(`delete project ${escapeRegExp(projectName)}`, 'i') }).click();
await expect(homeDesignCard(page, projectName)).toHaveCount(0);
const response = await page.request.get(`/api/projects/${projectId}`);
expect(response.status()).toBe(404);
});
test('home designs view toggle switches between grid and kanban and persists', async ({ page }) => {
const projectName = `Home view toggle flow ${Date.now()}`;
await page.goto('/');
await createProject(page, projectName);
await expectWorkspaceReady(page);
await page.getByRole('button', { name: /back to projects/i }).click();
await expect(page.getByTestId('new-project-panel')).toBeVisible();
await expect(homeDesignCard(page, projectName)).toBeVisible();
await expect(page.locator('.design-grid')).toBeVisible();
await expect(page.locator('.design-kanban-board')).toHaveCount(0);
await expect(page.getByTestId('designs-view-grid')).toHaveAttribute('aria-pressed', 'true');
await page.getByTestId('designs-view-kanban').click();
await expect(page.locator('.design-kanban-board')).toBeVisible();
await expect(page.locator('.design-grid')).toHaveCount(0);
await expect(page.getByTestId('designs-view-kanban')).toHaveAttribute('aria-pressed', 'true');
await expect(page.locator('.design-kanban-card', { hasText: projectName })).toBeVisible();
await page.reload();
await expect(page.getByTestId('new-project-panel')).toBeVisible();
await expect(page.locator('.design-kanban-board')).toBeVisible();
await expect(page.getByTestId('designs-view-kanban')).toHaveAttribute('aria-pressed', 'true');
await page.getByTestId('designs-view-grid').click();
await expect(page.locator('.design-grid')).toBeVisible();
await expect(homeDesignCard(page, projectName)).toBeVisible();
await expect(page.getByTestId('designs-view-grid')).toHaveAttribute('aria-pressed', 'true');
});
test('home designs search filters projects and recovers from no results', async ({ page }) => {
const stamp = Date.now();
const alphaName = `Home search alpha ${stamp}`;
const betaName = `Home search beta ${stamp}`;
await page.goto('/');
await createProject(page, alphaName);
await expectWorkspaceReady(page);
await page.getByRole('button', { name: /back to projects/i }).click();
await expect(page.getByTestId('new-project-panel')).toBeVisible();
await createProject(page, betaName);
await expectWorkspaceReady(page);
await page.getByRole('button', { name: /back to projects/i }).click();
await expect(page.getByTestId('new-project-panel')).toBeVisible();
await expect(homeDesignCard(page, alphaName)).toBeVisible();
await expect(homeDesignCard(page, betaName)).toBeVisible();
const search = page.locator('.tab-panel-toolbar .toolbar-search input');
await search.fill('alpha');
await expect(homeDesignCard(page, alphaName)).toBeVisible();
await expect(homeDesignCard(page, betaName)).toHaveCount(0);
await search.fill(`missing-${stamp}`);
await expect(homeDesignCard(page, alphaName)).toHaveCount(0);
await expect(homeDesignCard(page, betaName)).toHaveCount(0);
await expect(page.locator('.tab-empty')).toBeVisible();
await search.fill('');
await expect(homeDesignCard(page, alphaName)).toBeVisible();
await expect(homeDesignCard(page, betaName)).toBeVisible();
});
test('change pet opens pet settings and saves a custom companion', async ({ page }) => {
await seedAdoptedPet(page);
await page.route('**/api/codex-pets', async (route) => {
await route.fulfill({ json: { pets: [], rootDir: '' } });
});
await page.goto('/');
await expect(page.getByTestId('new-project-panel')).toBeVisible();
await page
.locator('.entry-side-foot')
.getByRole('button', { name: /change pet/i })
.click();
const dialog = page.getByRole('dialog');
await expect(dialog).toBeVisible();
await expect(dialog.getByRole('heading', { name: 'Pets' })).toBeVisible();
await dialog.getByRole('tab', { name: 'Custom' }).click();
const customPanel = dialog.locator('.pet-custom');
await expect(customPanel).toBeVisible();
await customPanel.getByLabel('Name').fill('QA Turtle');
await customPanel.getByLabel('Glyph').fill('🐢');
await customPanel.getByLabel('Greeting').fill('Shell yeah, tests are green.');
await expect(customPanel.getByRole('button', { name: /adopted/i })).toBeVisible();
await dialog.getByRole('button', { name: 'Save', exact: true }).click();
await expect(dialog).toHaveCount(0);
await expect(page.locator('.pet-overlay .pet-sprite')).toHaveAttribute(
'aria-label',
/QA Turtle/i,
);
const petConfig = await readPetConfig(page);
expect(petConfig).toMatchObject({
adopted: true,
enabled: true,
petId: 'custom',
custom: {
name: 'QA Turtle',
glyph: '🐢',
greeting: 'Shell yeah, tests are green.',
},
});
});
async function createProject(
page: Page,
projectName: string,
) {
await expect(page.getByTestId('new-project-panel')).toBeVisible();
await page.getByTestId('new-project-tab-prototype').click();
await page.getByTestId('new-project-name').fill(projectName);
await page.getByTestId('create-project').click();
}
async function expectWorkspaceReady(page: Page) {
await expect(page).toHaveURL(/\/projects\//);
await expect(page.getByTestId('chat-composer')).toBeVisible();
await expect(page.getByTestId('file-workspace')).toBeVisible();
await expect(page.getByText('Start a conversation')).toBeVisible();
}
async function renameProjectTitle(
page: Page,
title: Locator,
nextName: string,
) {
await title.click();
await page.keyboard.press('Meta+A');
const selected = await page.evaluate(() => window.getSelection()?.toString() ?? '');
if (selected.length === 0) {
await page.keyboard.press('Control+A');
}
await page.keyboard.type(nextName);
await page.keyboard.press('Enter');
}
async function uploadTinyPng(
page: Page,
name: string,
): Promise<string> {
const pngBytes = Buffer.from(
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO5W6McAAAAASUVORK5CYII=',
'base64',
);
await page.getByTestId('design-files-upload-input').setInputFiles({
name,
mimeType: 'image/png',
buffer: pngBytes,
});
await expect(tabBySuffix(page, name)).toBeVisible();
const { projectId } = getProjectContextFromUrl(page);
const files = await listProjectFiles(page, projectId);
const uploaded = files.find((file) => file.name.endsWith(name));
expect(uploaded?.name).toBeTruthy();
return uploaded!.name;
}
function tabBySuffix(page: Page, name: string): Locator {
return page.getByRole('tab', { name: new RegExp(`${escapeRegExp(name)}$`, 'i') });
}
function rowByFileName(page: Page, name: string): Locator {
return page.getByTestId(`design-file-row-${name}`);
}
function menuByFileName(page: Page, name: string): Locator {
return page.getByTestId(`design-file-menu-${name}`);
}
function homeDesignCard(page: Page, name: string): Locator {
return page.locator('.design-card', {
has: page.locator('.design-card-name', { hasText: name }),
});
}
async function seedAdoptedPet(page: Page) {
await page.addInitScript((key) => {
window.localStorage.setItem(
key,
JSON.stringify({
mode: 'daemon',
apiKey: '',
baseUrl: 'https://api.anthropic.com',
model: 'claude-sonnet-4-5',
agentId: 'mock',
skillId: null,
designSystemId: null,
onboardingCompleted: true,
agentModels: {},
pet: {
adopted: true,
enabled: true,
petId: 'custom',
custom: {
name: 'Original Buddy',
glyph: '🦄',
accent: '#c96442',
greeting: 'Ready to pair.',
},
},
}),
);
}, STORAGE_KEY);
}
async function readPetConfig(page: Page) {
return page.evaluate((key) => {
const raw = window.localStorage.getItem(key);
return raw ? JSON.parse(raw).pet : null;
}, STORAGE_KEY) as Promise<{
adopted: boolean;
enabled: boolean;
petId: string;
custom: {
name: string;
glyph: string;
greeting: string;
};
} | null>;
}
async function fetchCurrentProject(page: Page) {
const { projectId } = getProjectContextFromUrl(page);
const response = await page.request.get(`/api/projects/${projectId}`);
expect(response.ok()).toBeTruthy();
const body = (await response.json()) as {
project: {
name: string;
designSystemId: string | null;
metadata?: {
inspirationDesignSystemIds?: string[];
};
};
};
return body.project;
}
async function listProjectFiles(page: Page, projectId: string) {
const response = await page.request.get(`/api/projects/${projectId}/files`);
expect(response.ok()).toBeTruthy();
const body = (await response.json()) as { files: Array<{ name: string }> };
return body.files;
}
function getProjectContextFromUrl(page: Page) {
const url = new URL(page.url());
const [, projectId] = url.pathname.match(/\/projects\/([^/]+)/) ?? [];
if (!projectId) throw new Error(`unexpected project route: ${url.pathname}`);
return { projectId };
}
function escapeRegExp(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
function skillSummary(
id: string,
name: string,
mode: 'prototype' | 'deck' | 'image',
surface: 'web' | 'image',
defaultFor: string[],
) {
return {
id,
name,
description: `${name} for tab switching coverage.`,
triggers: [],
mode,
surface,
platform: 'desktop',
scenario: 'qa',
previewType: 'html',
designSystemRequired: mode !== 'image',
defaultFor,
upstream: null,
featured: null,
fidelity: null,
speakerNotes: null,
animations: null,
hasBody: true,
examplePrompt: '',
};
}

View File

@@ -0,0 +1,90 @@
import { expect, test } from '@playwright/test';
import type { Page } from '@playwright/test';
const STORAGE_KEY = 'open-design:config';
async function bootstrapWithLegacyConfig(
page: Page,
config: Record<string, unknown>,
) {
await page.addInitScript(
({ key, value }) => {
window.localStorage.setItem(key, JSON.stringify(value));
},
{ key: STORAGE_KEY, value: config },
);
await page.route('**/api/health', async (route) => {
await route.fulfill({ status: 503, body: 'offline' });
});
await page.goto('/');
await page.getByTitle('Configure execution mode').click();
await expect(page.getByRole('dialog')).toBeVisible();
}
test('legacy known OpenAI provider switches to the matching Anthropic preset', async ({ page }) => {
await bootstrapWithLegacyConfig(page, {
mode: 'api',
apiKey: 'sk-test',
baseUrl: 'https://api.deepseek.com',
model: 'deepseek-chat',
agentId: null,
skillId: null,
designSystemId: null,
onboardingCompleted: true,
mediaProviders: {},
agentModels: {},
});
const protocolTabs = page.getByRole('tablist', { name: 'API protocol' });
const openAiTab = protocolTabs.getByRole('tab', { name: 'OpenAI', exact: true });
const anthropicTab = protocolTabs.getByRole('tab', { name: 'Anthropic', exact: true });
const baseUrlInput = page.getByLabel('Base URL');
const modelSelect = page.getByLabel('Model');
await expect(openAiTab).toHaveAttribute('aria-selected', 'true');
await expect(page.getByRole('heading', { name: 'OpenAI API' })).toBeVisible();
await expect(baseUrlInput).toHaveValue('https://api.deepseek.com');
await expect(modelSelect).toHaveValue('deepseek-chat');
await anthropicTab.click();
await expect(anthropicTab).toHaveAttribute('aria-selected', 'true');
await expect(page.getByRole('heading', { name: 'Anthropic API' })).toBeVisible();
await expect(baseUrlInput).toHaveValue('https://api.deepseek.com/anthropic');
await expect(modelSelect).toHaveValue('deepseek-chat');
});
test('legacy custom provider preserves custom baseUrl and model when switching protocols', async ({ page }) => {
await bootstrapWithLegacyConfig(page, {
mode: 'api',
apiKey: 'sk-test',
baseUrl: 'https://my-proxy.example.com/v1',
model: 'my-custom-model',
agentId: null,
skillId: null,
designSystemId: null,
onboardingCompleted: true,
mediaProviders: {},
agentModels: {},
});
const protocolTabs = page.getByRole('tablist', { name: 'API protocol' });
const openAiTab = protocolTabs.getByRole('tab', { name: 'OpenAI', exact: true });
const anthropicTab = protocolTabs.getByRole('tab', { name: 'Anthropic', exact: true });
const baseUrlInput = page.getByLabel('Base URL');
const customModelInput = page.getByLabel(/Custom model id/i);
await expect(openAiTab).toHaveAttribute('aria-selected', 'true');
await expect(page.getByRole('heading', { name: 'OpenAI API' })).toBeVisible();
await expect(baseUrlInput).toHaveValue('https://my-proxy.example.com/v1');
await expect(customModelInput).toHaveValue('my-custom-model');
await anthropicTab.click();
await expect(anthropicTab).toHaveAttribute('aria-selected', 'true');
await expect(page.getByRole('heading', { name: 'Anthropic API' })).toBeVisible();
await expect(baseUrlInput).toHaveValue('https://my-proxy.example.com/v1');
await expect(customModelInput).toHaveValue('my-custom-model');
});

View File

@@ -0,0 +1,232 @@
import { expect, test } from '@playwright/test';
import type { Locator, Page } from '@playwright/test';
const STORAGE_KEY = 'open-design:config';
const CHAT_PANEL_WIDTH_STORAGE_KEY = 'open-design.project.chatPanelWidth';
test.beforeEach(async ({ page }) => {
await page.addInitScript((key) => {
window.localStorage.setItem(
key,
JSON.stringify({
mode: 'daemon',
apiKey: '',
baseUrl: 'https://api.anthropic.com',
model: 'claude-sonnet-4-5',
agentId: 'mock',
skillId: null,
designSystemId: null,
onboardingCompleted: true,
agentModels: {},
}),
);
}, STORAGE_KEY);
await page.route('**/api/agents', async (route) => {
await route.fulfill({
json: {
agents: [
{
id: 'mock',
name: 'Mock Agent',
bin: 'mock-agent',
available: true,
version: 'test',
models: [{ id: 'default', label: 'Default' }],
},
],
},
});
});
});
test('quick switcher opens from keyboard and activates the selected file', async ({ page }) => {
await page.goto('/');
await createProject(page, 'Quick switcher keyboard flow');
await expectWorkspaceReady(page);
await uploadTinyPng(page, 'alpha-file.png');
await uploadTinyPng(page, 'beta-file.png');
const alphaTab = tabBySuffix(page, 'alpha-file.png');
const betaTab = tabBySuffix(page, 'beta-file.png');
await expect(alphaTab).toBeVisible();
await expect(betaTab).toBeVisible();
await alphaTab.click();
await expect(alphaTab).toHaveAttribute('aria-selected', 'true');
await openQuickSwitcher(page);
const quickSwitcher = page.locator('.qs-overlay');
const quickSwitcherInput = page.locator('.qs-input');
await expect(quickSwitcher).toBeVisible();
await expect(quickSwitcherInput).toBeVisible();
await quickSwitcherInput.fill('beta');
await expect(page.getByRole('option', { name: /beta-file\.png/i })).toBeVisible();
await quickSwitcherInput.press('Enter');
await expect(quickSwitcher).toBeHidden();
await expect(betaTab).toHaveAttribute('aria-selected', 'true');
await expect(alphaTab).toHaveAttribute('aria-selected', 'false');
await openQuickSwitcher(page);
await expect(quickSwitcher).toBeVisible();
await quickSwitcherInput.press('Escape');
await expect(quickSwitcher).toBeHidden();
});
test('quick switcher keeps the current file when search has no matches', async ({ page }) => {
await page.goto('/');
await createProject(page, 'Quick switcher empty search flow');
await expectWorkspaceReady(page);
await uploadTinyPng(page, 'alpha-empty-search.png');
await uploadTinyPng(page, 'beta-empty-search.png');
const alphaTab = tabBySuffix(page, 'alpha-empty-search.png');
await expect(alphaTab).toBeVisible();
await alphaTab.click();
await expect(alphaTab).toHaveAttribute('aria-selected', 'true');
await openQuickSwitcher(page);
const quickSwitcher = page.locator('.qs-overlay');
const quickSwitcherInput = page.locator('.qs-input');
await expect(quickSwitcher).toBeVisible();
await quickSwitcherInput.fill('no-file-with-this-name');
await expect(page.locator('.qs-empty')).toBeVisible();
await expect(page.getByRole('option')).toHaveCount(0);
await quickSwitcherInput.press('Enter');
await expect(quickSwitcher).toBeVisible();
await quickSwitcherInput.press('Escape');
await expect(quickSwitcher).toBeHidden();
await expect(alphaTab).toHaveAttribute('aria-selected', 'true');
});
test('quick switcher arrow keys move selection before opening a file', async ({ page }) => {
await page.goto('/');
await createProject(page, 'Quick switcher arrow navigation flow');
await expectWorkspaceReady(page);
await uploadTinyPng(page, 'arrow-alpha.png');
await uploadTinyPng(page, 'arrow-beta.png');
await uploadTinyPng(page, 'arrow-gamma.png');
await openQuickSwitcher(page);
const quickSwitcher = page.locator('.qs-overlay');
const quickSwitcherInput = page.locator('.qs-input');
const selectedOption = page.getByRole('option', { selected: true });
await expect(quickSwitcher).toBeVisible();
await expect(page.getByRole('option')).toHaveCount(3);
const initialSelection = await selectedOption.textContent();
await quickSwitcherInput.press('ArrowDown');
const nextSelection = await selectedOption.textContent();
expect(nextSelection).not.toBe(initialSelection);
await quickSwitcherInput.press('Enter');
await expect(quickSwitcher).toBeHidden();
const selectedFileName = selectedBaseName(nextSelection);
await expect(tabBySuffix(page, selectedFileName)).toHaveAttribute('aria-selected', 'true');
});
test('keyboard chat panel resize persists after reload', async ({ page }) => {
await page.goto('/');
await createProject(page, 'Chat panel resize persistence');
await expectWorkspaceReady(page);
await page.evaluate((key) => {
window.localStorage.removeItem(key);
}, CHAT_PANEL_WIDTH_STORAGE_KEY);
await page.reload();
await expectWorkspaceReady(page);
const handle = page.locator('.split-resize-handle');
await expect(handle).toBeVisible();
const initialWidth = await readChatPanelWidth(handle);
await handle.focus();
await page.keyboard.press('End');
let resizedWidth = await readChatPanelWidth(handle);
if (resizedWidth === initialWidth) {
await page.keyboard.press('Home');
resizedWidth = await readChatPanelWidth(handle);
}
expect(resizedWidth).not.toBe(initialWidth);
const savedWidth = await page.evaluate(
(key) => window.localStorage.getItem(key),
CHAT_PANEL_WIDTH_STORAGE_KEY,
);
expect(savedWidth).toBe(String(resizedWidth));
await page.reload();
await expectWorkspaceReady(page);
const restoredWidth = await readChatPanelWidth(handle);
expect(restoredWidth).toBe(resizedWidth);
});
async function createProject(
page: Page,
projectName: string,
) {
await expect(page.getByTestId('new-project-panel')).toBeVisible();
await page.getByTestId('new-project-tab-prototype').click();
await page.getByTestId('new-project-name').fill(projectName);
await page.getByTestId('create-project').click();
}
async function expectWorkspaceReady(page: Page) {
await expect(page).toHaveURL(/\/projects\//);
await expect(page.getByTestId('chat-composer')).toBeVisible();
await expect(page.getByTestId('file-workspace')).toBeVisible();
await expect(page.getByText('Start a conversation')).toBeVisible();
}
async function uploadTinyPng(
page: Page,
name: string,
) {
const pngBytes = Buffer.from(
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO5W6McAAAAASUVORK5CYII=',
'base64',
);
await page.getByTestId('design-files-upload-input').setInputFiles({
name,
mimeType: 'image/png',
buffer: pngBytes,
});
await expect(tabBySuffix(page, name)).toBeVisible();
}
async function readChatPanelWidth(handle: Locator): Promise<number> {
const raw = await handle.getAttribute('aria-valuenow');
const parsed = Number.parseInt(raw ?? '', 10);
expect(Number.isFinite(parsed)).toBeTruthy();
return parsed;
}
async function openQuickSwitcher(page: Page) {
const quickSwitcher = page.locator('.qs-overlay');
await page.keyboard.press('Meta+P');
if (await quickSwitcher.isVisible()) return;
await page.keyboard.press('Control+P');
await expect(quickSwitcher).toBeVisible();
}
function tabBySuffix(page: Page, name: string): Locator {
return page.getByRole('tab', { name: new RegExp(`${escapeRegExp(name)}$`, 'i') });
}
function selectedBaseName(selectionText: string | null): string {
const normalized = selectionText?.replace(/\s+/g, ' ').trim() ?? '';
const match = normalized.match(/arrow-(alpha|beta|gamma)\.png/i);
expect(match?.[0]).toBeTruthy();
return match![0];
}
function escapeRegExp(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}