@@ -2464,6 +2977,8 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }) {
loginProvider === 'claude' ? claudeAuthStatus.authenticated :
loginProvider === 'cursor' ? cursorAuthStatus.authenticated :
loginProvider === 'codex' ? codexAuthStatus.authenticated :
+ loginProvider === 'gemini' ? geminiAuthStatus.authenticated :
+ loginProvider === 'copilot' ? copilotAuthStatus.authenticated :
false
}
cliAvailable={
@@ -2471,6 +2986,7 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }) {
loginProvider === 'cursor' ? cursorAuthStatus.cliAvailable !== false :
loginProvider === 'codex' ? codexAuthStatus.cliAvailable !== false :
loginProvider === 'gemini' ? geminiAuthStatus.cliAvailable !== false :
+ loginProvider === 'copilot' ? copilotAuthStatus.cliAvailable !== false :
true
}
installHint={
@@ -2478,6 +2994,7 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }) {
loginProvider === 'cursor' ? cursorAuthStatus.installHint :
loginProvider === 'codex' ? codexAuthStatus.installHint :
loginProvider === 'gemini' ? geminiAuthStatus.installHint :
+ loginProvider === 'copilot' ? copilotAuthStatus.installHint :
null
}
installable={
@@ -2485,6 +3002,7 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }) {
loginProvider === 'cursor' ? cursorAuthStatus.installable === true :
loginProvider === 'codex' ? codexAuthStatus.installable === true :
loginProvider === 'gemini' ? geminiAuthStatus.installable === true :
+ loginProvider === 'copilot' ? copilotAuthStatus.installable === true :
false
}
installerAvailable={
@@ -2492,6 +3010,7 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }) {
loginProvider === 'cursor' ? cursorAuthStatus.installerAvailable !== false :
loginProvider === 'codex' ? codexAuthStatus.installerAvailable !== false :
loginProvider === 'gemini' ? geminiAuthStatus.installerAvailable !== false :
+ loginProvider === 'copilot' ? copilotAuthStatus.installerAvailable !== false :
true
}
installerHint={
@@ -2499,6 +3018,7 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }) {
loginProvider === 'cursor' ? cursorAuthStatus.installerHint :
loginProvider === 'codex' ? codexAuthStatus.installerHint :
loginProvider === 'gemini' ? geminiAuthStatus.installerHint :
+ loginProvider === 'copilot' ? copilotAuthStatus.installerHint :
null
}
docsUrl={
@@ -2506,6 +3026,7 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }) {
loginProvider === 'cursor' ? cursorAuthStatus.docsUrl :
loginProvider === 'codex' ? codexAuthStatus.docsUrl :
loginProvider === 'gemini' ? geminiAuthStatus.docsUrl :
+ loginProvider === 'copilot' ? copilotAuthStatus.docsUrl :
null
}
downloadUrl={
@@ -2513,6 +3034,7 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }) {
loginProvider === 'cursor' ? cursorAuthStatus.downloadUrl :
loginProvider === 'codex' ? codexAuthStatus.downloadUrl :
loginProvider === 'gemini' ? geminiAuthStatus.downloadUrl :
+ loginProvider === 'copilot' ? copilotAuthStatus.downloadUrl :
null
}
onStatusRefresh={() => refreshProviderStatus(loginProvider)}
diff --git a/src/components/Shell.jsx b/src/components/Shell.jsx
index 34273c1b..2bca503e 100644
--- a/src/components/Shell.jsx
+++ b/src/components/Shell.jsx
@@ -6,7 +6,7 @@ import { WebLinksAddon } from '@xterm/addon-web-links';
import '@xterm/xterm/css/xterm.css';
import { useTranslation } from 'react-i18next';
import { IS_PLATFORM } from '../constants/config';
-import { stripInternalContextPrefix } from '../utils/sessionFormatting';
+import { getSessionDisplayTitle } from '../utils/sessionFormatting';
const xtermStyles = `
.xterm .xterm-screen {
@@ -64,7 +64,7 @@ function getPreferredProvider(selectedSession) {
}
const storedProvider = localStorage.getItem('selected-provider');
- if (storedProvider === 'cursor' || storedProvider === 'codex' || storedProvider === 'claude' || storedProvider === 'gemini' || storedProvider === 'openrouter' || storedProvider === 'local' || storedProvider === 'nano') {
+ if (storedProvider === 'cursor' || storedProvider === 'codex' || storedProvider === 'claude' || storedProvider === 'gemini' || storedProvider === 'copilot' || storedProvider === 'openrouter' || storedProvider === 'local' || storedProvider === 'nano') {
return storedProvider;
}
@@ -311,10 +311,7 @@ function Shell({ selectedProject, selectedSession, initialCommand, isPlainShell
const sessionDisplayName = useMemo(() => {
if (!selectedSession) return null;
- const rawName = selectedSession.__provider === 'cursor'
- ? (selectedSession.name || 'Untitled Session')
- : (selectedSession.summary || 'New Session');
- return stripInternalContextPrefix(rawName) || 'New Session';
+ return getSessionDisplayTitle(selectedSession);
}, [selectedSession]);
const sessionDisplayNameShort = useMemo(() => {
diff --git a/src/components/chat/hooks/useChatComposerState.ts b/src/components/chat/hooks/useChatComposerState.ts
index 4bef1391..25ed7304 100644
--- a/src/components/chat/hooks/useChatComposerState.ts
+++ b/src/components/chat/hooks/useChatComposerState.ts
@@ -70,6 +70,7 @@ interface UseChatComposerStateArgs {
claudeModel: string;
codexModel: string;
geminiModel: string;
+ copilotModel: string;
openrouterModel: string;
localModel: string;
nanoModel: string;
@@ -249,6 +250,7 @@ export function useChatComposerState({
claudeModel,
codexModel,
geminiModel,
+ copilotModel,
openrouterModel,
localModel,
nanoModel,
@@ -1391,6 +1393,26 @@ export function useChatComposerState({
stageTagSource: 'task_context',
},
});
+ } else if (provider === 'copilot') {
+ console.log('[DEBUG] Sending copilot-command');
+ sendMessage({
+ type: 'copilot-command',
+ command: messageContent,
+ sessionId: effectiveSessionId,
+ options: {
+ cwd: resolvedProjectPath,
+ projectPath: resolvedProjectPath,
+ sessionId: effectiveSessionId,
+ resume: Boolean(effectiveSessionId),
+ permissionMode: effectivePermissionMode,
+ model: copilotModel,
+ toolsSettings,
+ telemetryEnabled,
+ sessionMode: isNewSession ? newSessionMode : selectedSession?.mode,
+ stageTagKeys: currentStageTagKeys,
+ stageTagSource: 'task_context',
+ },
+ });
} else if (provider === 'codex') {
console.log('[DEBUG] Sending codex-command');
sendMessage({
@@ -1528,6 +1550,7 @@ export function useChatComposerState({
executeCommand,
geminiThinkingMode,
geminiModel,
+ copilotModel,
openrouterModel,
localModel,
nanoModel,
diff --git a/src/components/chat/hooks/useChatProviderState.ts b/src/components/chat/hooks/useChatProviderState.ts
index 2e6dea38..4793797e 100644
--- a/src/components/chat/hooks/useChatProviderState.ts
+++ b/src/components/chat/hooks/useChatProviderState.ts
@@ -1,6 +1,6 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { authenticatedFetch } from '../../../utils/api';
-import { CLAUDE_MODELS, CODEX_MODELS, CURSOR_MODELS, GEMINI_MODELS, LOCAL_MODELS, NANO_CLAUDE_CODE_MODELS, OPENROUTER_MODELS } from '../../../../shared/modelConstants';
+import { CLAUDE_MODELS, CODEX_MODELS, CURSOR_MODELS, GEMINI_MODELS, GITHUB_COPILOT_MODELS, LOCAL_MODELS, NANO_CLAUDE_CODE_MODELS, OPENROUTER_MODELS } from '../../../../shared/modelConstants';
import type { PendingPermissionRequest, PermissionMode, Provider } from '../types/types';
import type { ProjectSession, SessionProvider } from '../../../types/app';
@@ -26,6 +26,9 @@ export function useChatProviderState({ selectedSession }: UseChatProviderStateAr
const [geminiModel, setGeminiModel] = useState
(() => {
return localStorage.getItem('gemini-model') || GEMINI_MODELS.DEFAULT;
});
+ const [copilotModel, setCopilotModel] = useState(() => {
+ return localStorage.getItem('copilot-model') || GITHUB_COPILOT_MODELS.DEFAULT;
+ });
const [openrouterModel, setOpenrouterModel] = useState(() => {
return localStorage.getItem('openrouter-model') || OPENROUTER_MODELS.DEFAULT;
});
@@ -139,6 +142,8 @@ export function useChatProviderState({ selectedSession }: UseChatProviderStateAr
setCodexModel,
geminiModel,
setGeminiModel,
+ copilotModel,
+ setCopilotModel,
openrouterModel,
setOpenrouterModel,
localModel,
diff --git a/src/components/chat/hooks/useChatRealtimeHandlers.ts b/src/components/chat/hooks/useChatRealtimeHandlers.ts
index 56915dfc..8a6437de 100644
--- a/src/components/chat/hooks/useChatRealtimeHandlers.ts
+++ b/src/components/chat/hooks/useChatRealtimeHandlers.ts
@@ -423,6 +423,7 @@ export function useChatRealtimeHandlers({
const isGlobalMessage = globalMessageTypes.includes(String(latestMessage.type));
const lifecycleMessageTypes = new Set([
'claude-complete',
+ 'copilot-complete',
'codex-complete',
'gemini-complete',
'openrouter-complete',
@@ -430,6 +431,7 @@ export function useChatRealtimeHandlers({
'cursor-result',
'session-aborted',
'claude-error',
+ 'copilot-error',
'cursor-error',
'codex-error',
'gemini-error',
@@ -472,6 +474,7 @@ export function useChatRealtimeHandlers({
pendingViewSessionRef.current &&
!pendingViewSessionRef.current.sessionId &&
(latestMessage.type === 'claude-error' ||
+ latestMessage.type === 'copilot-error' ||
latestMessage.type === 'cursor-error' ||
latestMessage.type === 'codex-error' ||
latestMessage.type === 'gemini-error');
@@ -605,7 +608,8 @@ export function useChatRealtimeHandlers({
}
break;
- case 'claude-response': {
+ case 'claude-response':
+ case 'copilot-response': {
if (messageData && typeof messageData === 'object' && messageData.type) {
if (Number.isFinite(messageData.startTime)) {
persistStartTime(messageData.startTime, latestMessage.sessionId, currentSessionId, selectedSession?.id);
@@ -815,6 +819,7 @@ export function useChatRealtimeHandlers({
}
case 'claude-complete':
+ case 'copilot-complete':
case 'gemini-complete':
case 'openrouter-complete':
case 'localgpu-complete': {
@@ -835,6 +840,7 @@ export function useChatRealtimeHandlers({
}
case 'claude-error':
+ case 'copilot-error':
case 'gemini-error':
case 'openrouter-error':
case 'localgpu-error': {
@@ -1419,6 +1425,7 @@ export function useChatRealtimeHandlers({
}
case 'claude-status':
+ case 'copilot-status':
case 'gemini-status': {
const statusData = latestMessage.data;
if (!statusData) break;
diff --git a/src/components/chat/hooks/useChatSessionState.ts b/src/components/chat/hooks/useChatSessionState.ts
index 2b90c967..5ab4d701 100644
--- a/src/components/chat/hooks/useChatSessionState.ts
+++ b/src/components/chat/hooks/useChatSessionState.ts
@@ -59,6 +59,7 @@ function resolveSessionProviderForLoad(session: ProjectSession | null, project:
if (project.nanoSessions?.some((s) => s.id === id)) return 'nano';
if (project.localSessions?.some((s) => s.id === id)) return 'local';
if (project.openrouterSessions?.some((s) => s.id === id)) return 'openrouter';
+ if (project.copilotSessions?.some((s) => s.id === id)) return 'copilot';
if (project.geminiSessions?.some((s) => s.id === id)) return 'gemini';
if (project.codexSessions?.some((s) => s.id === id)) return 'codex';
if (project.cursorSessions?.some((s) => s.id === id)) return 'cursor';
@@ -472,9 +473,12 @@ export function useChatSessionState({
setTokenBudget(null);
// Only set isLoading to false if it's NOT in the processingSessions set
+ // and there is no persisted in-flight timer carried over from a temporary session.
+ const hasPersistedInFlightTimer = Boolean(readSessionTimerStart(selectedSession.id));
const isProcessing =
processingSessions?.has(selectedSession.id) ||
- pendingStatusValidationSessionIdRef.current === selectedSession.id;
+ pendingStatusValidationSessionIdRef.current === selectedSession.id ||
+ hasPersistedInFlightTimer;
if (!isProcessing) {
setIsLoading(false);
}
diff --git a/src/components/chat/utils/chatFormatting.ts b/src/components/chat/utils/chatFormatting.ts
index c838d509..39c717d1 100644
--- a/src/components/chat/utils/chatFormatting.ts
+++ b/src/components/chat/utils/chatFormatting.ts
@@ -185,6 +185,7 @@ export function getProviderDisplayName(provider: string): string {
if (provider === 'cursor') return 'Cursor';
if (provider === 'codex') return 'Codex';
if (provider === 'gemini') return 'Gemini';
+ if (provider === 'copilot') return 'GitHub Copilot';
if (provider === 'openrouter') {
const slug = localStorage.getItem('openrouter-model') || '';
if (slug) {
diff --git a/src/components/chat/utils/chatStorage.ts b/src/components/chat/utils/chatStorage.ts
index f21c79bb..7e714ee2 100644
--- a/src/components/chat/utils/chatStorage.ts
+++ b/src/components/chat/utils/chatStorage.ts
@@ -4,6 +4,7 @@ export const CLAUDE_SETTINGS_KEY = 'claude-settings';
export const GEMINI_SETTINGS_KEY = 'gemini-settings';
export const CURSOR_SETTINGS_KEY = 'cursor-tools-settings';
export const CODEX_SETTINGS_KEY = 'codex-settings';
+export const COPILOT_SETTINGS_KEY = 'copilot-settings';
export const NANO_SETTINGS_KEY = 'nano-claude-code-settings';
const SESSION_TIMER_PREFIX = 'session_timer_start_';
@@ -37,6 +38,7 @@ export function getProviderSettingsKey(provider?: string) {
case 'gemini': return GEMINI_SETTINGS_KEY;
case 'cursor': return CURSOR_SETTINGS_KEY;
case 'codex': return CODEX_SETTINGS_KEY;
+ case 'copilot': return COPILOT_SETTINGS_KEY;
case 'nano': return NANO_SETTINGS_KEY;
default: return CLAUDE_SETTINGS_KEY;
}
diff --git a/src/components/chat/view/ChatInterface.tsx b/src/components/chat/view/ChatInterface.tsx
index 7e9890f7..bf545fe2 100644
--- a/src/components/chat/view/ChatInterface.tsx
+++ b/src/components/chat/view/ChatInterface.tsx
@@ -24,7 +24,7 @@ import { readCliAvailability, writeCliAvailability } from '../../../utils/cliAva
import { Button } from '../../ui/button';
import type { PendingAutoIntake } from '../../../types/app';
import type { EditingFile, DiffInfo } from '../../main-content/types/types';
-import { CLAUDE_MODELS, CURSOR_MODELS, CODEX_MODELS, GEMINI_MODELS, LOCAL_MODELS, NANO_CLAUDE_CODE_MODELS, OPENROUTER_MODELS } from '../../../../shared/modelConstants';
+import { CLAUDE_MODELS, CURSOR_MODELS, CODEX_MODELS, GEMINI_MODELS, GITHUB_COPILOT_MODELS, LOCAL_MODELS, NANO_CLAUDE_CODE_MODELS, OPENROUTER_MODELS } from '../../../../shared/modelConstants';
import { getProviderDisplayName } from '../utils/chatFormatting';
import { buildEditableMessageDraft, buildReplayMessageDraft, getChatMessageId, getMessageReplayContent } from '../utils/chatMessages';
import { normalizePath, toRelativePath, isSafePath, fileNameFromPath } from '../../../utils/pathUtils';
@@ -35,6 +35,7 @@ const DEFAULT_PROVIDER_AVAILABILITY: Record = {
cursor: { cliAvailable: true, cliCommand: 'agent', installHint: null },
codex: { cliAvailable: true, cliCommand: 'codex', installHint: null },
gemini: { cliAvailable: true, cliCommand: 'gemini', installHint: null },
+ copilot: { cliAvailable: true, cliCommand: 'copilot', installHint: null },
openrouter: { cliAvailable: true, cliCommand: 'openrouter', installHint: null },
local: { cliAvailable: true, cliCommand: null, installHint: null },
nano: { cliAvailable: true, cliCommand: 'nano-claude-code', installHint: null },
@@ -68,6 +69,7 @@ const getProviderModelConfig = (provider: Provider) => {
if (provider === 'claude') return CLAUDE_MODELS;
if (provider === 'codex') return CODEX_MODELS;
if (provider === 'gemini') return GEMINI_MODELS;
+ if (provider === 'copilot') return GITHUB_COPILOT_MODELS;
if (provider === 'openrouter') return OPENROUTER_MODELS;
if (provider === 'local') return LOCAL_MODELS;
if (provider === 'nano') return NANO_CLAUDE_CODE_MODELS;
@@ -174,6 +176,8 @@ function ChatInterface({
setCodexModel,
geminiModel,
setGeminiModel,
+ copilotModel,
+ setCopilotModel,
openrouterModel,
setOpenrouterModel,
localModel,
@@ -312,6 +316,7 @@ function ChatInterface({
claudeModel,
codexModel,
geminiModel,
+ copilotModel,
openrouterModel,
localModel,
nanoModel,
@@ -446,6 +451,7 @@ function ChatInterface({
cursor: cached.cursor ?? DEFAULT_PROVIDER_AVAILABILITY.cursor,
codex: cached.codex ?? DEFAULT_PROVIDER_AVAILABILITY.codex,
gemini: cached.gemini ?? DEFAULT_PROVIDER_AVAILABILITY.gemini,
+ copilot: cached.copilot ?? DEFAULT_PROVIDER_AVAILABILITY.copilot,
openrouter: cached.openrouter ?? DEFAULT_PROVIDER_AVAILABILITY.openrouter,
local: cached.local ?? DEFAULT_PROVIDER_AVAILABILITY.local,
nano: cached.nano ?? DEFAULT_PROVIDER_AVAILABILITY.nano,
@@ -456,11 +462,12 @@ function ChatInterface({
if (importedProjectAnalysisProvider === 'claude') return claudeModel;
if (importedProjectAnalysisProvider === 'codex') return codexModel;
if (importedProjectAnalysisProvider === 'gemini') return geminiModel;
+ if (importedProjectAnalysisProvider === 'copilot') return copilotModel;
if (importedProjectAnalysisProvider === 'openrouter') return openrouterModel;
if (importedProjectAnalysisProvider === 'local') return localModel;
if (importedProjectAnalysisProvider === 'nano') return nanoModel;
return cursorModel;
- }, [claudeModel, codexModel, cursorModel, geminiModel, openrouterModel, localModel, nanoModel, importedProjectAnalysisProvider]);
+ }, [claudeModel, codexModel, cursorModel, geminiModel, copilotModel, openrouterModel, localModel, nanoModel, importedProjectAnalysisProvider]);
const handleStartTaskInChat = useCallback((prompt?: string, task?: { stage?: string } | null) => {
const nextPrompt = prompt && prompt.trim()
@@ -480,6 +487,7 @@ function ChatInterface({
{ provider: 'cursor', endpoint: '/api/cli/cursor/status', fallbackCommand: 'agent' },
{ provider: 'codex', endpoint: '/api/cli/codex/status', fallbackCommand: 'codex' },
{ provider: 'gemini', endpoint: '/api/cli/gemini/status', fallbackCommand: 'gemini' },
+ { provider: 'copilot', endpoint: '/api/cli/copilot/status', fallbackCommand: 'copilot' },
{ provider: 'openrouter', endpoint: '/api/cli/openrouter/status', fallbackCommand: 'openrouter' },
{ provider: 'nano', endpoint: '/api/cli/nano/status', fallbackCommand: 'nano-claude-code' },
];
@@ -527,7 +535,7 @@ function ChatInterface({
useEffect(() => {
if (providerAvailability[provider]?.cliAvailable === false) {
- const fallbackProvider = (['claude', 'cursor', 'codex', 'gemini', 'openrouter', 'local', 'nano'] as const).find(
+ const fallbackProvider = (['claude', 'cursor', 'codex', 'gemini', 'copilot', 'openrouter', 'local', 'nano'] as const).find(
(candidate) => providerAvailability[candidate]?.cliAvailable !== false,
);
@@ -680,6 +688,7 @@ function ChatInterface({
if (
latestMessage.type === 'claude-complete' ||
+ latestMessage.type === 'copilot-complete' ||
latestMessage.type === 'cursor-result' ||
latestMessage.type === 'codex-complete'
) {
@@ -981,6 +990,8 @@ function ChatInterface({
setCursorModel={setCursorModel}
setCodexModel={setCodexModel}
setGeminiModel={setGeminiModel}
+ copilotModel={copilotModel}
+ setCopilotModel={setCopilotModel}
openrouterModel={openrouterModel}
setOpenrouterModel={setOpenrouterModel}
localModel={localModel}
diff --git a/src/components/chat/view/subcomponents/ChatComposer.tsx b/src/components/chat/view/subcomponents/ChatComposer.tsx
index f4c087b8..68922f10 100644
--- a/src/components/chat/view/subcomponents/ChatComposer.tsx
+++ b/src/components/chat/view/subcomponents/ChatComposer.tsx
@@ -27,7 +27,7 @@ import type { GeminiThinkingModeId } from '../../../../../shared/geminiThinkingS
import type { AttachedPrompt, PendingPermissionRequest, PermissionMode, Provider, TokenBudget } from '../../types/types';
import type { ProviderAvailability } from '../../types/types';
import type { SessionMode, SessionProvider } from '../../../../types/app';
-import { CLAUDE_MODELS, CURSOR_MODELS, CODEX_MODELS, GEMINI_MODELS, LOCAL_MODELS, NANO_CLAUDE_CODE_MODELS, OPENROUTER_MODELS } from '../../../../../shared/modelConstants';
+import { CLAUDE_MODELS, CURSOR_MODELS, CODEX_MODELS, GEMINI_MODELS, GITHUB_COPILOT_MODELS, LOCAL_MODELS, NANO_CLAUDE_CODE_MODELS, OPENROUTER_MODELS } from '../../../../../shared/modelConstants';
import { authenticatedFetch } from '../../../../utils/api';
import { isAutoResearchScenario } from '../../utils/autoResearch';
@@ -61,6 +61,7 @@ interface SlashCommand {
const PROVIDERS: ProviderDef[] = [
{ id: 'claude', name: 'Claude Code', accent: 'border-primary', ring: 'ring-primary/15', check: 'bg-primary text-primary-foreground' },
{ id: 'gemini', name: 'Gemini CLI', accent: 'border-blue-500 dark:border-blue-400', ring: 'ring-blue-500/15', check: 'bg-blue-500 text-white' },
+ { id: 'copilot', name: 'GitHub Copilot CLI', accent: 'border-slate-500 dark:border-slate-400', ring: 'ring-slate-500/15', check: 'bg-slate-700 text-white' },
{ id: 'codex', name: 'Codex', accent: 'border-emerald-600 dark:border-emerald-400', ring: 'ring-emerald-600/15', check: 'bg-emerald-600 dark:bg-emerald-500 text-white' },
{ id: 'openrouter', name: 'OpenRouter', accent: 'border-violet-500 dark:border-violet-400', ring: 'ring-violet-500/15', check: 'bg-violet-500 text-white' },
{ id: 'local', name: 'Local GPU', accent: 'border-emerald-500 dark:border-emerald-400', ring: 'ring-emerald-500/15', check: 'bg-emerald-500 text-white' },
@@ -71,16 +72,18 @@ function getModelConfig(p: SessionProvider) {
if (p === 'claude') return CLAUDE_MODELS;
if (p === 'codex') return CODEX_MODELS;
if (p === 'gemini') return GEMINI_MODELS;
+ if (p === 'copilot') return GITHUB_COPILOT_MODELS;
if (p === 'openrouter') return OPENROUTER_MODELS;
if (p === 'local') return LOCAL_MODELS;
if (p === 'nano') return NANO_CLAUDE_CODE_MODELS;
return CURSOR_MODELS;
}
-function getModelValue(p: SessionProvider, c: string, cu: string, co: string, g: string, or: string, lo: string, na: string) {
+function getModelValue(p: SessionProvider, c: string, cu: string, co: string, g: string, cp: string, or: string, lo: string, na: string) {
if (p === 'claude') return c;
if (p === 'codex') return co;
if (p === 'gemini') return g;
+ if (p === 'copilot') return cp;
if (p === 'openrouter') return or;
if (p === 'local') return lo;
if (p === 'nano') return na;
@@ -102,6 +105,7 @@ interface ChatComposerProps {
onModeSwitch: () => void;
codexModel: string;
geminiModel: string;
+ copilotModel?: string;
thinkingMode: string;
setThinkingMode: Dispatch>;
codexReasoningEffort: CodexReasoningEffortId;
@@ -166,6 +170,7 @@ interface ChatComposerProps {
setCursorModel?: (model: string) => void;
setCodexModel?: (model: string) => void;
setGeminiModel?: (model: string) => void;
+ setCopilotModel?: (model: string) => void;
openrouterModel?: string;
setOpenrouterModel?: (model: string) => void;
localModel?: string;
@@ -189,6 +194,7 @@ export default function ChatComposer({
onModeSwitch,
codexModel,
geminiModel,
+ copilotModel,
thinkingMode,
setThinkingMode,
codexReasoningEffort,
@@ -253,6 +259,7 @@ export default function ChatComposer({
setCursorModel,
setCodexModel,
setGeminiModel,
+ setCopilotModel,
openrouterModel: openrouterModelProp,
setOpenrouterModel,
localModel: localModelProp,
@@ -285,7 +292,7 @@ export default function ChatComposer({
// Provider/model handling for centered mode
const sessionProvider = provider as SessionProvider;
- const currentModel = getModelValue(sessionProvider, claudeModelProp || '', cursorModelProp || '', codexModel, geminiModel, openrouterModelProp || '', localModelProp || '', nanoModelProp || '');
+ const currentModel = getModelValue(sessionProvider, claudeModelProp || '', cursorModelProp || '', codexModel, geminiModel, copilotModel || '', openrouterModelProp || '', localModelProp || '', nanoModelProp || '');
const [ollamaModels, setOllamaModels] = useState>([]);
const [isLoadingOllamaModels, setIsLoadingOllamaModels] = useState(false);
@@ -352,6 +359,7 @@ export default function ChatComposer({
if (sessionProvider === 'claude') { setClaudeModel?.(value); localStorage.setItem('claude-model', value); }
else if (sessionProvider === 'codex') { setCodexModel?.(value); localStorage.setItem('codex-model', value); }
else if (sessionProvider === 'gemini') { setGeminiModel?.(value); localStorage.setItem('gemini-model', value); }
+ else if (sessionProvider === 'copilot') { setCopilotModel?.(value); localStorage.setItem('copilot-model', value); }
else if (sessionProvider === 'openrouter') { setOpenrouterModel?.(value); localStorage.setItem('openrouter-model', value); }
else if (sessionProvider === 'local') { setLocalModel?.(value); localStorage.setItem('local-model', value); }
else if (sessionProvider === 'nano') { setNanoModel?.(value); localStorage.setItem('nano-claude-code-model', value); }
@@ -619,7 +627,16 @@ export default function ChatComposer({
{modelConfig && (
<>
{(modelConfig as any).ALLOWS_CUSTOM ? (
-
+
) : (modelConfig as any).IS_LOCAL && modelConfig.OPTIONS.length === 0 ? (
();
interface OpenRouterModelInputProps {
value: string;
options: Array<{ value: string; label: string }>;
onChange: (v: string) => void;
+ storageKey?: string;
+ fetchUrl?: string | null;
+ searchPlaceholder?: string;
+ customModelPlaceholder?: string;
+ addCustomLabel?: string;
}
export default function OpenRouterModelInput({
value,
options: fallbackOptions,
onChange,
+ storageKey = 'openrouter-custom-models',
+ fetchUrl = '/api/settings/openrouter-models',
+ searchPlaceholder = 'Search models...',
+ customModelPlaceholder = 'e.g. provider/model-name',
+ addCustomLabel = 'Add custom model',
}: OpenRouterModelInputProps) {
+ const initialModels = fetchUrl ? modelsCacheByUrl.get(fetchUrl) || (fallbackOptions as ModelOption[]) : (fallbackOptions as ModelOption[]);
const [open, setOpen] = useState(false);
const [search, setSearch] = useState('');
- const [models, setModels] = useState(_modelsCache || (fallbackOptions as ModelOption[]));
+ const [models, setModels] = useState(initialModels);
const [loading, setLoading] = useState(false);
const [customDraft, setCustomDraft] = useState('');
const [showCustomInput, setShowCustomInput] = useState(false);
const containerRef = useRef(null);
const inputRef = useRef(null);
- const customModels: ModelOption[] = JSON.parse(localStorage.getItem('openrouter-custom-models') || '[]');
+ const customModels: ModelOption[] = JSON.parse(localStorage.getItem(storageKey) || '[]');
const fetchModels = useCallback(async () => {
- if (_modelsCache) {
- setModels(_modelsCache);
+ if (!fetchUrl) {
+ setModels(fallbackOptions as ModelOption[]);
+ return;
+ }
+ const cachedModels = modelsCacheByUrl.get(fetchUrl);
+ if (cachedModels?.length) {
+ setModels(cachedModels);
return;
}
setLoading(true);
try {
- const res = await authenticatedFetch('/api/settings/openrouter-models');
+ const res = await authenticatedFetch(fetchUrl);
if (res.ok) {
const data = await res.json();
if (data.models?.length) {
- _modelsCache = data.models;
+ modelsCacheByUrl.set(fetchUrl, data.models);
setModels(data.models);
+ } else {
+ setModels(fallbackOptions as ModelOption[]);
}
}
} catch {
/* use fallback */
}
+ if (!modelsCacheByUrl.get(fetchUrl)?.length) {
+ setModels(fallbackOptions as ModelOption[]);
+ }
setLoading(false);
- }, []);
+ }, [fallbackOptions, fetchUrl]);
useEffect(() => {
if (open) {
@@ -88,10 +109,10 @@ export default function OpenRouterModelInput({
const addCustomModel = () => {
const slug = customDraft.trim();
if (!slug) return;
- const existing: ModelOption[] = JSON.parse(localStorage.getItem('openrouter-custom-models') || '[]');
+ const existing: ModelOption[] = JSON.parse(localStorage.getItem(storageKey) || '[]');
if (!existing.some((m) => m.value === slug)) {
const updated = [...existing, { value: slug, label: slug, isCustom: true }];
- localStorage.setItem('openrouter-custom-models', JSON.stringify(updated));
+ localStorage.setItem(storageKey, JSON.stringify(updated));
}
onChange(slug);
setCustomDraft('');
@@ -100,9 +121,9 @@ export default function OpenRouterModelInput({
};
const removeCustomModel = (slug: string) => {
- const existing: ModelOption[] = JSON.parse(localStorage.getItem('openrouter-custom-models') || '[]');
+ const existing: ModelOption[] = JSON.parse(localStorage.getItem(storageKey) || '[]');
localStorage.setItem(
- 'openrouter-custom-models',
+ storageKey,
JSON.stringify(existing.filter((m) => m.value !== slug)),
);
if (value === slug) onChange(fallbackOptions[0]?.value || 'anthropic/claude-sonnet-4');
@@ -126,7 +147,7 @@ export default function OpenRouterModelInput({
ref={inputRef}
value={search}
onChange={(e) => setSearch(e.target.value)}
- placeholder="Search models..."
+ placeholder={searchPlaceholder}
className="flex-1 bg-transparent text-[11px] text-foreground placeholder:text-muted-foreground/50 focus:outline-none"
/>
{loading && Loading...}
@@ -192,7 +213,7 @@ export default function OpenRouterModelInput({
if (e.key === 'Enter') addCustomModel();
if (e.key === 'Escape') setShowCustomInput(false);
}}
- placeholder="e.g. provider/model-name"
+ placeholder={customModelPlaceholder}
className="flex-1 bg-transparent text-[11px] text-foreground border border-border/60 rounded px-2 py-1 focus:outline-none focus:ring-1 focus:ring-primary/30"
/>
)}
diff --git a/src/components/main-content/view/subcomponents/MainContentTitle.tsx b/src/components/main-content/view/subcomponents/MainContentTitle.tsx
index 632ae569..c9f93f62 100644
--- a/src/components/main-content/view/subcomponents/MainContentTitle.tsx
+++ b/src/components/main-content/view/subcomponents/MainContentTitle.tsx
@@ -1,7 +1,7 @@
import { useTranslation } from 'react-i18next';
import SessionProviderLogo from '../../../SessionProviderLogo';
import type { AppTab, Project, ProjectSession } from '../../../../types/app';
-import { stripInternalContextPrefix } from '../../../../utils/sessionFormatting';
+import { getSessionDisplayTitle } from '../../../../utils/sessionFormatting';
type MainContentTitleProps = {
activeTab: AppTab;
@@ -51,11 +51,7 @@ function getTabTitle(activeTab: AppTab, shouldShowTasksTab: boolean, t: (key: st
}
function getSessionTitle(session: ProjectSession): string {
- const name = session.__provider === 'cursor'
- ? (session.name as string) || 'Untitled Session'
- : (session.summary as string) || 'New Session';
-
- return stripInternalContextPrefix(name) || 'New Session';
+ return getSessionDisplayTitle(session);
}
export default function MainContentTitle({
diff --git a/src/components/project-dashboard/view/ProjectDashboard.tsx b/src/components/project-dashboard/view/ProjectDashboard.tsx
index 008e2cb6..e6666789 100644
--- a/src/components/project-dashboard/view/ProjectDashboard.tsx
+++ b/src/components/project-dashboard/view/ProjectDashboard.tsx
@@ -193,6 +193,7 @@ function getProjectSessions(project: Project): ProjectSession[] {
...(project.cursorSessions ?? []),
...(project.codexSessions ?? []),
...(project.geminiSessions ?? []),
+ ...(project.copilotSessions ?? []),
...(project.openrouterSessions ?? []),
...(project.localSessions ?? []),
...(project.nanoSessions ?? []),
diff --git a/src/components/settings/AccountContent.jsx b/src/components/settings/AccountContent.jsx
index 97d1d1a7..0df15003 100644
--- a/src/components/settings/AccountContent.jsx
+++ b/src/components/settings/AccountContent.jsx
@@ -48,6 +48,16 @@ const agentConfig = {
subtextClass: 'text-blue-700 dark:text-blue-300',
buttonClass: 'bg-blue-600 hover:bg-blue-700',
},
+ copilot: {
+ name: 'GitHub Copilot',
+ description: 'GitHub Copilot CLI assistant',
+ cliCommand: 'copilot',
+ bgClass: 'bg-slate-100 dark:bg-slate-800/50',
+ borderClass: 'border-slate-300 dark:border-slate-600',
+ textClass: 'text-slate-900 dark:text-slate-100',
+ subtextClass: 'text-slate-700 dark:text-slate-300',
+ buttonClass: 'bg-slate-800 hover:bg-slate-900 dark:bg-slate-700 dark:hover:bg-slate-600',
+ },
openrouter: {
name: 'OpenRouter',
description: 'Route to any model via OpenRouter API',
diff --git a/src/components/settings/AgentListItem.jsx b/src/components/settings/AgentListItem.jsx
index ec7ea53a..05e9ccf8 100644
--- a/src/components/settings/AgentListItem.jsx
+++ b/src/components/settings/AgentListItem.jsx
@@ -18,6 +18,10 @@ const agentConfig = {
name: 'Gemini',
color: 'blue',
},
+ copilot: {
+ name: 'GitHub Copilot',
+ color: 'gray',
+ },
openrouter: {
name: 'OpenRouter',
color: 'purple',
diff --git a/src/components/settings/McpServersContent.jsx b/src/components/settings/McpServersContent.jsx
index 257f58ab..828f3ce2 100644
--- a/src/components/settings/McpServersContent.jsx
+++ b/src/components/settings/McpServersContent.jsx
@@ -304,6 +304,96 @@ function CodexMcpServers({ servers, onAdd, onEdit, onDelete }) {
);
}
+function CopilotMcpServers({ servers, onAdd, onEdit, onDelete }) {
+ const { t } = useTranslation('settings');
+ return (
+