Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions apps/mcp-server/src/keyword/keyword.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -463,6 +463,12 @@ export interface ParseModeResult {
availableStrategies?: string[];
/** @apiProperty External API - do not rename. Hint for installing TaskMaestro when not available */
taskmaestroInstallHint?: string;
/**
* @apiProperty External API - do not rename.
* Visual data for agent visualization (banner, agent faces, collaboration config).
* When present, AI clients can render agent characters and collaboration format.
*/
visual?: VisualData;
}

/**
Expand Down Expand Up @@ -495,6 +501,48 @@ export interface DispatchReady {
parallelAgents?: DispatchReadyAgent[];
}

// ============================================================================
// Visual Data Types (for parse_mode response agent visualization)
// ============================================================================

/** Raw visual data from agent JSON files */
export interface AgentVisualRaw {
eye?: string;
eyeFallback?: string;
colorAnsi?: string;
group?: string;
}

/** Visual information for an agent in parse_mode response */
export interface AgentVisualInfo {
/** Agent display name */
name: string;
/** Face expression using eye symbols (e.g., "⬡‿⬡") */
face: string;
/** Display color name */
color: string;
/** Agent status in current mode */
status: 'analyzing' | 'waiting' | 'active';
}

/** Collaboration display configuration */
export interface CollaborationConfig {
/** Display format: "minimal" (eco) or "discussion" (full) */
format: 'minimal' | 'discussion';
/** Render hint for AI clients */
renderHint: string;
}

/** Visual data included in parse_mode response for agent visualization */
export interface VisualData {
/** ASCII art banner with mode character */
banner: string;
/** Agent visual information */
agents: AgentVisualInfo[];
/** Collaboration display configuration */
collaboration: CollaborationConfig;
}

