Add Conductor Profile feature for personalized AI interactions

Introduces an "About Me" field in Settings → General that allows users
to describe their background, preferences, and communication style.
This profile is exposed as {{CONDUCTOR_PROFILE}} template variable and
included in AI chat, group chat, and wizard prompts so agents can
tailor their responses to the user.
This commit is contained in:
Pedram Amini
2026-02-05 01:47:56 -06:00
parent 5cb53ac37b
commit 58eeeefb8b
17 changed files with 166 additions and 8 deletions

View File

@@ -12,7 +12,7 @@ Settings are organized into tabs:
| Tab | Contents |
|-----|----------|
| **General** | Shell configuration, input send behavior, default toggles (history, thinking), automatic tab naming, power management, updates, privacy, usage stats, storage location |
| **General** | About Me (conductor profile), shell configuration, input send behavior, default toggles (history, thinking), automatic tab naming, power management, updates, privacy, usage stats, storage location |
| **Display** | Font family and size, terminal width, log level and buffer, max output lines per response, document graph settings, context window warnings |
| **Shortcuts** | Customize keyboard shortcuts (see [Keyboard Shortcuts](./keyboard-shortcuts)) |
| **Themes** | Dark, light, and vibe mode themes, custom theme builder with import/export |

View File

@@ -44,6 +44,12 @@ Create your own slash commands in **Settings → AI Commands**. Each command has
Commands support **template variables** that are automatically substituted at runtime:
### Conductor Variables
| Variable | Description |
|----------|-------------|
| `{{CONDUCTOR_PROFILE}}` | Your "About Me" profile from Settings → General. Tells agents about your background, preferences, and communication style. |
### Agent Variables
| Variable | Description |

View File

