From 2adacdbfa1d1049a0497e55f2b3ed00551bf876f Mon Sep 17 00:00:00 2001
From: YurenHao0426 <blackhao0426@gmail.com>
Date: Fri, 13 Feb 2026 22:46:06 +0000
Subject: Add per-model council settings, Quick Chat council mode, and
 per-member trace selection

Council members now support individual temperature, reasoning effort, web search, and
context trace overrides. Quick Chat inherits council config from the source node and
streams through the 3-stage council pipeline. Blueprint loading migrates old string[]
council formats to CouncilMemberConfig[].

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---
 frontend/src/components/Sidebar.tsx | 967 +++++++++++++++++++++++++++---------
 1 file changed, 731 insertions(+), 236 deletions(-)

(limited to 'frontend/src/components/Sidebar.tsx')

diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx
index 4d7bb51..64cd79a 100644
--- a/frontend/src/components/Sidebar.tsx
+++ b/frontend/src/components/Sidebar.tsx
@@ -2,7 +2,7 @@ import React, { useState, useEffect, useRef, useMemo } from 'react';
 import { useReactFlow } from 'reactflow';
 import useFlowStore from '../store/flowStore';
 import { useAuthStore } from '../store/authStore';
-import type { NodeData, Trace, Message, MergedTrace, MergeStrategy, CouncilData } from '../store/flowStore';
+import type { NodeData, Trace, Message, MergedTrace, MergeStrategy, CouncilData, CouncilMemberConfig } from '../store/flowStore';
 import type { Edge } from 'reactflow';
 import ReactMarkdown from 'react-markdown';
 import remarkGfm from 'remark-gfm';
@@ -17,7 +17,7 @@ const preprocessLaTeX = (content: string): string => {
     .replace(/\\\(([\s\S]*?)\\\)/g, (_, math) => `$${math}$`);
 };
 
