Skip to content

Commit 4d43545

Browse files
barckcodeclaude
andcommitted
feat: structured subagent configuration with frontmatter format (#52)
Separate sub_agent_description (short one-liner) from sub_agent_instructions (detailed markdown body) in the subagent form. - Add sub_agent_instructions field to Agent, CreateAgentInput, CreateAgentRequest, UpdateAgentRequest interfaces - Restructure Step 2 form: Description as single-line input, Instructions as auto-growing textarea with Markdown support - Update generateSubAgentPreview to show instructions as body after frontmatter - Include sub_agent_instructions in JSON Preview (Step 3) and handleCreate payload - Add 3 new tests for preview generation with instructions - Bump version to 0.3.7 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 3c24b07 commit 4d43545

File tree

4 files changed

+120
-9
lines changed

4 files changed

+120
-9
lines changed

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.3.6
1+
0.3.7

src/pages/TeamBuilderPage.test.tsx

Lines changed: 92 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -296,7 +296,7 @@ describe('TeamBuilderPage', () => {
296296
await userEvent.click(screen.getByText('+ Add Sub-Agent'));
297297

298298
// Sub-agent should show structured fields
299-
expect(screen.getByPlaceholderText('What does this sub-agent do? The leader uses this to decide when to invoke it.')).toBeInTheDocument();
299+
expect(screen.getByPlaceholderText('Short one-liner: what does this sub-agent do?')).toBeInTheDocument();
300300
// Both leader and sub-agent have repo/skill inputs now
301301
const repoInputs = screen.getAllByPlaceholderText('https://github.com/owner/repo');
302302
expect(repoInputs.length).toBe(2); // leader + sub-agent
@@ -369,7 +369,7 @@ describe('TeamBuilderPage', () => {
369369
const allNameInputs = screen.getAllByPlaceholderText('Agent name');
370370
await userEvent.type(allNameInputs[1], 'worker-1');
371371
await userEvent.type(
372-
screen.getByPlaceholderText('What does this sub-agent do? The leader uses this to decide when to invoke it.'),
372+
screen.getByPlaceholderText('Short one-liner: what does this sub-agent do?'),
373373
'Handles backend API tasks',
374374
);
375375
// Use the sub-agent's repo/skill inputs (index 1, leader is index 0)
@@ -447,7 +447,7 @@ describe('TeamBuilderPage', () => {
447447
const allNameInputs = screen.getAllByPlaceholderText('Agent name');
448448
await userEvent.type(allNameInputs[1], 'worker-1');
449449
await userEvent.type(
450-
screen.getByPlaceholderText('What does this sub-agent do? The leader uses this to decide when to invoke it.'),
450+
screen.getByPlaceholderText('Short one-liner: what does this sub-agent do?'),
451451
'A sub-agent',
452452
);
453453
// Leave model as inherit and permission as default
@@ -494,7 +494,7 @@ describe('TeamBuilderPage', () => {
494494
const allNameInputs = screen.getAllByPlaceholderText('Agent name');
495495
await userEvent.type(allNameInputs[1], 'my-worker');
496496
await userEvent.type(
497-
screen.getByPlaceholderText('What does this sub-agent do? The leader uses this to decide when to invoke it.'),
497+
screen.getByPlaceholderText('Short one-liner: what does this sub-agent do?'),
498498
'Builds frontend components',
499499
);
500500
// Use sub-agent inputs (index 1, leader is index 0)
@@ -531,6 +531,94 @@ describe('TeamBuilderPage', () => {
531531
expect(preview.textContent).toContain(' - skill_name: write');
532532
});
533533

534+
it('shows sub-agent instructions as body after frontmatter in preview', async () => {
535+
renderPage();
536+
await userEvent.type(screen.getByPlaceholderText('My Agent Team'), 'my-team');
537+
await userEvent.click(screen.getByText('Next'));
538+
539+
const nameInputs = screen.getAllByPlaceholderText('Agent name');
540+
await userEvent.type(nameInputs[0], 'leader');
541+
542+
await userEvent.click(screen.getByText('+ Add Sub-Agent'));
543+
const allNameInputs = screen.getAllByPlaceholderText('Agent name');
544+
await userEvent.type(allNameInputs[1], 'my-worker');
545+
await userEvent.type(
546+
screen.getByPlaceholderText('Short one-liner: what does this sub-agent do?'),
547+
'Runs tests',
548+
);
549+
await userEvent.type(
550+
screen.getByPlaceholderText('Detailed instructions for the sub-agent (supports Markdown)'),
551+
'You must run all unit tests before reporting results.',
552+
);
553+
554+
await userEvent.click(screen.getByText('Next'));
555+
556+
const preview = screen.getByTestId('sub-agent-preview-my-worker');
557+
const text = preview.textContent || '';
558+
// Instructions should appear as body after the closing ---
559+
const lastFrontmatterClose = text.lastIndexOf('---');
560+
const instructionsText = text.slice(lastFrontmatterClose + 3);
561+
expect(instructionsText).toContain('You must run all unit tests before reporting results.');
562+
// Description stays in frontmatter
563+
expect(text).toContain('description: Runs tests');
564+
});
565+
566+
it('omits instructions body from preview when instructions is empty', async () => {
567+
renderPage();
568+
await userEvent.type(screen.getByPlaceholderText('My Agent Team'), 'my-team');
569+
await userEvent.click(screen.getByText('Next'));
570+
571+
const nameInputs = screen.getAllByPlaceholderText('Agent name');
572+
await userEvent.type(nameInputs[0], 'leader');
573+
574+
await userEvent.click(screen.getByText('+ Add Sub-Agent'));
575+
const allNameInputs = screen.getAllByPlaceholderText('Agent name');
576+
await userEvent.type(allNameInputs[1], 'my-worker');
577+
await userEvent.type(
578+
screen.getByPlaceholderText('Short one-liner: what does this sub-agent do?'),
579+
'Runs tests',
580+
);
581+
// Leave instructions empty
582+
583+
await userEvent.click(screen.getByText('Next'));
584+
585+
const preview = screen.getByTestId('sub-agent-preview-my-worker');
586+
const text = preview.textContent || '';
587+
// Should end with the closing --- (no body content after it)
588+
const parts = text.split('---');
589+
// frontmatter has opening and closing ---, so parts[2] (after second ---) should be empty or not exist
590+
const afterFrontmatter = (parts[2] || '').trim();
591+
expect(afterFrontmatter).toBe('');
592+
});
593+
594+
it('includes sub_agent_instructions in JSON preview for workers', async () => {
595+
renderPage();
596+
await userEvent.type(screen.getByPlaceholderText('My Agent Team'), 'my-team');
597+
await userEvent.click(screen.getByText('Next'));
598+
599+
const nameInputs = screen.getAllByPlaceholderText('Agent name');
600+
await userEvent.type(nameInputs[0], 'leader');
601+
602+
await userEvent.click(screen.getByText('+ Add Sub-Agent'));
603+
const allNameInputs = screen.getAllByPlaceholderText('Agent name');
604+
await userEvent.type(allNameInputs[1], 'my-worker');
605+
await userEvent.type(
606+
screen.getByPlaceholderText('Short one-liner: what does this sub-agent do?'),
607+
'Runs tests',
608+
);
609+
await userEvent.type(
610+
screen.getByPlaceholderText('Detailed instructions for the sub-agent (supports Markdown)'),
611+
'Run pytest with coverage.',
612+
);
613+
614+
await userEvent.click(screen.getByText('Next'));
615+
616+
// The JSON preview should contain sub_agent_instructions
617+
const pre = screen.getByText(/\"name\": \"my-team\"/);
618+
expect(pre.textContent).toContain('sub_agent_instructions');
619+
expect(pre.textContent).toContain('Run pytest with coverage.');
620+
});
621+
534622
it('shows JSON preview in step 3 with instructions_md field', async () => {
535623
renderPage();
536624
await userEvent.type(screen.getByPlaceholderText('My Agent Team'), 'my-team');

src/pages/TeamBuilderPage.tsx

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ interface AgentDraft {
1111
name: string;
1212
instructions_md: string;
1313
sub_agent_description: string;
14+
sub_agent_instructions: string;
1415
sub_agent_skills: SkillConfig[];
1516
sub_agent_model: string;
1617
}
@@ -69,6 +70,10 @@ function generateSubAgentPreview(agent: AgentDraft): string {
6970
});
7071
}
7172
lines.push('---');
73+
if (agent.sub_agent_instructions.trim()) {
74+
lines.push('');
75+
lines.push(agent.sub_agent_instructions.trim());
76+
}
7277
return lines.join('\n');
7378
}
7479

@@ -95,6 +100,7 @@ export function TeamBuilderPage() {
95100
name: '',
96101
instructions_md: defaultInstructionsMd(''),
97102
sub_agent_description: '',
103+
sub_agent_instructions: '',
98104
sub_agent_skills: [],
99105
sub_agent_model: 'inherit',
100106
},
@@ -148,6 +154,7 @@ export function TeamBuilderPage() {
148154
name: '',
149155
instructions_md: '',
150156
sub_agent_description: '',
157+
sub_agent_instructions: '',
151158
sub_agent_skills: [],
152159
sub_agent_model: 'inherit',
153160
}]);
@@ -462,6 +469,7 @@ export function TeamBuilderPage() {
462469
name: a.name.trim(),
463470
role: 'worker' as const,
464471
sub_agent_description: a.sub_agent_description.trim() || undefined,
472+
sub_agent_instructions: a.sub_agent_instructions.trim() || undefined,
465473
sub_agent_skills: a.sub_agent_skills.length > 0 ? a.sub_agent_skills : undefined,
466474
sub_agent_model: a.sub_agent_model !== 'inherit' ? a.sub_agent_model : undefined,
467475
};
@@ -765,13 +773,23 @@ export function TeamBuilderPage() {
765773
<div className="mt-3 space-y-3">
766774
<div>
767775
<label className="mb-1 block text-xs text-slate-400">Description *</label>
768-
<textarea
776+
<input
777+
type="text"
769778
value={agent.sub_agent_description}
770779
onChange={(e) => updateAgent(i, 'sub_agent_description', e.target.value)}
780+
className="w-full rounded border border-slate-600 bg-slate-900 px-2.5 py-1.5 text-sm text-white placeholder-slate-500 focus:border-blue-500 focus:outline-none"
781+
placeholder="Short one-liner: what does this sub-agent do?"
782+
/>
783+
</div>
784+
<div>
785+
<label className="mb-1 block text-xs text-slate-400">Instructions</label>
786+
<textarea
787+
value={agent.sub_agent_instructions}
788+
onChange={(e) => updateAgent(i, 'sub_agent_instructions', e.target.value)}
771789
onInput={autoGrow}
772-
rows={2}
773-
className="min-h-[80px] max-h-[400px] w-full resize-none overflow-y-auto rounded border border-slate-600 bg-slate-900 px-2.5 py-1.5 text-sm text-white placeholder-slate-500 focus:border-blue-500 focus:outline-none"
774-
placeholder="What does this sub-agent do? The leader uses this to decide when to invoke it."
790+
rows={6}
791+
className="min-h-[120px] max-h-[400px] w-full resize-none overflow-y-auto rounded border border-slate-600 bg-slate-900 px-2.5 py-1.5 text-sm text-white placeholder-slate-500 focus:border-blue-500 focus:outline-none"
792+
placeholder="Detailed instructions for the sub-agent (supports Markdown)"
775793
/>
776794
</div>
777795
<div>
@@ -1198,6 +1216,7 @@ export function TeamBuilderPage() {
11981216
name: a.name,
11991217
role: 'worker',
12001218
sub_agent_description: a.sub_agent_description || undefined,
1219+
sub_agent_instructions: a.sub_agent_instructions.trim() || undefined,
12011220
sub_agent_skills: a.sub_agent_skills.length > 0 ? a.sub_agent_skills : undefined,
12021221
sub_agent_model: a.sub_agent_model !== 'inherit' ? a.sub_agent_model : undefined,
12031222
};

src/types/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,7 @@ export interface Agent {
180180
container_id: string;
181181
container_status: ContainerStatus;
182182
sub_agent_description?: string;
183+
sub_agent_instructions?: string;
183184
sub_agent_skills?: SkillConfig[];
184185
sub_agent_model?: string;
185186
created_at: string;
@@ -231,6 +232,7 @@ export interface CreateAgentInput {
231232
role?: 'leader' | 'worker';
232233
instructions_md?: string;
233234
sub_agent_description?: string;
235+
sub_agent_instructions?: string;
234236
sub_agent_skills?: SkillConfig[];
235237
sub_agent_model?: string;
236238
}
@@ -240,6 +242,7 @@ export interface CreateAgentRequest {
240242
role?: 'leader' | 'worker';
241243
instructions_md?: string;
242244
sub_agent_description?: string;
245+
sub_agent_instructions?: string;
243246
sub_agent_skills?: SkillConfig[];
244247
sub_agent_model?: string;
245248
}
@@ -249,6 +252,7 @@ export interface UpdateAgentRequest {
249252
role?: 'leader' | 'worker';
250253
instructions_md?: string;
251254
sub_agent_description?: string;
255+
sub_agent_instructions?: string;
252256
sub_agent_skills?: SkillConfig[];
253257
sub_agent_model?: string;
254258
}

0 commit comments

Comments
 (0)