export interface ModeConfig {
description: string;
instructions: string;
Expand Down
172 changes: 172 additions & 0 deletions apps/mcp-server/src/keyword/visual-data.builder.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import {
buildFace,
mapColor,
buildBanner,
buildCollaboration,
buildVisualData,
} from './visual-data.builder';

describe('buildFace', () => {
it('creates face from eye symbol', () => {
expect(buildFace('◇')).toBe('◇‿◇');
});

it('works with complex unicode symbols', () => {
expect(buildFace('⬡')).toBe('⬡‿⬡');
expect(buildFace('✦')).toBe('✦‿✦');
expect(buildFace('◆')).toBe('◆‿◆');
});

it('works with simple ASCII', () => {
expect(buildFace('O')).toBe('O‿O');
});
});

describe('mapColor', () => {
it('maps "bright" to "magenta"', () => {
expect(mapColor('bright')).toBe('magenta');
});

it('passes through known ANSI colors', () => {
expect(mapColor('blue')).toBe('blue');
expect(mapColor('green')).toBe('green');
expect(mapColor('red')).toBe('red');
expect(mapColor('yellow')).toBe('yellow');
expect(mapColor('cyan')).toBe('cyan');
});

it('passes through unknown color names', () => {
expect(mapColor('purple')).toBe('purple');
});

it('returns "white" for undefined', () => {
expect(mapColor(undefined)).toBe('white');
});
});

describe('buildBanner', () => {
it('creates ASCII banner with mode eye and name', () => {
const banner = buildBanner('◇', 'PLAN');
expect(banner).toBe('╭━━━━━╮\n┃ ◇‿◇ ┃ PLAN mode!\n╰━━┳━━╯');
});

it('works with different modes', () => {
const banner = buildBanner('◆', 'ACT');
expect(banner).toContain('◆‿◆');
expect(banner).toContain('ACT mode!');
});
});

describe('buildCollaboration', () => {
it('returns "minimal" format when eco=true', () => {
const result = buildCollaboration(true);
expect(result.format).toBe('minimal');
expect(result.renderHint).toBe('Display agent collaboration in character format');
});

it('returns "discussion" format when eco=false', () => {
const result = buildCollaboration(false);
expect(result.format).toBe('discussion');
expect(result.renderHint).toBe('Display agent collaboration in character format');
});
});

describe('buildVisualData', () => {
it('builds complete visual data with all agents', () => {
const result = buildVisualData(
'PLAN',
{ eye: '◇', colorAnsi: 'blue', group: 'workflow' },
{ name: 'Architecture', visual: { eye: '⬡', colorAnsi: 'bright' } },
[
{ name: 'Security', visual: { eye: '◮', colorAnsi: 'red' } },
{ name: 'Testing', visual: { eye: '⊛', colorAnsi: 'green' } },
],
false,
);

expect(result.banner).toContain('◇‿◇');
expect(result.banner).toContain('PLAN mode!');
expect(result.agents).toHaveLength(3);

// Primary agent is "analyzing"
expect(result.agents[0]).toEqual({
name: 'Architecture',
face: '⬡‿⬡',
color: 'magenta',
status: 'analyzing',
});

// Specialists are "waiting"
expect(result.agents[1]).toEqual({
name: 'Security',
face: '◮‿◮',
color: 'red',
status: 'waiting',
});
expect(result.agents[2]).toEqual({
name: 'Testing',
face: '⊛‿⊛',
color: 'green',
status: 'waiting',
});

expect(result.collaboration.format).toBe('discussion');
});

it('uses default eye when visual is missing', () => {
const result = buildVisualData('ACT', undefined, { name: 'Engineer' }, [], true);

expect(result.banner).toContain('●‿●');
expect(result.agents[0].face).toBe('●‿●');
expect(result.agents[0].color).toBe('white');
});

it('handles no primary agent', () => {
const result = buildVisualData(
'EVAL',
{ eye: '⊘' },
undefined,
[{ name: 'Quality', visual: { eye: '⊛', colorAnsi: 'green' } }],
true,
);

expect(result.agents).toHaveLength(1);
expect(result.agents[0].status).toBe('waiting');
});

it('handles empty specialists', () => {
const result = buildVisualData(
'AUTO',
{ eye: '⟐' },
{ name: 'Architect', visual: { eye: '⬣', colorAnsi: 'bright' } },
[],
false,
);

expect(result.agents).toHaveLength(1);
expect(result.agents[0].status).toBe('analyzing');
});

it('respects eco setting for collaboration format', () => {
const ecoResult = buildVisualData('PLAN', undefined, undefined, [], true);
expect(ecoResult.collaboration.format).toBe('minimal');

const fullResult = buildVisualData('PLAN', undefined, undefined, [], false);
expect(fullResult.collaboration.format).toBe('discussion');
});

it('handles agents with partial visual data', () => {
const result = buildVisualData(
'PLAN',
{ eye: '◇' },
{ name: 'Dev', visual: { eye: '★' } }, // no colorAnsi
[{ name: 'Sec', visual: { colorAnsi: 'red' } }], // no eye
true,
);

expect(result.agents[0].face).toBe('★‿★');
expect(result.agents[0].color).toBe('white'); // default
expect(result.agents[1].face).toBe('●‿●'); // default eye
expect(result.agents[1].color).toBe('red');
});
});
116 changes: 116 additions & 0 deletions apps/mcp-server/src/keyword/visual-data.builder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import type {
AgentVisualRaw,
AgentVisualInfo,
CollaborationConfig,
VisualData,
} from './keyword.types';

/** ANSI color name to display color mapping */
const COLOR_MAP: Record<string, string> = {
bright: 'magenta',
blue: 'blue',
green: 'green',
yellow: 'yellow',
red: 'red',
cyan: 'cyan',
white: 'white',
};

/** Default eye symbol when visual data is missing */
const DEFAULT_EYE = '●';

/** Default color when colorAnsi is missing */
const DEFAULT_COLOR = 'white';

/**
* Build face expression from eye symbol.
* Combines eye symbol with a smile to create a character face.
*
* @example buildFace('◇') => '◇‿◇'
*/
export function buildFace(eye: string): string {
return `${eye}‿${eye}`;
}

/**
* Map ANSI color name to display color.
* Falls back to input value if no mapping exists.
*/
export function mapColor(colorAnsi: string | undefined): string {
if (!colorAnsi) return DEFAULT_COLOR;
return COLOR_MAP[colorAnsi] ?? colorAnsi;
}

/**
* Build ASCII art banner for the current mode.
* Uses the mode agent's eye symbol to create a character face in the banner.
*/
export function buildBanner(modeEye: string, modeName: string): string {
const face = buildFace(modeEye);
return `╭━━━━━╮\n┃ ${face} ┃ ${modeName} mode!\n╰━━┳━━╯`;
}

/**
* Build collaboration config based on eco setting.
* eco=true → "minimal" (core consensus only)
* eco=false → "discussion" (full agent discussion)
*/
export function buildCollaboration(eco: boolean): CollaborationConfig {
return {
format: eco ? 'minimal' : 'discussion',
renderHint: 'Display agent collaboration in character format',
};
}

/** Input for building agent visual info */
export interface AgentVisualInput {
name: string;
visual?: AgentVisualRaw;
}

/**
* Build complete visual data for parse_mode response.
*
* @param modeName - Current mode name (e.g., "PLAN", "ACT")
* @param modeVisual - Visual data from the mode agent JSON
* @param primaryAgent - Primary agent with optional visual data
* @param specialists - Specialist agents with optional visual data
* @param eco - Whether eco mode is enabled (affects collaboration format)
*/
export function buildVisualData(
modeName: string,
modeVisual: AgentVisualRaw | undefined,
primaryAgent: AgentVisualInput | undefined,
specialists: AgentVisualInput[],
eco: boolean,
): VisualData {
const modeEye = modeVisual?.eye ?? DEFAULT_EYE;

const agents: AgentVisualInfo[] = [];

// Add primary agent as "analyzing"
if (primaryAgent) {
agents.push({
name: primaryAgent.name,
face: buildFace(primaryAgent.visual?.eye ?? DEFAULT_EYE),
color: mapColor(primaryAgent.visual?.colorAnsi),
status: 'analyzing',
});
}

// Add specialists as "waiting"
for (const specialist of specialists) {
agents.push({
name: specialist.name,
face: buildFace(specialist.visual?.eye ?? DEFAULT_EYE),
color: mapColor(specialist.visual?.colorAnsi),
status: 'waiting',
});
}

return {
banner: buildBanner(modeEye, modeName),
agents,
collaboration: buildCollaboration(eco),
};
}
Loading
Loading