Skip to content

Commit 32b27e8

Browse files
feat: redesign setup modal with toolkit tabs and agent frameworks (#1506)
* feat: redesign setup modal with toolkit tabs, agent frameworks, and syntax highlighting - Add top-level Agents/Toolkits segmented tabs - Reorganize code snippets as toolkit-first sub-tabs (OpenAI SDK, Vercel AI SDK, LangChain, cURL) - Add Python/TypeScript language toggle for OpenAI SDK tab - Add Model field (auto) to connection details - Add agent framework tabs: OpenClaw (with CLI/wizard sub-tabs) and Hermes Agent (coming soon) - Add OpenClaw interactive wizard with onboard fields and CLI configuration - Add syntax highlighting via highlight.js (Python, TypeScript, Bash) - Add eye toggle + disabled copy button when API key is masked - Add tab/toolkit logos (OpenAI, Vercel, LangChain, Python, TypeScript, OpenClaw, Hermes) - Add ARIA roles (tablist, tab, tabpanel) for accessibility - Add CopyButton disabled prop with visual feedback * fix: disable API key copy button when key is masked in FrameworkSnippets * feat: add Python language toggle for Vercel AI SDK tab * feat: add agent type system with category/platform selection and platform-filtered setup - Add agent_category (personal/app) and agent_platform columns to agents table - Migration backfills existing agents to personal/openclaw - Update CreateAgentDto and RenameAgentDto to accept category/platform - Add updateAgentType method to AgentLifecycleService - Add AgentTypePicker component (reusable in create modal + settings) - Add OpenClawSetup and HermesSetup dedicated setup components - Add platform-filtered wizard in SetupStepAddProvider (reactive Show/Switch) - Merge Settings tabs into single page with name + type + API key + setup - Show platform icon in agent cards and Overview header - Add LangChain (blue pinwheel), Other (3D box) icons - Add YAML syntax highlighting for Hermes config - Replace Total cost with Tokens in agent cards - Fix SolidJS reactivity in SetupStepAddProvider (use Show/Switch not if/return) - Fix CopyButton disabled reactivity (check inside handler) - Add eye toggle + disabled copy to all code blocks with API keys - Add "Change agent type" link in setup that scrolls to type picker - Accessibility: visually-hidden radio inputs, focus-within rings, ARIA roles * fix: address review feedback — clear stale platform, fix save validation * feat: redesign setup modal UX — tab-style type selection, platform-specific instructions, Settings change modal (#1522) - Replace category radio buttons with panel tab style in AgentTypePicker - Default to Personal AI Agent / OpenClaw on create; auto-select first platform on tab switch - Show platform-specific setup instructions (OpenClaw CLI, Hermes config, SDK snippets, cURL) - Fix FrameworkSnippets reactivity — activeTab now syncs when defaultToolkit prop changes - Redesign Settings agent type section: read-only display + Change button opens modal - After saving type change, SetupModal opens automatically with new instructions - Add platform icon to header breadcrumb - Update syntax highlighting: softer string colors, blue instead of green - Darken muted-foreground text color for better readability - Remove border-radius from platform icons, invert OpenAI/Vercel logos in dark mode - Update all related tests (AgentTypePicker, FrameworkSnippets, SetupStepAddProvider, Settings, etc.) * chore: regenerate plugin bundle after merging main * fix: handle undefined platform fallback in category change handlers * fix: remove regex HTML stripping flagged by CodeQL Check highlighted output directly instead of stripping tags with a regex that CodeQL flags as incomplete multi-character sanitization. --------- Co-authored-by: Sébastien Conejo <sebastien.conejo@buddyweb.fr> Co-authored-by: Sébastien Conejo <sebastien@buddyweb.fr>
1 parent 6caf204 commit 32b27e8

File tree

126 files changed

+4886
-1038
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

126 files changed

+4886
-1038
lines changed

.changeset/setup-modal-redesign.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"manifest": minor
3+
---
4+
5+
Redesign setup modal UI with tab-style agent type selection, platform-specific setup instructions, and Settings page agent type change modal

package-lock.json

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/backend/src/analytics/controllers/agents.controller.spec.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,11 @@ describe('AgentsController', () => {
5858
},
5959
{
6060
provide: AgentLifecycleService,
61-
useValue: { deleteAgent: mockDeleteAgent, renameAgent: mockRenameAgent },
61+
useValue: {
62+
deleteAgent: mockDeleteAgent,
63+
renameAgent: mockRenameAgent,
64+
updateAgentType: jest.fn(),
65+
},
6266
},
6367
{
6468
provide: ApiKeyGeneratorService,
@@ -183,7 +187,7 @@ describe('AgentsController', () => {
183187

184188
it('renames agent and returns success with slug', async () => {
185189
const user = { id: 'u1' };
186-
const result = await controller.renameAgent(user as never, 'bot-1', {
190+
const result = await controller.updateAgent(user as never, 'bot-1', {
187191
name: 'Bot Renamed',
188192
} as never);
189193

@@ -195,7 +199,7 @@ describe('AgentsController', () => {
195199
it('rejects rename with empty slug', async () => {
196200
const user = { id: 'u1' };
197201
await expect(
198-
controller.renameAgent(user as never, 'bot-1', { name: '!!!' } as never),
202+
controller.updateAgent(user as never, 'bot-1', { name: '!!!' } as never),
199203
).rejects.toThrow(BadRequestException);
200204
});
201205

@@ -242,7 +246,7 @@ describe('AgentsController', () => {
242246
{ provide: TimeseriesQueriesService, useValue: { getAgentList: jest.fn() } },
243247
{
244248
provide: AgentLifecycleService,
245-
useValue: { deleteAgent: jest.fn(), renameAgent: jest.fn() },
249+
useValue: { deleteAgent: jest.fn(), renameAgent: jest.fn(), updateAgentType: jest.fn() },
246250
},
247251
{
248252
provide: ApiKeyGeneratorService,
@@ -272,7 +276,7 @@ describe('AgentsController', () => {
272276
{ provide: TimeseriesQueriesService, useValue: { getAgentList: jest.fn() } },
273277
{
274278
provide: AgentLifecycleService,
275-
useValue: { deleteAgent: jest.fn(), renameAgent: jest.fn() },
279+
useValue: { deleteAgent: jest.fn(), renameAgent: jest.fn(), updateAgentType: jest.fn() },
276280
},
277281
{
278282
provide: ApiKeyGeneratorService,
@@ -301,7 +305,7 @@ describe('AgentsController', () => {
301305
{ provide: TimeseriesQueriesService, useValue: { getAgentList: jest.fn() } },
302306
{
303307
provide: AgentLifecycleService,
304-
useValue: { deleteAgent: jest.fn(), renameAgent: jest.fn() },
308+
useValue: { deleteAgent: jest.fn(), renameAgent: jest.fn(), updateAgentType: jest.fn() },
305309
},
306310
{
307311
provide: ApiKeyGeneratorService,
@@ -329,7 +333,7 @@ describe('AgentsController', () => {
329333
{ provide: TimeseriesQueriesService, useValue: { getAgentList: jest.fn() } },
330334
{
331335
provide: AgentLifecycleService,
332-
useValue: { deleteAgent: jest.fn(), renameAgent: jest.fn() },
336+
useValue: { deleteAgent: jest.fn(), renameAgent: jest.fn(), updateAgentType: jest.fn() },
333337
},
334338
{
335339
provide: ApiKeyGeneratorService,

packages/backend/src/analytics/controllers/agents.controller.ts

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ export class AgentsController {
6767
agentName: slug,
6868
displayName,
6969
email: user.email,
70+
agentCategory: body.agent_category,
71+
agentPlatform: body.agent_platform,
7072
});
7173
} catch (error) {
7274
if (error instanceof QueryFailedError && /unique|duplicate/i.test(error.message)) {
@@ -76,7 +78,13 @@ export class AgentsController {
7678
}
7779
await this.cacheManager.del(this.agentListCacheKey(user.id));
7880
return {
79-
agent: { id: result.agentId, name: slug, display_name: displayName },
81+
agent: {
82+
id: result.agentId,
83+
name: slug,
84+
display_name: displayName,
85+
agent_category: body.agent_category ?? null,
86+
agent_platform: body.agent_platform ?? null,
87+
},
8088
apiKey: result.apiKey,
8189
};
8290
}
@@ -102,19 +110,34 @@ export class AgentsController {
102110
}
103111

104112
@Patch('agents/:agentName')
105-
async renameAgent(
113+
async updateAgent(
106114
@CurrentUser() user: AuthUser,
107115
@Param('agentName') agentName: string,
108116
@Body() body: RenameAgentDto,
109117
) {
110-
const slug = slugify(body.name);
111-
if (!slug) {
112-
throw new BadRequestException('Agent name produces an empty slug');
118+
const result: Record<string, unknown> = {};
119+
120+
if (body.name) {
121+
const slug = slugify(body.name);
122+
if (!slug) throw new BadRequestException('Agent name produces an empty slug');
123+
const displayName = body.name.trim();
124+
await this.lifecycle.renameAgent(user.id, agentName, slug, displayName);
125+
result['renamed'] = true;
126+
result['name'] = slug;
127+
result['display_name'] = displayName;
113128
}
114-
const displayName = body.name.trim();
115-
await this.lifecycle.renameAgent(user.id, agentName, slug, displayName);
129+
130+
if (body.agent_category !== undefined || body.agent_platform !== undefined) {
131+
await this.lifecycle.updateAgentType(user.id, body.name ? slugify(body.name)! : agentName, {
132+
agent_category: body.agent_category,
133+
agent_platform: body.agent_platform,
134+
});
135+
if (body.agent_category !== undefined) result['agent_category'] = body.agent_category;
136+
if (body.agent_platform !== undefined) result['agent_platform'] = body.agent_platform;
137+
}
138+
116139
await this.cacheManager.del(this.agentListCacheKey(user.id));
117-
return { renamed: true, name: slug, display_name: displayName };
140+
return result;
118141
}
119142

120143
@Delete('agents/:agentName')

packages/backend/src/analytics/services/agent-lifecycle.service.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,36 @@ export class AgentLifecycleService {
2525
await this.agentRepo.delete(agent.id);
2626
}
2727

28+
private async findAgentByUser(userId: string, agentName: string) {
29+
return this.agentRepo
30+
.createQueryBuilder('a')
31+
.leftJoin('a.tenant', 't')
32+
.where('t.name = :userId', { userId })
33+
.andWhere('a.name = :agentName', { agentName })
34+
.getOne();
35+
}
36+
37+
async updateAgentType(
38+
userId: string,
39+
agentName: string,
40+
fields: { agent_category?: string; agent_platform?: string },
41+
): Promise<void> {
42+
const agent = await this.findAgentByUser(userId, agentName);
43+
if (!agent) throw new NotFoundException(`Agent "${agentName}" not found`);
44+
45+
const update: Record<string, unknown> = {};
46+
if (fields.agent_category !== undefined) update['agent_category'] = fields.agent_category;
47+
if (fields.agent_platform !== undefined) update['agent_platform'] = fields.agent_platform;
48+
if (Object.keys(update).length === 0) return;
49+
50+
await this.agentRepo
51+
.createQueryBuilder()
52+
.update('agents')
53+
.set(update)
54+
.where('id = :id', { id: agent.id })
55+
.execute();
56+
}
57+
2858
async renameAgent(
2959
userId: string,
3060
currentName: string,

packages/backend/src/analytics/services/timeseries-queries.service.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,8 @@ export class TimeseriesQueriesService {
250250
return {
251251
agent_name: name,
252252
display_name: a.display_name ?? name,
253+
agent_category: a.agent_category ?? null,
254+
agent_platform: a.agent_platform ?? null,
253255
message_count: Number(stats?.['message_count'] ?? 0),
254256
last_active: String(stats?.['last_active'] ?? a.created_at ?? ''),
255257
total_cost: Number(stats?.['total_cost'] ?? 0),
Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,31 @@
1-
import { IsString, IsNotEmpty, MinLength, MaxLength, Matches } from 'class-validator';
1+
import {
2+
IsString,
3+
IsNotEmpty,
4+
MinLength,
5+
MaxLength,
6+
Matches,
7+
IsOptional,
8+
IsIn,
9+
} from 'class-validator';
10+
import { AGENT_CATEGORIES, AGENT_PLATFORMS } from 'manifest-shared';
211

312
export class CreateAgentDto {
413
@IsString()
514
@IsNotEmpty()
615
@MinLength(1)
716
@MaxLength(100)
8-
@Matches(/^[a-zA-Z0-9 _-]+$/, { message: 'Agent name must contain only letters, numbers, spaces, dashes, and underscores' })
17+
@Matches(/^[a-zA-Z0-9 _-]+$/, {
18+
message: 'Agent name must contain only letters, numbers, spaces, dashes, and underscores',
19+
})
920
name!: string;
21+
22+
@IsOptional()
23+
@IsString()
24+
@IsIn([...AGENT_CATEGORIES])
25+
agent_category?: string;
26+
27+
@IsOptional()
28+
@IsString()
29+
@IsIn([...AGENT_PLATFORMS])
30+
agent_platform?: string;
1031
}

packages/backend/src/common/dto/rename-agent.dto.spec.ts

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,10 @@ describe('RenameAgentDto', () => {
1818
expect(errors.length).toBeGreaterThan(0);
1919
});
2020

21-
it('rejects missing name', async () => {
21+
it('allows missing name (optional)', async () => {
2222
const dto = plainToInstance(RenameAgentDto, {});
2323
const errors = await validate(dto);
24-
expect(errors.length).toBeGreaterThan(0);
24+
expect(errors).toHaveLength(0);
2525
});
2626

2727
it('accepts names with spaces', async () => {
@@ -41,4 +41,28 @@ describe('RenameAgentDto', () => {
4141
const errors = await validate(dto);
4242
expect(errors.length).toBeGreaterThan(0);
4343
});
44+
45+
it('accepts valid agent_category', async () => {
46+
const dto = plainToInstance(RenameAgentDto, { agent_category: 'personal' });
47+
const errors = await validate(dto);
48+
expect(errors).toHaveLength(0);
49+
});
50+
51+
it('rejects invalid agent_category', async () => {
52+
const dto = plainToInstance(RenameAgentDto, { agent_category: 'invalid' });
53+
const errors = await validate(dto);
54+
expect(errors.length).toBeGreaterThan(0);
55+
});
56+
57+
it('accepts valid agent_platform', async () => {
58+
const dto = plainToInstance(RenameAgentDto, { agent_platform: 'openclaw' });
59+
const errors = await validate(dto);
60+
expect(errors).toHaveLength(0);
61+
});
62+
63+
it('rejects invalid agent_platform', async () => {
64+
const dto = plainToInstance(RenameAgentDto, { agent_platform: 'invalid' });
65+
const errors = await validate(dto);
66+
expect(errors.length).toBeGreaterThan(0);
67+
});
4468
});
Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,32 @@
1-
import { IsString, IsNotEmpty, MinLength, MaxLength, Matches } from 'class-validator';
1+
import {
2+
IsString,
3+
IsNotEmpty,
4+
MinLength,
5+
MaxLength,
6+
Matches,
7+
IsOptional,
8+
IsIn,
9+
} from 'class-validator';
10+
import { AGENT_CATEGORIES, AGENT_PLATFORMS } from 'manifest-shared';
211

312
export class RenameAgentDto {
13+
@IsOptional()
414
@IsString()
515
@IsNotEmpty()
616
@MinLength(1)
717
@MaxLength(100)
8-
@Matches(/^[a-zA-Z0-9 _-]+$/, { message: 'Agent name must contain only letters, numbers, spaces, dashes, and underscores' })
9-
name!: string;
18+
@Matches(/^[a-zA-Z0-9 _-]+$/, {
19+
message: 'Agent name must contain only letters, numbers, spaces, dashes, and underscores',
20+
})
21+
name?: string;
22+
23+
@IsOptional()
24+
@IsString()
25+
@IsIn([...AGENT_CATEGORIES])
26+
agent_category?: string;
27+
28+
@IsOptional()
29+
@IsString()
30+
@IsIn([...AGENT_PLATFORMS])
31+
agent_platform?: string;
1032
}

packages/backend/src/database/database-seeder.service.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,8 @@ export class DatabaseSeederService implements OnModuleInit {
133133
id: SEED_AGENT_ID,
134134
name: 'demo-agent',
135135
description: 'Default development agent',
136+
agent_category: 'personal',
137+
agent_platform: 'openclaw',
136138
is_active: true,
137139
tenant_id: SEED_TENANT_ID,
138140
});

0 commit comments

Comments
 (0)