-import { Play, Settings, Info, ChevronLeft, ChevronRight, Maximize2, Edit3, X, Check, FileText, MessageCircle, Send, GripVertical, GitMerge, Trash2, AlertCircle, Loader2, Navigation, Upload, Search, Link, Layers, Eye, EyeOff, Copy, ClipboardCheck, Users } from 'lucide-react';
+import { Play, Settings, Info, ChevronLeft, ChevronRight, ChevronDown, Maximize2, Edit3, X, Check, FileText, MessageCircle, Send, GripVertical, GitMerge, Trash2, AlertCircle, Loader2, Navigation, Upload, Search, Link, Layers, Eye, EyeOff, Copy, ClipboardCheck, Users } from 'lucide-react';
 
 interface SidebarProps {
   isOpen: boolean;
@@ -96,10 +96,19 @@ const Sidebar: React.FC<SidebarProps> = ({ isOpen, onToggle, onInteract }) => {
   // Council mode states
   const [councilStage, setCouncilStage] = useState<string>('');
   const [councilStreamBuffer, setCouncilStreamBuffer] = useState('');
+  const [openCtxDropdown, setOpenCtxDropdown] = useState<string | null>(null);
   const [councilTab, setCouncilTab] = useState<'final' | 'responses' | 'rankings'>('final');
   const [councilResponseTab, setCouncilResponseTab] = useState(0);
   const [councilRankingTab, setCouncilRankingTab] = useState<'aggregate' | number>('aggregate');
 
+  // Quick Chat council mode states
+  const [quickChatCouncilMode, setQuickChatCouncilMode] = useState(false);
+  const [quickChatCouncilModels, setQuickChatCouncilModels] = useState<CouncilMemberConfig[]>([]);
+  const [quickChatChairmanModel, setQuickChatChairmanModel] = useState<CouncilMemberConfig | null>(null);
+  const [quickChatCouncilStage, setQuickChatCouncilStage] = useState('');
+  const [quickChatCouncilData, setQuickChatCouncilData] = useState<CouncilData | null>(null);
+  const [quickChatCouncilConfigOpen, setQuickChatCouncilConfigOpen] = useState(false);
+
   const selectedNode = nodes.find((n) => n.id === selectedNodeId);
 
   // Reset stream buffer and modal states when node changes
@@ -424,8 +433,8 @@ const Sidebar: React.FC<SidebarProps> = ({ isOpen, onToggle, onInteract }) => {
   // Council mode: 3-stage LLM council execution
   const handleRunCouncil = async () => {
     if (!selectedNode) return;
-    const councilModels = selectedNode.data.councilModels || [];
-    const chairmanModel = selectedNode.data.chairmanModel || councilModels[0];
+    const councilModels: CouncilMemberConfig[] = selectedNode.data.councilModels || [];
+    const chairmanConfig: CouncilMemberConfig = selectedNode.data.chairmanModel || councilModels[0];
     if (councilModels.length < 2) return;
 
     const tracesCheck = checkActiveTracesComplete();
@@ -473,6 +482,27 @@ const Sidebar: React.FC<SidebarProps> = ({ isOpen, onToggle, onInteract }) => {
         ? 'Please analyze the attached files.'
         : '';
 
+    // Resolve context messages for a specific trace ID
+    const resolveTraceContext = (traceId: string): Message[] => {
+      const node = nodes.find(n => n.id === runningNodeId);
+      if (!node) return [];
+      // Search incoming traces
+      let trace: Trace | undefined = (node.data.traces || []).find((t: Trace) => t.id === traceId);
+      // Then outgoing traces
+      if (!trace) trace = (node.data.outgoingTraces || []).find((t: Trace) => t.id === traceId);
+      if (trace) {
+        const nodePrefix = `${runningNodeId}-`;
+        const isOriginated = trace.id === `trace-${runningNodeId}` ||
+          trace.id.startsWith('fork-') ||
+          (trace.id.startsWith('prepend-') && trace.id.includes(`-from-${runningNodeId}`));
+        return isOriginated ? trace.messages.filter(m => !m.id?.startsWith(nodePrefix)) : [...trace.messages];
+      }
+      // Check merged traces
+      const merged = (node.data.mergedTraces || []).find((m: MergedTrace) => m.id === traceId);
+      if (merged) return [...merged.messages];
+      return [];
+    };
+
     try {
       const response = await fetch(`/api/run_council_stream?user=${encodeURIComponent(user?.username || 'test')}`, {
         method: 'POST',
@@ -481,11 +511,28 @@ const Sidebar: React.FC<SidebarProps> = ({ isOpen, onToggle, onInteract }) => {
           node_id: runningNodeId,
           incoming_contexts: [{ messages: context }],
           user_prompt: effectivePrompt,
-          council_models: councilModels.map((m: string) => ({ model_name: m })),
-          chairman_model: chairmanModel,
+          council_models: councilModels.map((cfg) => {
+            const base: Record<string, any> = {
+              model_name: cfg.model,
+              temperature: cfg.temperature ?? null,
+              reasoning_effort: cfg.reasoningEffort ?? null,
+              enable_google_search: cfg.enableWebSearch ?? null,
+            };
+            if (cfg.traceId) {
+              base.incoming_contexts = [{ messages: resolveTraceContext(cfg.traceId) }];
+            }
+            return base;
+          }),
+          chairman_model: {
+            model_name: chairmanConfig.model,
+            temperature: chairmanConfig.temperature ?? null,
+            reasoning_effort: chairmanConfig.reasoningEffort ?? null,
+            enable_google_search: chairmanConfig.enableWebSearch ?? null,
+          },
           system_prompt: selectedNode.data.systemPrompt || null,
           temperature: selectedNode.data.temperature,
           reasoning_effort: selectedNode.data.reasoningEffort || 'medium',
+          enable_google_search: selectedNode.data.enableGoogleSearch !== false,
           merge_strategy: selectedNode.data.mergeStrategy || 'smart',
           attached_file_ids: attachedFiles,
           scopes,
@@ -856,7 +903,19 @@ const Sidebar: React.FC<SidebarProps> = ({ isOpen, onToggle, onInteract }) => {
       // handleQuickChatSend will decide whether to overwrite (if empty) or create new node (if has response)
       setQuickChatLastNodeId(selectedNode.id);
     }
-    
+
+    // Copy council settings from node if active
+    if (selectedNode.data.councilMode && selectedNode.data.councilModels && selectedNode.data.councilModels.length >= 2) {
+      setQuickChatCouncilMode(true);
+      setQuickChatCouncilModels([...selectedNode.data.councilModels]);
+      setQuickChatChairmanModel(selectedNode.data.chairmanModel || selectedNode.data.councilModels[0]);
+      setQuickChatCouncilConfigOpen(false);
+    } else {
+      setQuickChatCouncilMode(false);
+    }
+    setQuickChatCouncilData(null);
+    setQuickChatCouncilStage('');
+
     setQuickChatOpen(true);
     // If there's an unsent draft, put it in the input box
     setQuickChatInput(hasDraftPrompt ? selectedNode.data.userPrompt : '');
@@ -868,6 +927,9 @@ const Sidebar: React.FC<SidebarProps> = ({ isOpen, onToggle, onInteract }) => {
     setQuickChatMessages([]);
     setQuickChatAttachedFiles([]);
     setQuickChatSentFiles([]);
+    setQuickChatCouncilMode(false);
+    setQuickChatCouncilData(null);
+    setQuickChatCouncilStage('');
   };
 
   // Quick Chat file attachment helpers
@@ -1221,69 +1283,201 @@ const Sidebar: React.FC<SidebarProps> = ({ isOpen, onToggle, onInteract }) => {
     const tempAtSend = quickChatTemp;
     const effortAtSend = quickChatEffort;
     const webSearchAtSend = quickChatWebSearch;
+    const reasoningModels = ['gpt-5', 'gpt-5-chat-latest', 'gpt-5-mini', 'gpt-5-nano', 'gpt-5-pro', 'gpt-5.1', 'gpt-5.1-chat-latest', 'gpt-5.2', 'gpt-5.2-chat-latest', 'gpt-5.2-pro', 'o3'];
 
-    try {
-      // Determine provider
-      const isClaude = modelAtSend.includes('claude');
-      const isOpenAI = modelAtSend.includes('gpt') || modelAtSend === 'o3';
-      const provider = isClaude ? 'claude' : isOpenAI ? 'openai' : 'google';
-      const reasoningModels = ['gpt-5', 'gpt-5-chat-latest', 'gpt-5-mini', 'gpt-5-nano', 'gpt-5-pro', 'gpt-5.1', 'gpt-5.1-chat-latest', 'gpt-5.2', 'gpt-5.2-chat-latest', 'gpt-5.2-pro', 'o3'];
-      const isReasoning = reasoningModels.includes(modelAtSend);
+    // Snapshot council config at send time
+    const isCouncilSend = quickChatCouncilMode && quickChatCouncilModels.length >= 2;
+    const councilModelsAtSend = isCouncilSend ? [...quickChatCouncilModels] : [];
+    const chairmanAtSend = isCouncilSend ? (quickChatChairmanModel || quickChatCouncilModels[0]) : null;
 
+    try {
       // Build scopes for file search (Quick Chat uses a temp scope)
       const projectPath = currentBlueprintPath || 'untitled';
       const scopes = [`${projectPath}/quick_chat_temp`];
 
-      // Call LLM API with current messages as context
-      const response = await fetch(`/api/run_node_stream?user=${encodeURIComponent(user?.username || 'test')}`, {
-        method: 'POST',
-        headers: { 'Content-Type': 'application/json', ...getAuthHeader() },
-        body: JSON.stringify({
-          node_id: 'quick_chat_temp',
-          incoming_contexts: [{ messages: messagesBeforeSend }],
-          user_prompt: userInput || 'Please analyze the attached files.',
-          attached_file_ids: attachedFilesCopy,
-          scopes,
-          merge_strategy: 'smart',
-          config: {
-            provider,
-            model_name: modelAtSend,
-            temperature: isReasoning ? 1 : tempAtSend,
-            enable_google_search: webSearchAtSend,
-            reasoning_effort: effortAtSend,
-          }
-        })
-      });
+      let fullResponse = '';
 
-      if (!response.ok) {
-        const errText = await response.text();
-        throw new Error(errText || `HTTP ${response.status}`);
-      }
-      if (!response.body) throw new Error('No response body');
+      if (isCouncilSend && chairmanAtSend) {
+        // ========== COUNCIL MODE ==========
+        setQuickChatCouncilStage('Starting council...');
+        setQuickChatCouncilData({ stage1: null, stage2: null, stage3: null });
 
-      const reader = response.body.getReader();
-      const decoder = new TextDecoder();
-      let fullResponse = '';
+        const response = await fetch(`/api/run_council_stream?user=${encodeURIComponent(user?.username || 'test')}`, {
+          method: 'POST',
+          headers: { 'Content-Type': 'application/json', ...getAuthHeader() },
+          body: JSON.stringify({
+            node_id: 'quick_chat_temp',
+            incoming_contexts: [{ messages: messagesBeforeSend }],
+            user_prompt: userInput || 'Please analyze the attached files.',
+            council_models: councilModelsAtSend.map(cfg => ({
+              model_name: cfg.model,
+              temperature: cfg.temperature ?? null,
+              reasoning_effort: cfg.reasoningEffort ?? null,
+              enable_google_search: cfg.enableWebSearch ?? null,
+            })),
+            chairman_model: {
+              model_name: chairmanAtSend.model,
+              temperature: chairmanAtSend.temperature ?? null,
+              reasoning_effort: chairmanAtSend.reasoningEffort ?? null,
+              enable_google_search: chairmanAtSend.enableWebSearch ?? null,
+            },
+            system_prompt: selectedNode.data.systemPrompt || null,
+            temperature: selectedNode.data.temperature,
+            reasoning_effort: selectedNode.data.reasoningEffort || 'medium',
+            enable_google_search: selectedNode.data.enableGoogleSearch !== false,
+            merge_strategy: 'smart',
+            attached_file_ids: attachedFilesCopy,
+            scopes,
+          }),
+        });
 
-      // Stream response
-      while (true) {
-        const { value, done } = await reader.read();
-        if (done) break;
-        const chunk = decoder.decode(value);
-        fullResponse += chunk;
-        
-        // Update display in real-time
-        setQuickChatMessages(prev => {
-          const newMsgs = [...prev];
-          const lastMsg = newMsgs[newMsgs.length - 1];
-          if (lastMsg?.role === 'assistant') {
-            // Update existing assistant message
-            return [...newMsgs.slice(0, -1), { ...lastMsg, content: fullResponse }];
-          } else {
-            // Add new assistant message
-            return [...newMsgs, { id: `qc_${Date.now()}_a`, role: 'assistant', content: fullResponse }];
+        if (!response.ok) {
+          const errText = await response.text();
+          throw new Error(errText || `HTTP ${response.status}`);
+        }
+        if (!response.body) throw new Error('No response body');
+
+        const reader = response.body.getReader();
+        const decoder = new TextDecoder();
+        let sseBuffer = '';
+        let stage1Results: Array<{ model: string; response: string }> = [];
+        let stage2Data: any = null;
+        let stage3Full = '';
+        let stage3Model = '';
+
+        while (true) {
+          const { value, done } = await reader.read();
+          if (done) break;
+          sseBuffer += decoder.decode(value, { stream: true });
+          const parts = sseBuffer.split('\n\n');
+          sseBuffer = parts.pop() || '';
+
+          for (const part of parts) {
+            const line = part.trim();
+            if (!line.startsWith('data: ')) continue;
+            let evt: any;
+            try { evt = JSON.parse(line.slice(6)); } catch { continue; }
+
+            switch (evt.type) {
+              case 'stage1_start':
+                setQuickChatCouncilStage('Stage 1: Collecting responses...');
+                break;
+              case 'stage1_model_complete':
+                stage1Results = [...stage1Results, evt.data];
+                setQuickChatCouncilStage(`Stage 1: ${stage1Results.length}/${councilModelsAtSend.length} models done`);
+                setQuickChatCouncilData({ stage1: [...stage1Results], stage2: null, stage3: null });
+                break;
+              case 'stage1_complete':
+                stage1Results = evt.data;
+                setQuickChatCouncilData({ stage1: stage1Results, stage2: null, stage3: null });
+                break;
+              case 'stage2_start':
+                setQuickChatCouncilStage('Stage 2: Peer ranking...');
+                break;
+              case 'stage2_complete':
+                stage2Data = evt.data;
+                setQuickChatCouncilData({ stage1: stage1Results, stage2: stage2Data, stage3: null });
+                break;
+              case 'stage3_start':
+                setQuickChatCouncilStage('Stage 3: Chairman synthesizing...');
+                break;
+              case 'stage3_chunk':
+                stage3Full += evt.data.chunk;
+                // Stream chairman response into chat
+                setQuickChatMessages(prev => {
+                  const newMsgs = [...prev];
+                  const lastMsg = newMsgs[newMsgs.length - 1];
+                  if (lastMsg?.role === 'assistant') {
+                    return [...newMsgs.slice(0, -1), { ...lastMsg, content: stage3Full }];
+                  } else {
+                    return [...newMsgs, { id: `qc_${Date.now()}_a`, role: 'assistant', content: stage3Full }];
+                  }
+                });
+                break;
+              case 'stage3_complete':
+                stage3Model = evt.data.model;
+                stage3Full = evt.data.response;
+                break;
+              case 'complete': {
+                const finalData: CouncilData = {
+                  stage1: stage1Results,
+                  stage2: stage2Data,
+                  stage3: { model: stage3Model, response: stage3Full },
+                };
+                setQuickChatCouncilData(finalData);
+                setQuickChatCouncilStage('');
+                // Ensure final message is set
+                setQuickChatMessages(prev => {
+                  const newMsgs = [...prev];
+                  const lastMsg = newMsgs[newMsgs.length - 1];
+                  if (lastMsg?.role === 'assistant') {
+                    return [...newMsgs.slice(0, -1), { ...lastMsg, content: stage3Full }];
+                  } else {
+                    return [...newMsgs, { id: `qc_${Date.now()}_a`, role: 'assistant', content: stage3Full }];
+                  }
+                });
+                break;
+              }
+              case 'error':
+                setQuickChatCouncilStage('');
+                throw new Error(evt.data?.message || 'Council error');
+            }
           }
+        }
+        fullResponse = stage3Full;
+      } else {
+        // ========== SINGLE MODEL MODE ==========
+        // Determine provider
+        const isClaude = modelAtSend.includes('claude');
+        const isOpenAI = modelAtSend.includes('gpt') || modelAtSend === 'o3';
+        const provider = isClaude ? 'claude' : isOpenAI ? 'openai' : 'google';
+        const isReasoning = reasoningModels.includes(modelAtSend);
+
+        const response = await fetch(`/api/run_node_stream?user=${encodeURIComponent(user?.username || 'test')}`, {
+          method: 'POST',
+          headers: { 'Content-Type': 'application/json', ...getAuthHeader() },
+          body: JSON.stringify({
+            node_id: 'quick_chat_temp',
+            incoming_contexts: [{ messages: messagesBeforeSend }],
+            user_prompt: userInput || 'Please analyze the attached files.',
+            attached_file_ids: attachedFilesCopy,
+            scopes,
+            merge_strategy: 'smart',
+            config: {
+              provider,
+              model_name: modelAtSend,
+              temperature: isReasoning ? 1 : tempAtSend,
+              enable_google_search: webSearchAtSend,
+              reasoning_effort: effortAtSend,
+            }
+          })
         });
+
+        if (!response.ok) {
+          const errText = await response.text();
+          throw new Error(errText || `HTTP ${response.status}`);
+        }
+        if (!response.body) throw new Error('No response body');
+
+        const reader = response.body.getReader();
+        const decoder = new TextDecoder();
+
+        while (true) {
+          const { value, done } = await reader.read();
+          if (done) break;
+          const chunk = decoder.decode(value);
+          fullResponse += chunk;
+
+          setQuickChatMessages(prev => {
+            const newMsgs = [...prev];
+            const lastMsg = newMsgs[newMsgs.length - 1];
+            if (lastMsg?.role === 'assistant') {
+              return [...newMsgs.slice(0, -1), { ...lastMsg, content: fullResponse }];
+            } else {
+              return [...newMsgs, { id: `qc_${Date.now()}_a`, role: 'assistant', content: fullResponse }];
+            }
+          });
+        }
       }
 
       // Determine whether to overwrite current node or create new one
@@ -1295,18 +1489,25 @@ const Sidebar: React.FC<SidebarProps> = ({ isOpen, onToggle, onInteract }) => {
       
       if (!fromNodeHasResponse && fromNode) {
         // Overwrite the source node (it's empty)
-        updateNodeData(fromNodeId, {
+        const nodeUpdate: any = {
           userPrompt: userInput,
           response: fullResponse,
-          model: modelAtSend,
-          temperature: isReasoning ? 1 : tempAtSend,
-          reasoningEffort: effortAtSend,
-          enableGoogleSearch: webSearchAtSend,
+          model: isCouncilSend ? chairmanAtSend!.model : modelAtSend,
+          temperature: isCouncilSend ? selectedNode.data.temperature : (reasoningModels.includes(modelAtSend) ? 1 : tempAtSend),
+          reasoningEffort: isCouncilSend ? (selectedNode.data.reasoningEffort || 'medium') : effortAtSend,
+          enableGoogleSearch: isCouncilSend ? (selectedNode.data.enableGoogleSearch !== false) : webSearchAtSend,
           attachedFileIds: attachedFilesCopy,
           status: 'success',
           querySentAt: Date.now(),
-          responseReceivedAt: Date.now()
-        });
+          responseReceivedAt: Date.now(),
+        };
+        if (isCouncilSend) {
+          nodeUpdate.councilMode = true;
+          nodeUpdate.councilModels = councilModelsAtSend;
+          nodeUpdate.chairmanModel = chairmanAtSend;
+          nodeUpdate.councilData = quickChatCouncilData;
+        }
+        updateNodeData(fromNodeId, nodeUpdate);
         
         // Update trace to reflect current node now has content
         setQuickChatTrace(prev => prev ? {
@@ -1328,19 +1529,15 @@ const Sidebar: React.FC<SidebarProps> = ({ isOpen, onToggle, onInteract }) => {
           y: sourceNode.position.y
         };
         
-        const newNode = {
-          id: newNodeId,
-          type: 'llmNode',
-          position: newPos,
-          data: {
-            label: 'Quick Chat',
-            model: modelAtSend,
-            temperature: isReasoning ? 1 : tempAtSend,
+        const newNodeData: any = {
+            label: isCouncilSend ? 'Council Chat' : 'Quick Chat',
+            model: isCouncilSend ? chairmanAtSend!.model : modelAtSend,
+            temperature: isCouncilSend ? selectedNode.data.temperature : (reasoningModels.includes(modelAtSend) ? 1 : tempAtSend),
             systemPrompt: '',
             userPrompt: userInput,
             mergeStrategy: 'smart' as const,
-            reasoningEffort: effortAtSend,
-            enableGoogleSearch: webSearchAtSend,
+            reasoningEffort: isCouncilSend ? (selectedNode.data.reasoningEffort || 'medium') : effortAtSend,
+            enableGoogleSearch: isCouncilSend ? (selectedNode.data.enableGoogleSearch !== false) : webSearchAtSend,
             traces: [],
             outgoingTraces: [],
             forkedTraces: [],
@@ -1351,7 +1548,20 @@ const Sidebar: React.FC<SidebarProps> = ({ isOpen, onToggle, onInteract }) => {
             status: 'success' as const,
             inputs: 1,
             querySentAt: Date.now(),
-            responseReceivedAt: Date.now()
+            responseReceivedAt: Date.now(),
+        };
+        if (isCouncilSend) {
+          newNodeData.councilMode = true;
+          newNodeData.councilModels = councilModelsAtSend;
+          newNodeData.chairmanModel = chairmanAtSend;
+          newNodeData.councilData = quickChatCouncilData;
+        }
+        const newNode = {
+          id: newNodeId,
+          type: 'llmNode',
+          position: newPos,
+          data: {
+            ...newNodeData,
           }
         };
 
@@ -1553,13 +1763,15 @@ const Sidebar: React.FC<SidebarProps> = ({ isOpen, onToggle, onInteract }) => {
 
     } catch (error) {
       console.error('Quick chat error:', error);
-      setQuickChatMessages(prev => [...prev, { 
-        id: `qc_err_${Date.now()}`, 
-        role: 'assistant', 
-        content: `Error: ${error}` 
+      setQuickChatMessages(prev => [...prev, {
+        id: `qc_err_${Date.now()}`,
+        role: 'assistant',
+        content: `Error: ${error}`
       }]);
+      setQuickChatCouncilStage('');
     } finally {
       setQuickChatLoading(false);
+      setQuickChatCouncilStage('');
       // Refocus the input after sending
       setTimeout(() => {
         quickChatInputRef.current?.focus();
@@ -1697,66 +1909,239 @@ const Sidebar: React.FC<SidebarProps> = ({ isOpen, onToggle, onInteract }) => {
                 <div className={`space-y-2 p-2 rounded border ${isDark ? 'bg-gray-900 border-amber-800/50' : 'bg-amber-50/50 border-amber-200'}`}>
                   <div>
                     <label className={`block text-xs font-semibold mb-1 ${isDark ? 'text-gray-400' : 'text-gray-600'}`}>Council Members (pick 2-6)</label>
-                    <div className="grid grid-cols-1 gap-0.5 max-h-48 overflow-y-auto">
-                      {[
-                        { value: 'claude-sonnet-4-5', label: 'claude-sonnet-4.5' },
-                        { value: 'claude-opus-4', label: 'claude-opus-4' },
-                        { value: 'claude-opus-4-5', label: 'claude-opus-4.5' },
-                        { value: 'claude-opus-4-6', label: 'claude-opus-4.6' },
-                        { value: 'gemini-2.5-flash', label: 'gemini-2.5-flash' },
-                        { value: 'gemini-2.5-flash-lite', label: 'gemini-2.5-flash-lite' },
-                        { value: 'gemini-3-pro-preview', label: 'gemini-3-pro-preview' },
-                        { value: 'gpt-4.1', label: 'gpt-4.1' },
-                        { value: 'gpt-4o', label: 'gpt-4o' },
-                        { value: 'gpt-5', label: 'gpt-5' },
-                        { value: 'gpt-5-mini', label: 'gpt-5-mini' },
-                        { value: 'gpt-5-nano', label: 'gpt-5-nano' },
-                        { value: 'gpt-5.1', label: 'gpt-5.1' },
-                        { value: 'gpt-5.2', label: 'gpt-5.2' },
-                        { value: 'o3', label: 'o3', premium: true },
-                      ].map(m => {
-                        const selected = (selectedNode.data.councilModels || []).includes(m.value);
-                        const disabled = m.premium && !canUsePremiumModels;
-                        return (
-                          <label key={m.value} className={`flex items-center gap-1.5 text-xs py-0.5 px-1 rounded cursor-pointer ${
-                            disabled ? 'opacity-40 cursor-not-allowed' : selected
-                              ? isDark ? 'bg-amber-900/40 text-amber-200' : 'bg-amber-100 text-amber-800'
-                              : isDark ? 'hover:bg-gray-800 text-gray-300' : 'hover:bg-gray-100 text-gray-700'
-                          }`}>
-                            <input
-                              type="checkbox"
-                              checked={selected}
-                              disabled={disabled}
-                              onChange={() => {
-                                const current = selectedNode.data.councilModels || [];
-                                const next = selected
-                                  ? current.filter((v: string) => v !== m.value)
-                                  : [...current, m.value];
-                                handleChange('councilModels', next);
-                                // Auto-set chairman to first selected if current chairman was removed
-                                if (selected && selectedNode.data.chairmanModel === m.value && next.length > 0) {
-                                  handleChange('chairmanModel', next[0]);
-                                }
-                              }}
-                              className="w-3 h-3 accent-amber-500"
-                            />
-                            {m.label}
-                          </label>
-                        );
-                      })}
+                    <div className="grid grid-cols-1 gap-0.5 max-h-40 overflow-y-auto">
+                      {(() => {
+                        const councilModelsList = [
+                          { value: 'claude-sonnet-4-5', label: 'claude-sonnet-4.5' },
+                          { value: 'claude-opus-4', label: 'claude-opus-4' },
+                          { value: 'claude-opus-4-5', label: 'claude-opus-4.5' },
+                          { value: 'claude-opus-4-6', label: 'claude-opus-4.6' },
+                          { value: 'gemini-2.5-flash', label: 'gemini-2.5-flash' },
+                          { value: 'gemini-2.5-flash-lite', label: 'gemini-2.5-flash-lite' },
+                          { value: 'gemini-3-pro-preview', label: 'gemini-3-pro-preview' },
+                          { value: 'gpt-4.1', label: 'gpt-4.1' },
+                          { value: 'gpt-4o', label: 'gpt-4o' },
+                          { value: 'gpt-5', label: 'gpt-5' },
+                          { value: 'gpt-5-mini', label: 'gpt-5-mini' },
+                          { value: 'gpt-5-nano', label: 'gpt-5-nano' },
+                          { value: 'gpt-5.1', label: 'gpt-5.1' },
+                          { value: 'gpt-5.2', label: 'gpt-5.2' },
+                          { value: 'o3', label: 'o3', premium: true },
+                        ];
+                        const members: CouncilMemberConfig[] = selectedNode.data.councilModels || [];
+                        const isReasoningModel = (v: string) => ['gpt-5', 'gpt-5-mini', 'gpt-5-nano', 'gpt-5-pro', 'gpt-5.1', 'gpt-5.2', 'gpt-5.2-pro', 'o3'].includes(v);
+                        const supportsWebSearch = (v: string) => v.startsWith('gemini-') || v.startsWith('gpt-5') || v === 'o3' || v === 'gpt-4o';
+                        const updateCouncilMember = (modelName: string, field: string, value: any) => {
+                          const updated = [...members];
+                          const idx = updated.findIndex(c => c.model === modelName);
+                          if (idx >= 0) {
+                            updated[idx] = { ...updated[idx], [field]: value };
+                            handleChange('councilModels', updated);
+                          }
+                        };
+                        // Gather available complete traces for per-member context selection
+                        const availableTraces: Array<{ id: string; label: string; color: string }> = [];
+                        const incTraces = selectedNode.data.traces || [];
+                        const outTraces = (selectedNode.data.outgoingTraces || []) as Trace[];
+                        const origTraces = outTraces.filter(t => {
+                          if (t.id.startsWith('merged-')) return false;
+                          return t.sourceNodeId === selectedNode.id || t.id.includes(`fork-${selectedNode.id}`) || t.id === `trace-${selectedNode.id}`;
+                        });
+                        const trMap = new Map<string, Trace>();
+                        origTraces.forEach(t => trMap.set(t.id, t));
+                        incTraces.forEach(t => trMap.set(t.id, t));
+                        trMap.forEach((trace, id) => {
+                          if (isTraceComplete(trace)) {
+                            availableTraces.push({ id, label: `#${id.slice(-4)}`, color: trace.color });
+                          }
+                        });
+                        (selectedNode.data.mergedTraces || []).forEach((mt: MergedTrace) => {
+                          if (mt.messages && mt.messages.length > 0) {
+                            availableTraces.push({ id: mt.id, label: `M#${mt.id.slice(-4)}`, color: mt.colors?.[0] || '#888' });
+                          }
+                        });
+                        const defaultTraceId = selectedNode.data.activeTraceIds?.[0] || '';
+                        const defaultTraceColor = availableTraces.find(t => t.id === defaultTraceId)?.color
+                          || trMap.get(defaultTraceId)?.color || '#888';
+                        return councilModelsList.map(m => {
+                          const selected = members.some(c => c.model === m.value);
+                          const disabled = (m as any).premium && !canUsePremiumModels;
+                          const cfg = members.find(c => c.model === m.value);
+                          return (
+                            <div key={m.value}>
+                              <label className={`flex items-center gap-1.5 text-xs py-0.5 px-1 rounded cursor-pointer ${
+                                disabled ? 'opacity-40 cursor-not-allowed' : selected
+                                  ? isDark ? 'bg-amber-900/40 text-amber-200' : 'bg-amber-100 text-amber-800'
+                                  : isDark ? 'hover:bg-gray-800 text-gray-300' : 'hover:bg-gray-100 text-gray-700'
+                              }`}>
+                                <input
+                                  type="checkbox"
+                                  checked={selected}
+                                  disabled={disabled}
+                                  onChange={() => {
+                                    const next = selected
+                                      ? members.filter(c => c.model !== m.value)
+                                      : [...members, { model: m.value }];
+                                    handleChange('councilModels', next);
+                                    // Auto-set chairman to first selected if current chairman was removed
+                                    if (selected && selectedNode.data.chairmanModel?.model === m.value && next.length > 0) {
+                                      handleChange('chairmanModel', { model: next[0].model });
+                                    }
+                                  }}
+                                  className="w-3 h-3 accent-amber-500"
+                                />
+                                {m.label}
+                              </label>
+                              {selected && cfg && (
+                                <div className={`flex items-center gap-2 ml-5 mt-0.5 mb-1 px-1 py-0.5 text-xs rounded ${isDark ? 'bg-gray-800/60' : 'bg-gray-100/80'}`}>
+                                  <label className={`flex items-center gap-1 ${isDark ? 'text-gray-400' : 'text-gray-500'}`}>
+                                    T:
+                                    <input
+                                      type="number"
+                                      min={0} max={2} step={0.1}
+                                      value={cfg.temperature ?? selectedNode.data.temperature}
+                                      disabled={isReasoningModel(m.value)}
+                                      onChange={(e) => updateCouncilMember(m.value, 'temperature', parseFloat(e.target.value) || 0)}
+                                      className={`w-12 border rounded px-1 py-0 text-xs ${isReasoningModel(m.value) ? 'opacity-50 cursor-not-allowed' : ''} ${isDark ? 'bg-gray-700 border-gray-600 text-gray-200' : 'bg-white border-gray-300'}`}
+                                    />
+                                  </label>
+                                  {isReasoningModel(m.value) && (
+                                    <label className={`flex items-center gap-1 ${isDark ? 'text-gray-400' : 'text-gray-500'}`}>
+                                      Effort:
+                                      <select
+                                        value={cfg.reasoningEffort ?? selectedNode.data.reasoningEffort ?? 'medium'}
+                                        onChange={(e) => updateCouncilMember(m.value, 'reasoningEffort', e.target.value)}
+                                        className={`border rounded px-1 py-0 text-xs ${isDark ? 'bg-gray-700 border-gray-600 text-gray-200' : 'bg-white border-gray-300'}`}
+                                      >
+                                        <option value="low">Low</option>
+                                        <option value="medium">Med</option>
+                                        <option value="high">High</option>
+                                      </select>
+                                    </label>
+                                  )}
+                                  {supportsWebSearch(m.value) && (
+                                    <label className={`flex items-center gap-1 ${isDark ? 'text-gray-400' : 'text-gray-500'}`}>
+                                      <input
+                                        type="checkbox"
+                                        checked={cfg.enableWebSearch ?? (selectedNode.data.enableGoogleSearch !== false)}
+                                        onChange={(e) => updateCouncilMember(m.value, 'enableWebSearch', e.target.checked)}
+                                        className="w-3 h-3 accent-amber-500"
+                                      />
+                                      Web
+                                    </label>
+                                  )}
+                                  {availableTraces.length > 1 && (() => {
+                                    const selectedTrace = availableTraces.find(t => t.id === cfg.traceId);
+                                    return (
+                                      <div className="relative">
+                                        <button
+                                          type="button"
+                                          onClick={(e) => {
+                                            e.stopPropagation();
+                                            const key = `ctx-${m.value}`;
+                                            setOpenCtxDropdown(prev => prev === key ? null : key);
+                                          }}
+                                          className={`flex items-center gap-1 border rounded px-1 py-0 text-xs ${isDark ? 'bg-gray-700 border-gray-600 text-gray-200 hover:bg-gray-600' : 'bg-white border-gray-300 hover:bg-gray-50'}`}
+                                        >
+                                          <span className="w-2 h-2 rounded-full shrink-0" style={{ backgroundColor: selectedTrace ? selectedTrace.color : defaultTraceColor }} />
+                                          {selectedTrace ? selectedTrace.label : 'Ctx'}
+                                          <ChevronDown size={8} />
+                                        </button>
+                                        {openCtxDropdown === `ctx-${m.value}` && (
+                                          <div className={`absolute z-50 top-full left-0 mt-1 min-w-[100px] border rounded shadow-lg py-0.5 ${isDark ? 'bg-gray-800 border-gray-600' : 'bg-white border-gray-200'}`}>
+                                            <div
+                                              onClick={() => { updateCouncilMember(m.value, 'traceId', undefined); setOpenCtxDropdown(null); }}
+                                              className={`flex items-center gap-1.5 px-2 py-1 text-xs cursor-pointer ${!cfg.traceId ? 'font-semibold' : ''} ${isDark ? 'hover:bg-gray-700 text-gray-300' : 'hover:bg-gray-100 text-gray-700'}`}
+                                            >
+                                              <span className="w-2 h-2 rounded-full shrink-0" style={{ backgroundColor: defaultTraceColor }} />
+                                              Default
+                                            </div>
+                                            {availableTraces.map(t => (
+                                              <div
+                                                key={t.id}
+                                                onClick={() => { updateCouncilMember(m.value, 'traceId', t.id); setOpenCtxDropdown(null); }}
+                                                className={`flex items-center gap-1.5 px-2 py-1 text-xs cursor-pointer ${cfg.traceId === t.id ? 'font-semibold' : ''} ${isDark ? 'hover:bg-gray-700 text-gray-300' : 'hover:bg-gray-100 text-gray-700'}`}
+                                              >
+                                                <span className="w-2 h-2 rounded-full shrink-0" style={{ backgroundColor: t.color }} />
+                                                {t.label}
+                                              </div>
+                                            ))}
+                                          </div>
+                                        )}
+                                      </div>
+                                    );
+                                  })()}
+                                </div>
+                              )}
+                            </div>
+                          );
+                        });
+                      })()}
                     </div>
                   </div>
                   <div>
                     <label className={`block text-xs font-semibold mb-1 ${isDark ? 'text-gray-400' : 'text-gray-600'}`}>Chairman Model</label>
                     <select
-                      value={selectedNode.data.chairmanModel || (selectedNode.data.councilModels || [])[0] || ''}
-                      onChange={(e) => handleChange('chairmanModel', e.target.value)}
+                      value={selectedNode.data.chairmanModel?.model || (selectedNode.data.councilModels || [])[0]?.model || ''}
+                      onChange={(e) => {
+                        const existing = (selectedNode.data.councilModels || []).find((c: CouncilMemberConfig) => c.model === e.target.value);
+                        handleChange('chairmanModel', existing ? { ...existing } : { model: e.target.value });
+                      }}
                       className={`w-full border rounded-md p-1.5 text-xs ${isDark ? 'bg-gray-800 border-gray-600 text-gray-200' : 'border-gray-300'}`}
                     >
-                      {(selectedNode.data.councilModels || []).map((m: string) => (
-                        <option key={m} value={m}>{m}</option>
+                      {(selectedNode.data.councilModels || []).map((c: CouncilMemberConfig) => (
+                        <option key={c.model} value={c.model}>{c.model}</option>
                       ))}
                     </select>
+                    {(() => {
+                      const chairCfg = selectedNode.data.chairmanModel;
+                      if (!chairCfg) return null;
+                      const isReasoningModel = (v: string) => ['gpt-5', 'gpt-5-mini', 'gpt-5-nano', 'gpt-5-pro', 'gpt-5.1', 'gpt-5.2', 'gpt-5.2-pro', 'o3'].includes(v);
+                      const supportsWebSearch = (v: string) => v.startsWith('gemini-') || v.startsWith('gpt-5') || v === 'o3' || v === 'gpt-4o';
+                      const updateChairman = (field: string, value: any) => {
+                        handleChange('chairmanModel', { ...chairCfg, [field]: value });
+                      };
+                      return (
+                        <div className={`flex items-center gap-2 mt-1 px-1 py-0.5 text-xs rounded ${isDark ? 'bg-gray-800/60' : 'bg-gray-100/80'}`}>
+                          <label className={`flex items-center gap-1 ${isDark ? 'text-gray-400' : 'text-gray-500'}`}>
+                            T:
+                            <input
+                              type="number"
+                              min={0} max={2} step={0.1}
+                              value={chairCfg.temperature ?? selectedNode.data.temperature}
+                              disabled={isReasoningModel(chairCfg.model)}
+                              onChange={(e) => updateChairman('temperature', parseFloat(e.target.value) || 0)}
+                              className={`w-12 border rounded px-1 py-0 text-xs ${isReasoningModel(chairCfg.model) ? 'opacity-50 cursor-not-allowed' : ''} ${isDark ? 'bg-gray-700 border-gray-600 text-gray-200' : 'bg-white border-gray-300'}`}
+                            />
+                          </label>
+                          {isReasoningModel(chairCfg.model) && (
+                            <label className={`flex items-center gap-1 ${isDark ? 'text-gray-400' : 'text-gray-500'}`}>
+                              Effort:
+                              <select
+                                value={chairCfg.reasoningEffort ?? selectedNode.data.reasoningEffort ?? 'medium'}
+                                onChange={(e) => updateChairman('reasoningEffort', e.target.value)}
+                                className={`border rounded px-1 py-0 text-xs ${isDark ? 'bg-gray-700 border-gray-600 text-gray-200' : 'bg-white border-gray-300'}`}
+                              >
+                                <option value="low">Low</option>
+                                <option value="medium">Med</option>
+                                <option value="high">High</option>
+                              </select>
+                            </label>
+                          )}
+                          {supportsWebSearch(chairCfg.model) && (
+                            <label className={`flex items-center gap-1 ${isDark ? 'text-gray-400' : 'text-gray-500'}`}>
+                              <input
+                                type="checkbox"
+                                checked={chairCfg.enableWebSearch ?? (selectedNode.data.enableGoogleSearch !== false)}
+                                onChange={(e) => updateChairman('enableWebSearch', e.target.checked)}
+                                className="w-3 h-3 accent-amber-500"
+                              />
+                              Web
+                            </label>
+                          )}
+                        </div>
+                      );
+                    })()}
                   </div>
                 </div>
               )}
@@ -2098,18 +2483,23 @@ const Sidebar: React.FC<SidebarProps> = ({ isOpen, onToggle, onInteract }) => {
             )}
 
             {selectedNode.data.councilMode ? (
-              <button
-                onClick={handleRunCouncil}
-                disabled={selectedNode.data.status === 'loading' || !activeTracesCheck.complete || (selectedNode.data.councilModels || []).length < 2}
-                className={`w-full py-2 px-4 rounded-md flex items-center justify-center gap-2 transition-colors ${
-                  selectedNode.data.status === 'loading' || !activeTracesCheck.complete || (selectedNode.data.councilModels || []).length < 2
-                    ? 'bg-gray-300 text-gray-500 cursor-not-allowed dark:bg-gray-600 dark:text-gray-400'
-                    : 'bg-amber-600 text-white hover:bg-amber-700'
-                }`}
-              >
-                {selectedNode.data.status === 'loading' ? <Loader2 className="animate-spin" size={16} /> : <Users size={16} />}
-                {selectedNode.data.status === 'loading' && councilStage ? councilStage : `Run Council (${(selectedNode.data.councilModels || []).length})`}
-              </button>
+              <>
+                <button
+                  onClick={handleRunCouncil}
+                  disabled={selectedNode.data.status === 'loading' || !activeTracesCheck.complete || (selectedNode.data.councilModels || []).length < 2}
+                  className={`w-full py-2 px-4 rounded-md flex items-center justify-center gap-2 transition-colors ${
+                    selectedNode.data.status === 'loading' || !activeTracesCheck.complete || (selectedNode.data.councilModels || []).length < 2
+                      ? 'bg-gray-300 text-gray-500 cursor-not-allowed dark:bg-gray-600 dark:text-gray-400'
+                      : 'bg-amber-600 text-white hover:bg-amber-700'
+                  }`}
+                >
+                  {selectedNode.data.status === 'loading' ? <Loader2 className="animate-spin" size={16} /> : <Users size={16} />}
+                  {selectedNode.data.status === 'loading' && councilStage ? councilStage : `Run Council (${(selectedNode.data.councilModels || []).length})`}
+                </button>
+                <div className={`text-center text-[10px] mt-0.5 leading-tight ${isDark ? 'text-gray-600' : 'text-gray-400'}`}>
+                  Inspired by <a href="https://github.com/karpathy/llm-council" target="_blank" rel="noopener noreferrer" className={`underline decoration-dotted ${isDark ? 'text-gray-500 hover:text-gray-400' : 'text-gray-500 hover:text-gray-600'}`}>karpathy/llm-council</a>
+                </div>
+              </>
             ) : (
               <button
                 onClick={handleRun}
@@ -2321,6 +2711,7 @@ const Sidebar: React.FC<SidebarProps> = ({ isOpen, onToggle, onInteract }) => {
                       )}
                     </div>
                   )}
+
                 </div>
               ) : isEditing ? (
                 <div className="space-y-2">
@@ -2950,44 +3341,74 @@ const Sidebar: React.FC<SidebarProps> = ({ isOpen, onToggle, onInteract }) => {
                 )}
                 <span className={`text-xs font-mono ${isDark ? 'text-gray-500' : 'text-gray-400'}`}>#{quickChatTrace.id.slice(-8)}</span>
               </div>
-              <div className="flex items-center gap-3">
-                {/* Model Selector */}
-                <select
-                  value={quickChatModel}
-                  onChange={(e) => setQuickChatModel(e.target.value)}
-                  className={`border rounded-md px-3 py-1.5 text-sm ${
-                    isDark ? 'bg-gray-700 border-gray-600 text-gray-200' : 'border-gray-300 text-gray-900'
+              <div className="flex items-center gap-2">
+                {/* Council Toggle */}
+                <button
+                  onClick={() => {
+                    const next = !quickChatCouncilMode;
+                    setQuickChatCouncilMode(next);
+                    if (next && quickChatCouncilModels.length === 0) {
+                      setQuickChatCouncilConfigOpen(true);
+                    }
+                  }}
+                  className={`p-1.5 rounded transition-colors ${quickChatCouncilMode
+                    ? (isDark ? 'bg-amber-900/60 text-amber-300' : 'bg-amber-100 text-amber-700')
+                    : (isDark ? 'text-gray-400 hover:bg-gray-700' : 'text-gray-500 hover:bg-gray-200')
                   }`}
+                  title={quickChatCouncilMode ? 'Council mode ON' : 'Enable council mode'}
                 >
-                  <optgroup label="Claude">
-                    <option value="claude-sonnet-4-5">claude-sonnet-4.5</option>
-                    <option value="claude-opus-4">claude-opus-4</option>
-                    <option value="claude-opus-4-5">claude-opus-4.5</option>
-                    <option value="claude-opus-4-6">claude-opus-4.6</option>
-                  </optgroup>
-                  <optgroup label="Gemini">
-                    <option value="gemini-2.5-flash">gemini-2.5-flash</option>
-                    <option value="gemini-2.5-flash-lite">gemini-2.5-flash-lite</option>
-                    <option value="gemini-3-pro-preview">gemini-3-pro-preview</option>
-                  </optgroup>
-                  <optgroup label="OpenAI (Standard)">
-                    <option value="gpt-4.1">gpt-4.1</option>
-                    <option value="gpt-4o">gpt-4o</option>
-                  </optgroup>
-                  <optgroup label="OpenAI (Reasoning)">
-                    <option value="gpt-5">gpt-5</option>
-                    <option value="gpt-5-chat-latest">gpt-5-chat-latest</option>
-                    <option value="gpt-5-mini">gpt-5-mini</option>
-                    <option value="gpt-5-nano">gpt-5-nano</option>
-                    <option value="gpt-5-pro" disabled={!canUsePremiumModels}>gpt-5-pro {!canUsePremiumModels && '🔒'}</option>
-                    <option value="gpt-5.1">gpt-5.1</option>
-                    <option value="gpt-5.1-chat-latest">gpt-5.1-chat-latest</option>
-                    <option value="gpt-5.2">gpt-5.2</option>
-                    <option value="gpt-5.2-chat-latest">gpt-5.2-chat-latest</option>
-                    <option value="gpt-5.2-pro" disabled={!canUsePremiumModels}>gpt-5.2-pro {!canUsePremiumModels && '🔒'}</option>
-                    <option value="o3" disabled={!canUsePremiumModels}>o3 {!canUsePremiumModels && '🔒'}</option>
-                  </optgroup>
-                </select>
+                  <Users size={16} />
+                </button>
+                {/* Model Selector — hidden in council mode */}
+                {!quickChatCouncilMode ? (
+                  <select
+                    value={quickChatModel}
+                    onChange={(e) => setQuickChatModel(e.target.value)}
+                    className={`border rounded-md px-3 py-1.5 text-sm ${
+                      isDark ? 'bg-gray-700 border-gray-600 text-gray-200' : 'border-gray-300 text-gray-900'
+                    }`}
+                  >
+                    <optgroup label="Claude">
+                      <option value="claude-sonnet-4-5">claude-sonnet-4.5</option>
+                      <option value="claude-opus-4">claude-opus-4</option>
+                      <option value="claude-opus-4-5">claude-opus-4.5</option>
+                      <option value="claude-opus-4-6">claude-opus-4.6</option>
+                    </optgroup>
+                    <optgroup label="Gemini">
+                      <option value="gemini-2.5-flash">gemini-2.5-flash</option>
+                      <option value="gemini-2.5-flash-lite">gemini-2.5-flash-lite</option>
+                      <option value="gemini-3-pro-preview">gemini-3-pro-preview</option>
+                    </optgroup>
+                    <optgroup label="OpenAI (Standard)">
+                      <option value="gpt-4.1">gpt-4.1</option>
+                      <option value="gpt-4o">gpt-4o</option>
+                    </optgroup>
+                    <optgroup label="OpenAI (Reasoning)">
+                      <option value="gpt-5">gpt-5</option>
+                      <option value="gpt-5-chat-latest">gpt-5-chat-latest</option>
+                      <option value="gpt-5-mini">gpt-5-mini</option>
+                      <option value="gpt-5-nano">gpt-5-nano</option>
+                      <option value="gpt-5-pro" disabled={!canUsePremiumModels}>gpt-5-pro {!canUsePremiumModels && '🔒'}</option>
+                      <option value="gpt-5.1">gpt-5.1</option>
+                      <option value="gpt-5.1-chat-latest">gpt-5.1-chat-latest</option>
+                      <option value="gpt-5.2">gpt-5.2</option>
+                      <option value="gpt-5.2-chat-latest">gpt-5.2-chat-latest</option>
+                      <option value="gpt-5.2-pro" disabled={!canUsePremiumModels}>gpt-5.2-pro {!canUsePremiumModels && '🔒'}</option>
+                      <option value="o3" disabled={!canUsePremiumModels}>o3 {!canUsePremiumModels && '🔒'}</option>
+                    </optgroup>
+                  </select>
+                ) : (
+                  <button
+                    onClick={() => setQuickChatCouncilConfigOpen(!quickChatCouncilConfigOpen)}
+                    className={`border rounded-md px-3 py-1.5 text-sm flex items-center gap-1.5 ${
+                      isDark ? 'bg-gray-700 border-amber-700/50 text-amber-300' : 'border-amber-300 text-amber-700 bg-amber-50'
+                    }`}
+                  >
+                    <Users size={14} />
+                    Council ({quickChatCouncilModels.length})
+                    <ChevronRight size={12} className={`transition-transform ${quickChatCouncilConfigOpen ? 'rotate-90' : ''}`} />
+                  </button>
+                )}
                 <button
                   onClick={() => setRawTextMode(!rawTextMode)}
                   className={`p-1 rounded ${rawTextMode
@@ -3003,6 +3424,64 @@ const Sidebar: React.FC<SidebarProps> = ({ isOpen, onToggle, onInteract }) => {
               </div>
             </div>
 
+            {/* Council Config Panel (collapsible) */}
+            {quickChatCouncilMode && quickChatCouncilConfigOpen && (
+              <div className={`px-4 py-2 border-b ${isDark ? 'border-gray-700 bg-gray-800/50' : 'border-gray-200 bg-amber-50/30'}`}>
+                <div className="grid grid-cols-2 gap-x-3 gap-y-0.5 max-h-36 overflow-y-auto text-xs">
+                  {[
+                    { value: 'claude-sonnet-4-5', label: 'claude-sonnet-4.5' },
+                    { value: 'claude-opus-4', label: 'claude-opus-4' },
+                    { value: 'claude-opus-4-5', label: 'claude-opus-4.5' },
+                    { value: 'claude-opus-4-6', label: 'claude-opus-4.6' },
+                    { value: 'gemini-2.5-flash', label: 'gemini-2.5-flash' },
+                    { value: 'gemini-2.5-flash-lite', label: 'gemini-2.5-flash-lite' },
+                    { value: 'gemini-3-pro-preview', label: 'gemini-3-pro-preview' },
+                    { value: 'gpt-4.1', label: 'gpt-4.1' },
+                    { value: 'gpt-4o', label: 'gpt-4o' },
+                    { value: 'gpt-5', label: 'gpt-5' },
+                    { value: 'gpt-5-mini', label: 'gpt-5-mini' },
+                    { value: 'gpt-5-nano', label: 'gpt-5-nano' },
+                    { value: 'gpt-5.1', label: 'gpt-5.1' },
+                    { value: 'gpt-5.2', label: 'gpt-5.2' },
+                    { value: 'o3', label: 'o3' },
+                  ].map(m => {
+                    const sel = quickChatCouncilModels.some(c => c.model === m.value);
+                    return (
+                      <label key={m.value} className={`flex items-center gap-1 py-0.5 cursor-pointer ${sel
+                        ? isDark ? 'text-amber-300' : 'text-amber-700'
+                        : isDark ? 'text-gray-400' : 'text-gray-600'
+                      }`}>
+                        <input type="checkbox" checked={sel} onChange={() => {
+                          const next = sel
+                            ? quickChatCouncilModels.filter(c => c.model !== m.value)
+                            : [...quickChatCouncilModels, { model: m.value }];
+                          setQuickChatCouncilModels(next);
+                          if (sel && quickChatChairmanModel?.model === m.value && next.length > 0) {
+                            setQuickChatChairmanModel({ model: next[0].model });
+                          }
+                        }} className="w-3 h-3 accent-amber-500" />
+                        {m.label}
+                      </label>
+                    );
+                  })}
+                </div>
+                {quickChatCouncilModels.length > 0 && (
+                  <div className={`mt-1.5 pt-1.5 border-t flex items-center gap-2 ${isDark ? 'border-gray-700' : 'border-gray-200'}`}>
+                    <span className={`text-xs ${isDark ? 'text-gray-400' : 'text-gray-500'}`}>Chairman:</span>
+                    <select
+                      value={quickChatChairmanModel?.model || quickChatCouncilModels[0]?.model || ''}
+                      onChange={(e) => setQuickChatChairmanModel({ model: e.target.value })}
+                      className={`border rounded px-2 py-0.5 text-xs ${isDark ? 'bg-gray-700 border-gray-600 text-gray-200' : 'border-gray-300'}`}
+                    >
+                      {quickChatCouncilModels.map(c => (
+                        <option key={c.model} value={c.model}>{c.model}</option>
+                      ))}
+                    </select>
+                  </div>
+                )}
+              </div>
+            )}
+
             {/* Chat Messages */}
             <div className={`flex-1 overflow-y-auto p-4 space-y-4 ${isDark ? 'bg-gray-900' : 'bg-gray-50'}`}>
               {quickChatMessages.length === 0 ? (
@@ -3102,10 +3581,13 @@ const Sidebar: React.FC<SidebarProps> = ({ isOpen, onToggle, onInteract }) => {
               )}
               {quickChatLoading && (
                 <div className="flex justify-start">
-                  <div className={`rounded-lg px-4 py-3 shadow-sm ${
+                  <div className={`rounded-lg px-4 py-3 shadow-sm flex items-center gap-2 ${
                     isDark ? 'bg-gray-800 border border-gray-700' : 'bg-white border border-gray-200'
                   }`}>
-                    <Loader2 className="animate-spin text-blue-500" size={20} />
+                    <Loader2 className={`animate-spin ${quickChatCouncilMode ? 'text-amber-500' : 'text-blue-500'}`} size={20} />
+                    {quickChatCouncilStage && (
+                      <span className={`text-xs ${isDark ? 'text-amber-400' : 'text-amber-600'}`}>{quickChatCouncilStage}</span>
+                    )}
                   </div>
                 </div>
               )}
@@ -3116,56 +3598,65 @@ const Sidebar: React.FC<SidebarProps> = ({ isOpen, onToggle, onInteract }) => {
             <div className={`px-4 py-2 border-t flex items-center gap-4 text-xs ${
               isDark ? 'border-gray-700 bg-gray-800' : 'border-gray-100 bg-white'
             }`}>
-              {/* Temperature (hide for reasoning models) */}
-              {!['gpt-5', 'gpt-5-chat-latest', 'gpt-5-mini', 'gpt-5-nano', 'gpt-5-pro', 'gpt-5.1', 'gpt-5.1-chat-latest', 'gpt-5.2', 'gpt-5.2-chat-latest', 'gpt-5.2-pro', 'o3'].includes(quickChatModel) && (
-                <div className="flex items-center gap-2">
-                  <span className={isDark ? 'text-gray-400' : 'text-gray-500'}>Temp:</span>
-                  <input
-                    type="range"
-                    min="0"
-                    max="2"
-                    step="0.1"
-                    value={quickChatTemp}
-                    onChange={(e) => setQuickChatTemp(parseFloat(e.target.value))}
-                    className="w-20"
-                  />
-                  <span className={`w-8 ${isDark ? 'text-gray-300' : 'text-gray-700'}`}>{quickChatTemp}</span>
-                </div>
-              )}
-              
-              {/* Reasoning Effort (only for reasoning models, except chat-latest) */}
-              {['gpt-5', 'gpt-5-mini', 'gpt-5-nano', 'gpt-5-pro', 'gpt-5.1', 'gpt-5.2', 'gpt-5.2-pro', 'o3'].includes(quickChatModel) && (
-                <div className="flex items-center gap-2">
-                  <span className={isDark ? 'text-gray-400' : 'text-gray-500'}>Effort:</span>
-                  <select
-                    value={quickChatEffort}
-                    onChange={(e) => setQuickChatEffort(e.target.value as 'low' | 'medium' | 'high')}
-                    className={`border rounded px-2 py-0.5 text-xs ${
-                      isDark ? 'bg-gray-700 border-gray-600 text-gray-200' : 'border-gray-300'
-                    }`}
-                  >
-                    <option value="low">Low</option>
-                    <option value="medium">Medium</option>
-                    <option value="high">High</option>
-                  </select>
-                </div>
-              )}
-              
-              {/* Web Search */}
-              {(quickChatModel.startsWith('gemini') || quickChatModel.startsWith('gpt-5') || ['o3', 'gpt-4o'].includes(quickChatModel)) && (
-                <label className="flex items-center gap-1 cursor-pointer">
-                  <input
-                    type="checkbox"
-                    checked={quickChatWebSearch}
-                    onChange={(e) => setQuickChatWebSearch(e.target.checked)}
-                    className="form-checkbox h-3 w-3"
-                  />
-                  <span className={isDark ? 'text-gray-400' : 'text-gray-500'}>Web Search</span>
-                </label>
+              {quickChatCouncilMode ? (
+                <span className={`flex items-center gap-1.5 ${isDark ? 'text-amber-400' : 'text-amber-600'}`}>
+                  <Users size={12} />
+                  {quickChatCouncilModels.length} models &middot; Chairman: {quickChatChairmanModel?.model || '—'}
+                </span>
+              ) : (
+                <>
+                  {/* Temperature (hide for reasoning models) */}
+                  {!['gpt-5', 'gpt-5-chat-latest', 'gpt-5-mini', 'gpt-5-nano', 'gpt-5-pro', 'gpt-5.1', 'gpt-5.1-chat-latest', 'gpt-5.2', 'gpt-5.2-chat-latest', 'gpt-5.2-pro', 'o3'].includes(quickChatModel) && (
+                    <div className="flex items-center gap-2">
+                      <span className={isDark ? 'text-gray-400' : 'text-gray-500'}>Temp:</span>
+                      <input
+                        type="range"
+                        min="0"
+                        max="2"
+                        step="0.1"
+                        value={quickChatTemp}
+                        onChange={(e) => setQuickChatTemp(parseFloat(e.target.value))}
+                        className="w-20"
+                      />
+                      <span className={`w-8 ${isDark ? 'text-gray-300' : 'text-gray-700'}`}>{quickChatTemp}</span>
+                    </div>
+                  )}
+
+                  {/* Reasoning Effort (only for reasoning models, except chat-latest) */}
+                  {['gpt-5', 'gpt-5-mini', 'gpt-5-nano', 'gpt-5-pro', 'gpt-5.1', 'gpt-5.2', 'gpt-5.2-pro', 'o3'].includes(quickChatModel) && (
+                    <div className="flex items-center gap-2">
+                      <span className={isDark ? 'text-gray-400' : 'text-gray-500'}>Effort:</span>
+                      <select
+                        value={quickChatEffort}
+                        onChange={(e) => setQuickChatEffort(e.target.value as 'low' | 'medium' | 'high')}
+                        className={`border rounded px-2 py-0.5 text-xs ${
+                          isDark ? 'bg-gray-700 border-gray-600 text-gray-200' : 'border-gray-300'
+                        }`}
+                      >
+                        <option value="low">Low</option>
+                        <option value="medium">Medium</option>
+                        <option value="high">High</option>
+                      </select>
+                    </div>
+                  )}
+
+                  {/* Web Search */}
+                  {(quickChatModel.startsWith('gemini') || quickChatModel.startsWith('gpt-5') || ['o3', 'gpt-4o'].includes(quickChatModel)) && (
+                    <label className="flex items-center gap-1 cursor-pointer">
+                      <input
+                        type="checkbox"
+                        checked={quickChatWebSearch}
+                        onChange={(e) => setQuickChatWebSearch(e.target.checked)}
+                        className="form-checkbox h-3 w-3"
+                      />
+                      <span className={isDark ? 'text-gray-400' : 'text-gray-500'}>Web Search</span>
+                    </label>
+                  )}
+                </>
               )}
-              
+
               {/* File Attachment Buttons - Hidden for Gemini */}
-              {!quickChatModel.startsWith('gemini') && (
+              {!quickChatModel.startsWith('gemini') && !quickChatCouncilMode && (
               <div className="flex items-center gap-1 ml-auto">
                 <button
                   onClick={() => quickChatUploadRef.current?.click()}
@@ -3175,8 +3666,8 @@ const Sidebar: React.FC<SidebarProps> = ({ isOpen, onToggle, onInteract }) => {
                       ? 'opacity-50 cursor-not-allowed'
                       : ''
                   } ${
-                    isDark 
-                      ? 'bg-gray-700 hover:bg-gray-600 text-gray-300' 
+                    isDark
+                      ? 'bg-gray-700 hover:bg-gray-600 text-gray-300'
                       : 'bg-gray-100 hover:bg-gray-200 text-gray-700'
                   }`}
                   title={quickChatUploading ? "Uploading..." : "Upload & Attach"}
@@ -3190,8 +3681,8 @@ const Sidebar: React.FC<SidebarProps> = ({ isOpen, onToggle, onInteract }) => {
                     setShowQuickChatAttachModal(true);
                   }}
                   className={`px-2 py-0.5 rounded text-xs flex items-center gap-1 ${
-                    isDark 
-                      ? 'bg-gray-700 hover:bg-gray-600 text-gray-300' 
+                    isDark
+                      ? 'bg-gray-700 hover:bg-gray-600 text-gray-300'
                       : 'bg-gray-100 hover:bg-gray-200 text-gray-700'
                   }`}
                   title="Attach Existing File"
@@ -3272,8 +3763,12 @@ const Sidebar: React.FC<SidebarProps> = ({ isOpen, onToggle, onInteract }) => {
                 />
                 <button
                   onClick={handleQuickChatSend}
-                  disabled={(!quickChatInput.trim() && quickChatAttachedFiles.length === 0) || quickChatLoading}
-                  className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:bg-blue-300 disabled:cursor-not-allowed flex items-center gap-2"
+                  disabled={(!quickChatInput.trim() && quickChatAttachedFiles.length === 0) || quickChatLoading || (quickChatCouncilMode && quickChatCouncilModels.length < 2)}
+                  className={`px-4 py-2 text-white rounded-lg disabled:cursor-not-allowed flex items-center gap-2 ${
+                    quickChatCouncilMode
+                      ? 'bg-amber-600 hover:bg-amber-700 disabled:bg-amber-300'
+                      : 'bg-blue-600 hover:bg-blue-700 disabled:bg-blue-300'
+                  }`}
                 >
                   {quickChatLoading ? <Loader2 className="animate-spin" size={18} /> : <Send size={18} />}
                 </button>
-- 
cgit v1.2.3