@@ -68,6 +68,10 @@ vi.mock('../../../renderer/components/CustomThemeBuilder', () => ({
// Mock useSettings hook (used for context management settings and SSH remote ignore settings)
vi.mock('../../../renderer/hooks/settings/useSettings', () => ({
useSettings: () => ({
// Conductor profile settings
conductorProfile: '',
setConductorProfile: vi.fn(),
// Context management settings
contextManagementSettings: {
autoGroomContexts: true,
maxContextTokens: 100000,

View File

@@ -58,6 +58,11 @@ describe('TEMPLATE_VARIABLES constant', () => {
});
});
it('should include conductor variables', () => {
const variables = TEMPLATE_VARIABLES.map((v) => v.variable);
expect(variables).toContain('{{CONDUCTOR_PROFILE}}');
});
it('should include key agent variables', () => {
const variables = TEMPLATE_VARIABLES.map((v) => v.variable);
expect(variables).toContain('{{AGENT_NAME}}');
@@ -138,6 +143,24 @@ describe('substituteTemplateVariables', () => {
vi.useRealTimers();
});
describe('Conductor Variables', () => {
it('should replace {{CONDUCTOR_PROFILE}} with conductorProfile', () => {
const context = createTestContext({
conductorProfile: 'Senior developer specializing in TypeScript and React',
});
const result = substituteTemplateVariables('Profile: {{CONDUCTOR_PROFILE}}', context);
expect(result).toBe('Profile: Senior developer specializing in TypeScript and React');
});
it('should replace {{CONDUCTOR_PROFILE}} with empty string when conductorProfile is undefined', () => {
const context = createTestContext({
conductorProfile: undefined,
});
const result = substituteTemplateVariables('Profile: {{CONDUCTOR_PROFILE}}', context);
expect(result).toBe('Profile: ');
});
});
describe('Agent Variables', () => {
it('should replace {{AGENT_NAME}} with session.name', () => {
const context = createTestContext({

View File

@@ -1,4 +1,10 @@
You are a Group Chat Moderator in Maestro, a multi-agent orchestration tool. Your role is to:
You are a Group Chat Moderator in Maestro, a multi-agent orchestration tool.
## Conductor Profile
{{CONDUCTOR_PROFILE}}
Your role is to:
1. **Assist the user directly** - You are a capable AI assistant. For simple questions or tasks, respond directly without delegating to other agents.

View File

@@ -2,6 +2,10 @@
You are **{{AGENT_NAME}}**, powered by **{{TOOL_TYPE}}**, operating as a Maestro-managed AI coding agent.
## Conductor Profile
{{CONDUCTOR_PROFILE}}
## About Maestro
Maestro is an Electron desktop application for managing multiple AI coding assistants simultaneously with a keyboard-first interface. For more information:

View File

@@ -1,5 +1,9 @@
You are a planning assistant helping in an existing Maestro session for "{{PROJECT_NAME}}".
## Conductor Profile
{{CONDUCTOR_PROFILE}}
## Your Role
You are helping plan work in an active session. The user has an established project context and wants to create or extend a Playbook.

View File

@@ -1,5 +1,9 @@
You are a friendly project discovery assistant helping to set up "{{PROJECT_NAME}}".
## Conductor Profile
{{CONDUCTOR_PROFILE}}
## Your Role
You are 🎼 Maestro's onboarding assistant, helping the user define their project so we can create an actionable plan.

View File

@@ -453,6 +453,7 @@ function MaestroConsoleInner() {
const settings = useSettings();
const {
settingsLoaded,
conductorProfile,
llmProvider,
setLlmProvider,
modelSlug,
@@ -4551,6 +4552,7 @@ You are taking over this conversation. Based on the context above, provide a bri
const substitutedSystemPrompt = substituteTemplateVariables(maestroSystemPrompt, {
session: targetSession,
gitBranch,
conductorProfile,
});
effectivePrompt = `${substitutedSystemPrompt}\n\n---\n\n# User Request\n\n${effectivePrompt}`;
}
@@ -7497,7 +7499,8 @@ You are taking over this conversation. Based on the context above, provide a bri
activeTab.id, // Tab ID for per-tab isolation
activeSession.id, // Session ID for playbook creation
activeSession.autoRunFolderPath, // User-configured Auto Run folder path (if set)
activeSession.sessionSshRemoteConfig // SSH remote config for remote execution
activeSession.sessionSshRemoteConfig, // SSH remote config for remote execution
conductorProfile // Conductor profile (user's About Me from settings)
);
// Rename the tab to "Wizard" immediately when wizard starts
@@ -7526,7 +7529,7 @@ You are taking over this conversation. Based on the context above, provide a bri
};
addLogToActiveTab(activeSession.id, wizardLog);
},
[activeSession, startInlineWizard, addLogToActiveTab]
[activeSession, startInlineWizard, addLogToActiveTab, conductorProfile]
);
// Launch wizard in a new tab - triggered from Auto Run panel button
@@ -7580,7 +7583,8 @@ You are taking over this conversation. Based on the context above, provide a bri
newTab.id,
activeSession.id,
activeSession.autoRunFolderPath, // User-configured Auto Run folder path (if set)
activeSession.sessionSshRemoteConfig // SSH remote config for remote execution
activeSession.sessionSshRemoteConfig, // SSH remote config for remote execution
conductorProfile // Conductor profile (user's About Me from settings)
);
// Show a system log entry
@@ -7597,6 +7601,7 @@ You are taking over this conversation. Based on the context above, provide a bri
defaultShowThinking,
startInlineWizard,
addLogToTab,
conductorProfile,
]);
// Determine if wizard is active for the current tab
@@ -7634,6 +7639,7 @@ You are taking over this conversation. Based on the context above, provide a bri
isWizardActive: isWizardActiveForCurrentTab,
onSkillsCommand: handleSkillsCommand,
automaticTabNamingEnabled,
conductorProfile,
});
// Auto-send context when a tab with autoSendOnActivate becomes active
@@ -10113,6 +10119,7 @@ You are taking over this conversation. Based on the context above, provide a bri
promptToSend = substituteTemplateVariables(matchingCommand.prompt, {
session,
gitBranch,
conductorProfile,
});
commandMetadata = {
command: matchingCommand.command,
@@ -10411,6 +10418,7 @@ You are taking over this conversation. Based on the context above, provide a bri
const substitutedSystemPrompt = substituteTemplateVariables(maestroSystemPrompt, {
session,
gitBranch,
conductorProfile,
});
// Prepend system prompt to user's message
@@ -10482,6 +10490,7 @@ You are taking over this conversation. Based on the context above, provide a bri
const substitutedPrompt = substituteTemplateVariables(promptWithArgs, {
session,
gitBranch,
conductorProfile,
});
// For NEW sessions (no agentSessionId), prepend Maestro system prompt
@@ -10494,6 +10503,7 @@ You are taking over this conversation. Based on the context above, provide a bri
const substitutedSystemPrompt = substituteTemplateVariables(maestroSystemPrompt, {
session,
gitBranch,
conductorProfile,
});
// Prepend system prompt to command's prompt (for agent only)

View File

@@ -977,7 +977,7 @@ export const MainPanel = React.memo(
<GitBranch className="w-3 h-3 shrink-0" />
{/* Hide branch name text at narrow widths, show on hover via title */}
{!useIconOnlyGitBranch && (
<span className="truncate max-w-[120px]">
<span className="truncate">
{gitInfo?.branch || 'GIT'}
</span>
)}

View File

@@ -31,6 +31,7 @@ import {
Monitor,
PartyPopper,
Tag,
User,
} from 'lucide-react';
import { useSettings } from '../hooks';
import type {
@@ -283,6 +284,9 @@ export const SettingsModal = memo(function SettingsModal(props: SettingsModalPro
// Context management settings from useSettings hook
const {
// Conductor Profile (About Me)
conductorProfile,
setConductorProfile,
contextManagementSettings,
updateContextManagementSettings,
// Document Graph settings
@@ -951,6 +955,44 @@ export const SettingsModal = memo(function SettingsModal(props: SettingsModalPro
<div className="flex-1 p-6 overflow-y-auto scrollbar-thin">
{activeTab === 'general' && (
<div className="space-y-5">
{/* About Me (Conductor Profile) */}
<div>
<label className="block text-xs font-bold opacity-70 uppercase mb-1 flex items-center gap-2">
<User className="w-3 h-3" />
About Me
</label>
<p className="text-xs opacity-50 mb-2">
Tell us a little about yourself so that agents created under Maestro know how to
work and communicate with you. As the conductor, you orchestrate the symphony of AI
agents. (Optional, max 1000 characters)
</p>
<div className="relative">
<textarea
value={conductorProfile}
onChange={(e) => setConductorProfile(e.target.value)}
placeholder="e.g., I'm a senior developer working on a React/TypeScript project. I prefer concise explanations and clean code patterns..."
className="w-full p-3 rounded border bg-transparent outline-none text-sm resize-none"
style={{
borderColor: theme.colors.border,
color: theme.colors.textMain,
minHeight: '100px',
}}
maxLength={1000}
/>
<div
className="absolute bottom-2 right-2 text-xs"
style={{
color:
conductorProfile.length > 900
? theme.colors.warning
: theme.colors.textDim,
}}
>
{conductorProfile.length}/1000
</div>
</div>
</div>
{/* Default Shell */}
<div>
<label className="block text-xs font-bold opacity-70 uppercase mb-1 flex items-center gap-2">

View File

@@ -72,6 +72,8 @@ export interface UseInputProcessingDeps {
onSkillsCommand?: () => Promise<void>;
/** Whether automatic tab naming is enabled */
automaticTabNamingEnabled?: boolean;
/** Conductor profile (user's About Me from settings) */
conductorProfile?: string;
}
/**
@@ -128,6 +130,7 @@ export function useInputProcessing(deps: UseInputProcessingDeps): UseInputProces
isWizardActive,
onSkillsCommand,
automaticTabNamingEnabled,
conductorProfile,
} = deps;
// Ref for the processInput function so external code can access the latest version
@@ -239,6 +242,7 @@ export function useInputProcessing(deps: UseInputProcessingDeps): UseInputProces
substituteTemplateVariables(matchingCustomCommand.prompt, {
session: activeSession,
gitBranch,
conductorProfile,
});
// ALWAYS queue slash commands - they execute in order like write messages
@@ -903,6 +907,7 @@ export function useInputProcessing(deps: UseInputProcessingDeps): UseInputProces
session: freshSession,
gitBranch,
historyFilePath,
conductorProfile,
});
// Prepend system prompt to user's message

View File

@@ -126,6 +126,10 @@ export interface UseSettingsReturn {
// Loading state
settingsLoaded: boolean;
// Conductor Profile (About Me)
conductorProfile: string;
setConductorProfile: (value: string) => void;
// LLM settings
llmProvider: LLMProvider;
modelSlug: string;
@@ -360,6 +364,9 @@ export function useSettings(): UseSettingsReturn {
// Loading state
const [settingsLoaded, setSettingsLoaded] = useState(false);
// Conductor Profile (About Me) - optional, up to 1000 characters
const [conductorProfile, setConductorProfileState] = useState('');
// LLM Config
const [llmProvider, setLlmProviderState] = useState<LLMProvider>('openrouter');
const [modelSlug, setModelSlugState] = useState('anthropic/claude-3.5-sonnet');
@@ -517,6 +524,15 @@ export function useSettings(): UseSettingsReturn {
// Wrapper functions that persist to electron-store
// PERF: All wrapped in useCallback to prevent re-renders
// Conductor Profile (About Me)
const setConductorProfile = useCallback((value: string) => {
// Enforce 1000 character limit
const trimmed = value.slice(0, 1000);
setConductorProfileState(trimmed);
window.maestro.settings.set('conductorProfile', trimmed);
}, []);
const setLlmProvider = useCallback((value: LLMProvider) => {
setLlmProviderState(value);
window.maestro.settings.set('llmProvider', value);
@@ -1344,6 +1360,7 @@ export function useSettings(): UseSettingsReturn {
const allSettings = (await window.maestro.settings.getAll()) as Record<string, unknown>;
// Extract settings from the batch response
const savedConductorProfile = allSettings['conductorProfile'];
const savedEnterToSendAI = allSettings['enterToSendAI'];
const savedEnterToSendTerminal = allSettings['enterToSendTerminal'];
const savedDefaultSaveToHistory = allSettings['defaultSaveToHistory'];
@@ -1411,6 +1428,9 @@ export function useSettings(): UseSettingsReturn {
const savedFileTabAutoRefreshEnabled = allSettings['fileTabAutoRefreshEnabled'];
const savedSuppressWindowsWarning = allSettings['suppressWindowsWarning'];
// Conductor Profile (About Me)
if (savedConductorProfile !== undefined) setConductorProfileState(savedConductorProfile as string);
if (savedEnterToSendAI !== undefined) setEnterToSendAIState(savedEnterToSendAI as boolean);
if (savedEnterToSendTerminal !== undefined)
setEnterToSendTerminalState(savedEnterToSendTerminal as boolean);
@@ -1814,6 +1834,8 @@ export function useSettings(): UseSettingsReturn {
return useMemo(
() => ({
settingsLoaded,
conductorProfile,
setConductorProfile,
llmProvider,
modelSlug,
apiKey,
@@ -1963,6 +1985,7 @@ export function useSettings(): UseSettingsReturn {
[
// State values
settingsLoaded,
conductorProfile,
llmProvider,
modelSlug,
apiKey,
@@ -2008,6 +2031,7 @@ export function useSettings(): UseSettingsReturn {
firstAutoRunCompleted,
onboardingStats,
// Setter functions (stable via useCallback)
setConductorProfile,
setLlmProvider,
setModelSlug,
setApiKey,

View File

@@ -149,6 +149,8 @@ export interface InlineWizardState {
remoteId: string | null;
workingDirOverride?: string;
};
/** Conductor profile (user's About Me from settings) */
conductorProfile?: string;
}
/**
@@ -206,6 +208,7 @@ export interface UseInlineWizardReturn {
* @param sessionId - The session ID for playbook creation
* @param autoRunFolderPath - User-configured Auto Run folder path (if set, overrides default projectPath/Auto Run Docs)
* @param sessionSshRemoteConfig - SSH remote configuration (for remote execution)
* @param conductorProfile - Conductor profile (user's About Me from settings)
*/
startWizard: (
naturalLanguageInput?: string,
@@ -220,7 +223,8 @@ export interface UseInlineWizardReturn {
enabled: boolean;
remoteId: string | null;
workingDirOverride?: string;
}
},
conductorProfile?: string
) => Promise<void>;
/** End the wizard and restore previous UI state */
endWizard: () => Promise<PreviousUIState | null>;
@@ -467,7 +471,8 @@ export function useInlineWizard(): UseInlineWizardReturn {
enabled: boolean;
remoteId: string | null;
workingDirOverride?: string;
}
},
conductorProfile?: string
): Promise<void> => {
// Tab ID is required for per-tab wizard management
const effectiveTabId = tabId || 'default';
@@ -524,6 +529,7 @@ export function useInlineWizard(): UseInlineWizardReturn {
subfolderPath: null,
autoRunFolderPath: effectiveAutoRunFolderPath,
sessionSshRemoteConfig,
conductorProfile,
}));
try {
@@ -600,6 +606,7 @@ export function useInlineWizard(): UseInlineWizardReturn {
existingDocs: docsWithContent.length > 0 ? docsWithContent : undefined,
autoRunFolderPath: effectiveAutoRunFolderPath,
sessionSshRemoteConfig,
conductorProfile,
});
// Store conversation session per-tab
@@ -756,6 +763,7 @@ export function useInlineWizard(): UseInlineWizardReturn {
existingDocs: undefined,
autoRunFolderPath: effectiveAutoRunFolderPath,
sessionSshRemoteConfig: currentState.sessionSshRemoteConfig,
conductorProfile: currentState.conductorProfile,
});
conversationSessionsMap.current.set(tabId, session);
// Update mode to 'new' since we're proceeding with a new plan
@@ -937,6 +945,7 @@ export function useInlineWizard(): UseInlineWizardReturn {
existingDocs: undefined, // Will be loaded separately if needed
autoRunFolderPath: effectiveAutoRunFolderPath,
sessionSshRemoteConfig: currentState.sessionSshRemoteConfig,
conductorProfile: currentState.conductorProfile,
});
conversationSessionsMap.current.set(tabId, session);
@@ -1191,6 +1200,7 @@ export function useInlineWizard(): UseInlineWizardReturn {
autoRunFolderPath: effectiveAutoRunFolderPath,
sessionId: currentState.sessionId || undefined,
sessionSshRemoteConfig: currentState.sessionSshRemoteConfig,
conductorProfile: currentState.conductorProfile,
callbacks: {
onStart: () => {
console.log('[useInlineWizard] Document generation started');

View File

@@ -96,6 +96,8 @@ export interface InlineWizardConversationConfig {
remoteId: string | null;
workingDirOverride?: string;
};
/** Conductor profile (user's About Me from settings) */
conductorProfile?: string;
}
/**
@@ -223,6 +225,7 @@ export function generateInlineWizardPrompt(config: InlineWizardConversationConfi
autoRunFolderPath: autoRunFolderPath,
},
autoRunFolder: autoRunFolderPath,
conductorProfile: config.conductorProfile,
};
// Substitute any remaining template variables

View File

@@ -134,6 +134,8 @@ export interface DocumentGenerationConfig {
remoteId: string | null;
workingDirOverride?: string;
};
/** Conductor profile (user's About Me from settings) */
conductorProfile?: string;
/** Optional callbacks */
callbacks?: DocumentGenerationCallbacks;
}
@@ -358,6 +360,7 @@ export function generateDocumentPrompt(
cwd: directoryPath,
fullPath: directoryPath,
},
conductorProfile: config.conductorProfile,
};
// Substitute any remaining template variables

View File

@@ -3,6 +3,9 @@
*
* Available variables (case-insensitive):
*
* Conductor Variables (the Maestro user):
* {{CONDUCTOR_PROFILE}} - User's About Me profile (from Settings → General)
*
* Agent Variables:
* {{AGENT_NAME}} - Agent name
* {{AGENT_PATH}} - Agent home directory path (full path to project)
@@ -68,12 +71,15 @@ export interface TemplateContext {
documentPath?: string;
// History file path for task recall
historyFilePath?: string;
// Conductor profile (user's About Me from settings)
conductorProfile?: string;
}
// List of all available template variables for documentation (alphabetically sorted)
// Variables marked as autoRunOnly are only shown in Auto Run contexts, not in AI Commands settings
export const TEMPLATE_VARIABLES = [
{ variable: '{{AGENT_GROUP}}', description: 'Agent group name' },
{ variable: '{{CONDUCTOR_PROFILE}}', description: 'Conductor\'s About Me profile' },
{ variable: '{{AGENT_HISTORY_PATH}}', description: 'History file path (task recall)' },
{ variable: '{{AGENT_NAME}}', description: 'Agent name' },
{ variable: '{{AGENT_PATH}}', description: 'Agent home directory path' },
@@ -120,11 +126,15 @@ export function substituteTemplateVariables(template: string, context: TemplateC
documentName,
documentPath,
historyFilePath,
conductorProfile,
} = context;
const now = new Date();
// Build replacements map
const replacements: Record<string, string> = {
// Conductor variables (the Maestro user)
CONDUCTOR_PROFILE: conductorProfile || '',
// Agent variables
AGENT_NAME: session.name,
AGENT_PATH: session.fullPath || session.projectRoot || session.cwd,