mirror of
https://github.com/davidkaya/aryx.git
synced 2026-08-27 13:23:57 +02:00
feat: add MCP OAuth authentication frontend wiring and banner
- Add McpOauthRequiredEvent and McpOauthStaticClientConfigEvent to sidecar contracts - Add PendingMcpAuthRecord domain type with status tracking (pending/authenticating/failed/done) - Add pendingMcpAuth field to SessionRecord for session-level auth state - Wire onMcpOAuthRequired callback through sidecarProcess, runTurnPending, and AryxAppService - Handle mcp-oauth-required events: set session pendingMcpAuth, clear on turn finalize/cancel - Add dismissSessionMcpAuth IPC channel with preload binding and handler registration - Create McpAuthBanner component (amber-themed, shows server name/URL, dismiss button) - Integrate McpAuthBanner into ChatPane with placeholder text and App.tsx callback - Update test fixtures for new RunTurnPendingCommand.onMcpOAuthRequired field Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -8,6 +8,7 @@ import type {
|
|||||||
AgentActivityEvent,
|
AgentActivityEvent,
|
||||||
ApprovalRequestedEvent,
|
ApprovalRequestedEvent,
|
||||||
ExitPlanModeRequestedEvent,
|
ExitPlanModeRequestedEvent,
|
||||||
|
McpOauthRequiredEvent,
|
||||||
RunTurnToolingConfig,
|
RunTurnToolingConfig,
|
||||||
SidecarCapabilities,
|
SidecarCapabilities,
|
||||||
TurnDeltaEvent,
|
TurnDeltaEvent,
|
||||||
@@ -586,6 +587,7 @@ export class AryxAppService extends EventEmitter<AppServiceEvents> {
|
|||||||
session.status = 'running';
|
session.status = 'running';
|
||||||
session.lastError = undefined;
|
session.lastError = undefined;
|
||||||
session.pendingPlanReview = undefined;
|
session.pendingPlanReview = undefined;
|
||||||
|
session.pendingMcpAuth = undefined;
|
||||||
session.updatedAt = occurredAt;
|
session.updatedAt = occurredAt;
|
||||||
session.runs = [
|
session.runs = [
|
||||||
createSessionRunRecord({
|
createSessionRunRecord({
|
||||||
@@ -634,6 +636,9 @@ export class AryxAppService extends EventEmitter<AppServiceEvents> {
|
|||||||
await this.handleUserInputRequested(workspace, session.id, requestId, event, (answer, wasFreeform) =>
|
await this.handleUserInputRequested(workspace, session.id, requestId, event, (answer, wasFreeform) =>
|
||||||
this.sidecar.resolveUserInput(event.userInputId, answer, wasFreeform));
|
this.sidecar.resolveUserInput(event.userInputId, answer, wasFreeform));
|
||||||
},
|
},
|
||||||
|
async (event) => {
|
||||||
|
await this.handleMcpOAuthRequired(workspace, session.id, event);
|
||||||
|
},
|
||||||
async (event) => {
|
async (event) => {
|
||||||
await this.handleExitPlanModeRequested(workspace, session.id, event);
|
await this.handleExitPlanModeRequested(workspace, session.id, event);
|
||||||
},
|
},
|
||||||
@@ -874,6 +879,16 @@ export class AryxAppService extends EventEmitter<AppServiceEvents> {
|
|||||||
return this.persistAndBroadcast(workspace);
|
return this.persistAndBroadcast(workspace);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async dismissSessionMcpAuth(sessionId: string): Promise<WorkspaceState> {
|
||||||
|
const workspace = await this.loadWorkspace();
|
||||||
|
const session = this.requireSession(workspace, sessionId);
|
||||||
|
|
||||||
|
session.pendingMcpAuth = undefined;
|
||||||
|
session.updatedAt = nowIso();
|
||||||
|
|
||||||
|
return this.persistAndBroadcast(workspace);
|
||||||
|
}
|
||||||
|
|
||||||
async updateSessionTooling(
|
async updateSessionTooling(
|
||||||
sessionId: string,
|
sessionId: string,
|
||||||
enabledMcpServerIds: string[],
|
enabledMcpServerIds: string[],
|
||||||
@@ -1210,6 +1225,7 @@ export class AryxAppService extends EventEmitter<AppServiceEvents> {
|
|||||||
session.lastError = undefined;
|
session.lastError = undefined;
|
||||||
session.pendingUserInput = undefined;
|
session.pendingUserInput = undefined;
|
||||||
session.pendingPlanReview = undefined;
|
session.pendingPlanReview = undefined;
|
||||||
|
session.pendingMcpAuth = undefined;
|
||||||
session.updatedAt = completedAt;
|
session.updatedAt = completedAt;
|
||||||
const completedRun = this.updateSessionRun(session, requestId, (run) =>
|
const completedRun = this.updateSessionRun(session, requestId, (run) =>
|
||||||
completeSessionRunRecord(run, completedAt));
|
completeSessionRunRecord(run, completedAt));
|
||||||
@@ -1242,6 +1258,7 @@ export class AryxAppService extends EventEmitter<AppServiceEvents> {
|
|||||||
session.lastError = undefined;
|
session.lastError = undefined;
|
||||||
session.pendingUserInput = undefined;
|
session.pendingUserInput = undefined;
|
||||||
session.pendingPlanReview = undefined;
|
session.pendingPlanReview = undefined;
|
||||||
|
session.pendingMcpAuth = undefined;
|
||||||
session.updatedAt = cancelledAt;
|
session.updatedAt = cancelledAt;
|
||||||
const cancelledRun = this.updateSessionRun(session, requestId, (run) =>
|
const cancelledRun = this.updateSessionRun(session, requestId, (run) =>
|
||||||
cancelSessionRunRecord(run, cancelledAt));
|
cancelSessionRunRecord(run, cancelledAt));
|
||||||
@@ -1340,6 +1357,31 @@ export class AryxAppService extends EventEmitter<AppServiceEvents> {
|
|||||||
await this.persistAndBroadcast(workspace);
|
await this.persistAndBroadcast(workspace);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async handleMcpOAuthRequired(
|
||||||
|
workspace: WorkspaceState,
|
||||||
|
sessionId: string,
|
||||||
|
event: McpOauthRequiredEvent,
|
||||||
|
): Promise<void> {
|
||||||
|
const session = this.requireSession(workspace, sessionId);
|
||||||
|
const requestedAt = nowIso();
|
||||||
|
|
||||||
|
session.pendingMcpAuth = {
|
||||||
|
id: event.oauthRequestId,
|
||||||
|
status: 'pending',
|
||||||
|
agentId: event.agentId,
|
||||||
|
agentName: event.agentName,
|
||||||
|
serverName: event.serverName,
|
||||||
|
serverUrl: event.serverUrl,
|
||||||
|
staticClientConfig: event.staticClientConfig
|
||||||
|
? { clientId: event.staticClientConfig.clientId, publicClient: event.staticClientConfig.publicClient }
|
||||||
|
: undefined,
|
||||||
|
requestedAt,
|
||||||
|
};
|
||||||
|
session.updatedAt = requestedAt;
|
||||||
|
|
||||||
|
await this.persistAndBroadcast(workspace);
|
||||||
|
}
|
||||||
|
|
||||||
private createPendingApprovalFromSidecarEvent(event: ApprovalRequestedEvent): PendingApprovalRecord {
|
private createPendingApprovalFromSidecarEvent(event: ApprovalRequestedEvent): PendingApprovalRecord {
|
||||||
return {
|
return {
|
||||||
id: event.approvalId,
|
id: event.approvalId,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import type {
|
|||||||
ResolveProjectDiscoveredToolingInput,
|
ResolveProjectDiscoveredToolingInput,
|
||||||
ResolveWorkspaceDiscoveredToolingInput,
|
ResolveWorkspaceDiscoveredToolingInput,
|
||||||
DismissSessionPlanReviewInput,
|
DismissSessionPlanReviewInput,
|
||||||
|
DismissSessionMcpAuthInput,
|
||||||
DuplicateSessionInput,
|
DuplicateSessionInput,
|
||||||
RenameSessionInput,
|
RenameSessionInput,
|
||||||
RescanProjectConfigsInput,
|
RescanProjectConfigsInput,
|
||||||
@@ -122,6 +123,9 @@ export function registerIpcHandlers(window: BrowserWindow, service: AryxAppServi
|
|||||||
ipcMain.handle(ipcChannels.dismissSessionPlanReview, (_event, input: DismissSessionPlanReviewInput) =>
|
ipcMain.handle(ipcChannels.dismissSessionPlanReview, (_event, input: DismissSessionPlanReviewInput) =>
|
||||||
service.dismissSessionPlanReview(input.sessionId),
|
service.dismissSessionPlanReview(input.sessionId),
|
||||||
);
|
);
|
||||||
|
ipcMain.handle(ipcChannels.dismissSessionMcpAuth, (_event, input: DismissSessionMcpAuthInput) =>
|
||||||
|
service.dismissSessionMcpAuth(input.sessionId),
|
||||||
|
);
|
||||||
ipcMain.handle(
|
ipcMain.handle(
|
||||||
ipcChannels.updateSessionModelConfig,
|
ipcChannels.updateSessionModelConfig,
|
||||||
(_event, input: UpdateSessionModelConfigInput) =>
|
(_event, input: UpdateSessionModelConfigInput) =>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type {
|
|||||||
AgentActivityEvent,
|
AgentActivityEvent,
|
||||||
ApprovalRequestedEvent,
|
ApprovalRequestedEvent,
|
||||||
ExitPlanModeRequestedEvent,
|
ExitPlanModeRequestedEvent,
|
||||||
|
McpOauthRequiredEvent,
|
||||||
TurnDeltaEvent,
|
TurnDeltaEvent,
|
||||||
UserInputRequestedEvent,
|
UserInputRequestedEvent,
|
||||||
} from '@shared/contracts/sidecar';
|
} from '@shared/contracts/sidecar';
|
||||||
@@ -15,6 +16,7 @@ export interface RunTurnPendingCommand {
|
|||||||
onActivity: (event: AgentActivityEvent) => void | Promise<void>;
|
onActivity: (event: AgentActivityEvent) => void | Promise<void>;
|
||||||
onApproval: (event: ApprovalRequestedEvent) => void | Promise<void>;
|
onApproval: (event: ApprovalRequestedEvent) => void | Promise<void>;
|
||||||
onUserInput: (event: UserInputRequestedEvent) => void | Promise<void>;
|
onUserInput: (event: UserInputRequestedEvent) => void | Promise<void>;
|
||||||
|
onMcpOAuthRequired: (event: McpOauthRequiredEvent) => void | Promise<void>;
|
||||||
onExitPlanMode: (event: ExitPlanModeRequestedEvent) => void | Promise<void>;
|
onExitPlanMode: (event: ExitPlanModeRequestedEvent) => void | Promise<void>;
|
||||||
errored: boolean;
|
errored: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import type {
|
|||||||
SidecarEvent,
|
SidecarEvent,
|
||||||
TurnDeltaEvent,
|
TurnDeltaEvent,
|
||||||
UserInputRequestedEvent,
|
UserInputRequestedEvent,
|
||||||
|
McpOauthRequiredEvent,
|
||||||
ExitPlanModeRequestedEvent,
|
ExitPlanModeRequestedEvent,
|
||||||
ValidatePatternCommand,
|
ValidatePatternCommand,
|
||||||
RunTurnCommand,
|
RunTurnCommand,
|
||||||
@@ -103,9 +104,10 @@ export class SidecarClient {
|
|||||||
onActivity: (event: AgentActivityEvent) => void | Promise<void>,
|
onActivity: (event: AgentActivityEvent) => void | Promise<void>,
|
||||||
onApproval: (event: ApprovalRequestedEvent) => void | Promise<void>,
|
onApproval: (event: ApprovalRequestedEvent) => void | Promise<void>,
|
||||||
onUserInput: (event: UserInputRequestedEvent) => void | Promise<void>,
|
onUserInput: (event: UserInputRequestedEvent) => void | Promise<void>,
|
||||||
|
onMcpOAuthRequired: (event: McpOauthRequiredEvent) => void | Promise<void>,
|
||||||
onExitPlanMode: (event: ExitPlanModeRequestedEvent) => void | Promise<void>,
|
onExitPlanMode: (event: ExitPlanModeRequestedEvent) => void | Promise<void>,
|
||||||
): Promise<ChatMessageRecord[]> {
|
): Promise<ChatMessageRecord[]> {
|
||||||
return this.dispatch<ChatMessageRecord[]>(command, onDelta, onActivity, onApproval, onUserInput, onExitPlanMode);
|
return this.dispatch<ChatMessageRecord[]>(command, onDelta, onActivity, onApproval, onUserInput, onMcpOAuthRequired, onExitPlanMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
async resolveUserInput(userInputId: string, answer: string, wasFreeform: boolean): Promise<void> {
|
async resolveUserInput(userInputId: string, answer: string, wasFreeform: boolean): Promise<void> {
|
||||||
@@ -220,6 +222,7 @@ export class SidecarClient {
|
|||||||
onActivity?: (event: AgentActivityEvent) => void | Promise<void>,
|
onActivity?: (event: AgentActivityEvent) => void | Promise<void>,
|
||||||
onApproval?: (event: ApprovalRequestedEvent) => void | Promise<void>,
|
onApproval?: (event: ApprovalRequestedEvent) => void | Promise<void>,
|
||||||
onUserInput?: (event: UserInputRequestedEvent) => void | Promise<void>,
|
onUserInput?: (event: UserInputRequestedEvent) => void | Promise<void>,
|
||||||
|
onMcpOAuthRequired?: (event: McpOauthRequiredEvent) => void | Promise<void>,
|
||||||
onExitPlanMode?: (event: ExitPlanModeRequestedEvent) => void | Promise<void>,
|
onExitPlanMode?: (event: ExitPlanModeRequestedEvent) => void | Promise<void>,
|
||||||
): Promise<TResult> {
|
): Promise<TResult> {
|
||||||
const state = await this.ensureProcess();
|
const state = await this.ensureProcess();
|
||||||
@@ -235,6 +238,7 @@ export class SidecarClient {
|
|||||||
onActivity: onActivity ?? (() => undefined),
|
onActivity: onActivity ?? (() => undefined),
|
||||||
onApproval: onApproval ?? (() => undefined),
|
onApproval: onApproval ?? (() => undefined),
|
||||||
onUserInput: onUserInput ?? (() => undefined),
|
onUserInput: onUserInput ?? (() => undefined),
|
||||||
|
onMcpOAuthRequired: onMcpOAuthRequired ?? (() => undefined),
|
||||||
onExitPlanMode: onExitPlanMode ?? (() => undefined),
|
onExitPlanMode: onExitPlanMode ?? (() => undefined),
|
||||||
errored: false,
|
errored: false,
|
||||||
});
|
});
|
||||||
@@ -333,6 +337,11 @@ export class SidecarClient {
|
|||||||
this.invokeRunTurnHandler(event.requestId, pending, () => pending.onUserInput(event));
|
this.invokeRunTurnHandler(event.requestId, pending, () => pending.onUserInput(event));
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
|
case 'mcp-oauth-required':
|
||||||
|
if (pending.kind === 'run-turn' && shouldHandleRunTurnEvent(pending)) {
|
||||||
|
this.invokeRunTurnHandler(event.requestId, pending, () => pending.onMcpOAuthRequired(event));
|
||||||
|
}
|
||||||
|
return;
|
||||||
case 'exit-plan-mode-requested':
|
case 'exit-plan-mode-requested':
|
||||||
if (pending.kind === 'run-turn' && shouldHandleRunTurnEvent(pending)) {
|
if (pending.kind === 'run-turn' && shouldHandleRunTurnEvent(pending)) {
|
||||||
this.invokeRunTurnHandler(event.requestId, pending, () => pending.onExitPlanMode(event));
|
this.invokeRunTurnHandler(event.requestId, pending, () => pending.onExitPlanMode(event));
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ const api: ElectronApi = {
|
|||||||
resolveSessionUserInput: (input) => ipcRenderer.invoke(ipcChannels.resolveSessionUserInput, input),
|
resolveSessionUserInput: (input) => ipcRenderer.invoke(ipcChannels.resolveSessionUserInput, input),
|
||||||
setSessionInteractionMode: (input) => ipcRenderer.invoke(ipcChannels.setSessionInteractionMode, input),
|
setSessionInteractionMode: (input) => ipcRenderer.invoke(ipcChannels.setSessionInteractionMode, input),
|
||||||
dismissSessionPlanReview: (input) => ipcRenderer.invoke(ipcChannels.dismissSessionPlanReview, input),
|
dismissSessionPlanReview: (input) => ipcRenderer.invoke(ipcChannels.dismissSessionPlanReview, input),
|
||||||
|
dismissSessionMcpAuth: (input) => ipcRenderer.invoke(ipcChannels.dismissSessionMcpAuth, input),
|
||||||
updateSessionModelConfig: (input) =>
|
updateSessionModelConfig: (input) =>
|
||||||
ipcRenderer.invoke(ipcChannels.updateSessionModelConfig, input),
|
ipcRenderer.invoke(ipcChannels.updateSessionModelConfig, input),
|
||||||
querySessions: (input) => ipcRenderer.invoke(ipcChannels.querySessions, input),
|
querySessions: (input) => ipcRenderer.invoke(ipcChannels.querySessions, input),
|
||||||
|
|||||||
@@ -259,6 +259,9 @@ export default function App() {
|
|||||||
onDismissPlanReview={() => {
|
onDismissPlanReview={() => {
|
||||||
void api.dismissSessionPlanReview({ sessionId: selectedSession.id });
|
void api.dismissSessionPlanReview({ sessionId: selectedSession.id });
|
||||||
}}
|
}}
|
||||||
|
onDismissMcpAuth={() => {
|
||||||
|
void api.dismissSessionMcpAuth({ sessionId: selectedSession.id });
|
||||||
|
}}
|
||||||
onUpdateSessionModelConfig={(config) =>
|
onUpdateSessionModelConfig={(config) =>
|
||||||
api.updateSessionModelConfig({
|
api.updateSessionModelConfig({
|
||||||
sessionId: selectedSession.id,
|
sessionId: selectedSession.id,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { MarkdownContent } from '@renderer/components/MarkdownContent';
|
|||||||
import { MarkdownComposer, type MarkdownComposerHandle } from '@renderer/components/MarkdownComposer';
|
import { MarkdownComposer, type MarkdownComposerHandle } from '@renderer/components/MarkdownComposer';
|
||||||
import { ApprovalBanner, QueuedApprovalsList } from '@renderer/components/chat/ApprovalBanner';
|
import { ApprovalBanner, QueuedApprovalsList } from '@renderer/components/chat/ApprovalBanner';
|
||||||
import { PlanReviewBanner } from '@renderer/components/chat/PlanReviewBanner';
|
import { PlanReviewBanner } from '@renderer/components/chat/PlanReviewBanner';
|
||||||
|
import { McpAuthBanner } from '@renderer/components/chat/McpAuthBanner';
|
||||||
import { UserInputBanner } from '@renderer/components/chat/UserInputBanner';
|
import { UserInputBanner } from '@renderer/components/chat/UserInputBanner';
|
||||||
import { InlineApprovalPill, InlineModelPill, InlineThinkingPill, InlineToolsPill } from '@renderer/components/chat/InlinePills';
|
import { InlineApprovalPill, InlineModelPill, InlineThinkingPill, InlineToolsPill } from '@renderer/components/chat/InlinePills';
|
||||||
import { ThinkingDots } from '@renderer/components/chat/ThinkingDots';
|
import { ThinkingDots } from '@renderer/components/chat/ThinkingDots';
|
||||||
@@ -42,6 +43,7 @@ interface ChatPaneProps {
|
|||||||
onResolveUserInput?: (userInputId: string, answer: string, wasFreeform: boolean) => Promise<unknown>;
|
onResolveUserInput?: (userInputId: string, answer: string, wasFreeform: boolean) => Promise<unknown>;
|
||||||
onSetInteractionMode?: (mode: InteractionMode) => void;
|
onSetInteractionMode?: (mode: InteractionMode) => void;
|
||||||
onDismissPlanReview?: () => void;
|
onDismissPlanReview?: () => void;
|
||||||
|
onDismissMcpAuth?: () => void;
|
||||||
onUpdateSessionModelConfig?: (config: {
|
onUpdateSessionModelConfig?: (config: {
|
||||||
model: string;
|
model: string;
|
||||||
reasoningEffort?: ReasoningEffort;
|
reasoningEffort?: ReasoningEffort;
|
||||||
@@ -63,6 +65,7 @@ export function ChatPane({
|
|||||||
onResolveUserInput,
|
onResolveUserInput,
|
||||||
onSetInteractionMode,
|
onSetInteractionMode,
|
||||||
onDismissPlanReview,
|
onDismissPlanReview,
|
||||||
|
onDismissMcpAuth,
|
||||||
onUpdateSessionModelConfig,
|
onUpdateSessionModelConfig,
|
||||||
onUpdateSessionTooling,
|
onUpdateSessionTooling,
|
||||||
onUpdateSessionApprovalSettings,
|
onUpdateSessionApprovalSettings,
|
||||||
@@ -82,6 +85,10 @@ export function ChatPane({
|
|||||||
const totalPendingCount = (pendingApproval ? 1 : 0) + queuedApprovals.length;
|
const totalPendingCount = (pendingApproval ? 1 : 0) + queuedApprovals.length;
|
||||||
const pendingUserInput = session.pendingUserInput?.status === 'pending' ? session.pendingUserInput : undefined;
|
const pendingUserInput = session.pendingUserInput?.status === 'pending' ? session.pendingUserInput : undefined;
|
||||||
const pendingPlanReview = session.pendingPlanReview?.status === 'pending' ? session.pendingPlanReview : undefined;
|
const pendingPlanReview = session.pendingPlanReview?.status === 'pending' ? session.pendingPlanReview : undefined;
|
||||||
|
const pendingMcpAuth = session.pendingMcpAuth?.status === 'pending' || session.pendingMcpAuth?.status === 'authenticating'
|
||||||
|
|| session.pendingMcpAuth?.status === 'failed'
|
||||||
|
? session.pendingMcpAuth
|
||||||
|
: undefined;
|
||||||
const interactionMode: InteractionMode = session.interactionMode ?? 'interactive';
|
const interactionMode: InteractionMode = session.interactionMode ?? 'interactive';
|
||||||
const isPlanMode = interactionMode === 'plan';
|
const isPlanMode = interactionMode === 'plan';
|
||||||
const isScratchpad = isScratchpadProject(project);
|
const isScratchpad = isScratchpadProject(project);
|
||||||
@@ -134,6 +141,10 @@ export function ChatPane({
|
|||||||
onDismissPlanReview?.();
|
onDismissPlanReview?.();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleDismissMcpAuth() {
|
||||||
|
onDismissMcpAuth?.();
|
||||||
|
}
|
||||||
|
|
||||||
async function handleSessionModelConfigChange(config: {
|
async function handleSessionModelConfigChange(config: {
|
||||||
model: string;
|
model: string;
|
||||||
reasoningEffort?: ReasoningEffort;
|
reasoningEffort?: ReasoningEffort;
|
||||||
@@ -398,6 +409,16 @@ export function ChatPane({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* MCP auth required banner */}
|
||||||
|
{pendingMcpAuth && (
|
||||||
|
<div className="mb-3">
|
||||||
|
<McpAuthBanner
|
||||||
|
mcpAuth={pendingMcpAuth}
|
||||||
|
onDismiss={handleDismissMcpAuth}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Session config pills — tools/approval left, model/reasoning right */}
|
{/* Session config pills — tools/approval left, model/reasoning right */}
|
||||||
{isSingleAgent && (
|
{isSingleAgent && (
|
||||||
<div className="mb-2 flex items-center gap-2">
|
<div className="mb-2 flex items-center gap-2">
|
||||||
@@ -489,7 +510,9 @@ export function ChatPane({
|
|||||||
? 'Awaiting your input above...'
|
? 'Awaiting your input above...'
|
||||||
: pendingPlanReview
|
: pendingPlanReview
|
||||||
? 'Review the plan above...'
|
? 'Review the plan above...'
|
||||||
: isSessionBusy
|
: pendingMcpAuth
|
||||||
|
? 'MCP server requires authentication...'
|
||||||
|
: isSessionBusy
|
||||||
? 'Waiting for response...'
|
? 'Waiting for response...'
|
||||||
: isUpdatingSessionModelConfig
|
: isUpdatingSessionModelConfig
|
||||||
? 'Saving model settings...'
|
? 'Saving model settings...'
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import { useCallback } from 'react';
|
||||||
|
import { KeyRound, X } from 'lucide-react';
|
||||||
|
|
||||||
|
import type { PendingMcpAuthRecord } from '@shared/domain/mcpAuth';
|
||||||
|
|
||||||
|
export function McpAuthBanner({
|
||||||
|
mcpAuth,
|
||||||
|
onDismiss,
|
||||||
|
}: {
|
||||||
|
mcpAuth: PendingMcpAuthRecord;
|
||||||
|
onDismiss: () => void;
|
||||||
|
}) {
|
||||||
|
const handleDismiss = useCallback(() => {
|
||||||
|
onDismiss();
|
||||||
|
}, [onDismiss]);
|
||||||
|
|
||||||
|
const isAuthenticating = mcpAuth.status === 'authenticating';
|
||||||
|
const hasFailed = mcpAuth.status === 'failed';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="rounded-xl border border-amber-500/30 bg-amber-500/5 px-4 py-3" role="alert">
|
||||||
|
<div className="flex items-start gap-2.5">
|
||||||
|
<KeyRound className="mt-0.5 size-4 shrink-0 text-amber-400" />
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<div className="flex items-center justify-between gap-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-[13px] font-semibold text-amber-200">Authentication required</span>
|
||||||
|
<span className="rounded-full bg-amber-500/15 px-2 py-0.5 text-[9px] font-semibold uppercase tracking-wider text-amber-400">
|
||||||
|
MCP
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
aria-label="Dismiss authentication prompt"
|
||||||
|
className="rounded p-0.5 text-zinc-500 transition hover:bg-zinc-700/50 hover:text-zinc-300"
|
||||||
|
onClick={handleDismiss}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<X className="size-3.5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="mt-2 text-[13px] leading-relaxed text-zinc-200">
|
||||||
|
The MCP server{' '}
|
||||||
|
<span className="font-medium text-amber-200">{mcpAuth.serverName}</span>{' '}
|
||||||
|
requires OAuth authentication to connect.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p className="mt-1 text-[11px] text-zinc-500">{mcpAuth.serverUrl}</p>
|
||||||
|
|
||||||
|
{hasFailed && mcpAuth.errorMessage && (
|
||||||
|
<p className="mt-2 text-[12px] text-red-400">{mcpAuth.errorMessage}</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<p className="mt-3 text-[12px] leading-relaxed text-zinc-400">
|
||||||
|
{isAuthenticating
|
||||||
|
? 'Waiting for authentication to complete in the browser…'
|
||||||
|
: 'Authentication support for HTTP MCP servers is not yet available. Configure a static access token in the MCP server headers instead.'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -29,6 +29,7 @@ export const ipcChannels = {
|
|||||||
resolveSessionUserInput: 'sessions:resolve-user-input',
|
resolveSessionUserInput: 'sessions:resolve-user-input',
|
||||||
setSessionInteractionMode: 'sessions:set-interaction-mode',
|
setSessionInteractionMode: 'sessions:set-interaction-mode',
|
||||||
dismissSessionPlanReview: 'sessions:dismiss-plan-review',
|
dismissSessionPlanReview: 'sessions:dismiss-plan-review',
|
||||||
|
dismissSessionMcpAuth: 'sessions:dismiss-mcp-auth',
|
||||||
querySessions: 'sessions:query',
|
querySessions: 'sessions:query',
|
||||||
updateSessionModelConfig: 'sessions:update-model-config',
|
updateSessionModelConfig: 'sessions:update-model-config',
|
||||||
selectProject: 'selection:project',
|
selectProject: 'selection:project',
|
||||||
|
|||||||
@@ -116,6 +116,10 @@ export interface DismissSessionPlanReviewInput {
|
|||||||
sessionId: string;
|
sessionId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DismissSessionMcpAuthInput {
|
||||||
|
sessionId: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ElectronApi {
|
export interface ElectronApi {
|
||||||
describeSidecarCapabilities(): Promise<SidecarCapabilities>;
|
describeSidecarCapabilities(): Promise<SidecarCapabilities>;
|
||||||
refreshSidecarCapabilities(): Promise<SidecarCapabilities>;
|
refreshSidecarCapabilities(): Promise<SidecarCapabilities>;
|
||||||
@@ -145,6 +149,7 @@ export interface ElectronApi {
|
|||||||
resolveSessionUserInput(input: ResolveSessionUserInputInput): Promise<WorkspaceState>;
|
resolveSessionUserInput(input: ResolveSessionUserInputInput): Promise<WorkspaceState>;
|
||||||
setSessionInteractionMode(input: SetSessionInteractionModeInput): Promise<WorkspaceState>;
|
setSessionInteractionMode(input: SetSessionInteractionModeInput): Promise<WorkspaceState>;
|
||||||
dismissSessionPlanReview(input: DismissSessionPlanReviewInput): Promise<WorkspaceState>;
|
dismissSessionPlanReview(input: DismissSessionPlanReviewInput): Promise<WorkspaceState>;
|
||||||
|
dismissSessionMcpAuth(input: DismissSessionMcpAuthInput): Promise<WorkspaceState>;
|
||||||
updateSessionModelConfig(input: UpdateSessionModelConfigInput): Promise<WorkspaceState>;
|
updateSessionModelConfig(input: UpdateSessionModelConfigInput): Promise<WorkspaceState>;
|
||||||
querySessions(input: QuerySessionsInput): Promise<SessionQueryResult[]>;
|
querySessions(input: QuerySessionsInput): Promise<SessionQueryResult[]>;
|
||||||
selectProject(projectId?: string): Promise<WorkspaceState>;
|
selectProject(projectId?: string): Promise<WorkspaceState>;
|
||||||
|
|||||||
@@ -244,6 +244,23 @@ export interface UserInputRequestedEvent {
|
|||||||
allowFreeform?: boolean;
|
allowFreeform?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface McpOauthStaticClientConfigEvent {
|
||||||
|
clientId: string;
|
||||||
|
publicClient?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface McpOauthRequiredEvent {
|
||||||
|
type: 'mcp-oauth-required';
|
||||||
|
requestId: string;
|
||||||
|
sessionId: string;
|
||||||
|
oauthRequestId: string;
|
||||||
|
agentId?: string;
|
||||||
|
agentName?: string;
|
||||||
|
serverName: string;
|
||||||
|
serverUrl: string;
|
||||||
|
staticClientConfig?: McpOauthStaticClientConfigEvent;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ExitPlanModeRequestedEvent {
|
export interface ExitPlanModeRequestedEvent {
|
||||||
type: 'exit-plan-mode-requested';
|
type: 'exit-plan-mode-requested';
|
||||||
requestId: string;
|
requestId: string;
|
||||||
@@ -276,6 +293,7 @@ export type SidecarEvent =
|
|||||||
| AgentActivityEvent
|
| AgentActivityEvent
|
||||||
| ApprovalRequestedEvent
|
| ApprovalRequestedEvent
|
||||||
| UserInputRequestedEvent
|
| UserInputRequestedEvent
|
||||||
|
| McpOauthRequiredEvent
|
||||||
| ExitPlanModeRequestedEvent
|
| ExitPlanModeRequestedEvent
|
||||||
| CommandErrorEvent
|
| CommandErrorEvent
|
||||||
| CommandCompleteEvent;
|
| CommandCompleteEvent;
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
export type McpAuthStatus = 'pending' | 'authenticating' | 'authenticated' | 'failed';
|
||||||
|
|
||||||
|
export interface McpOauthStaticClientConfig {
|
||||||
|
clientId: string;
|
||||||
|
publicClient?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PendingMcpAuthRecord {
|
||||||
|
id: string;
|
||||||
|
status: McpAuthStatus;
|
||||||
|
agentId?: string;
|
||||||
|
agentName?: string;
|
||||||
|
serverName: string;
|
||||||
|
serverUrl: string;
|
||||||
|
staticClientConfig?: McpOauthStaticClientConfig;
|
||||||
|
requestedAt: string;
|
||||||
|
completedAt?: string;
|
||||||
|
errorMessage?: string;
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
import type { SessionRunRecord } from '@shared/domain/runTimeline';
|
import type { SessionRunRecord } from '@shared/domain/runTimeline';
|
||||||
import type { PendingUserInputRecord } from '@shared/domain/userInput';
|
import type { PendingUserInputRecord } from '@shared/domain/userInput';
|
||||||
import type { PendingPlanReviewRecord } from '@shared/domain/planReview';
|
import type { PendingPlanReviewRecord } from '@shared/domain/planReview';
|
||||||
|
import type { PendingMcpAuthRecord } from '@shared/domain/mcpAuth';
|
||||||
import type { InteractionMode } from '@shared/contracts/sidecar';
|
import type { InteractionMode } from '@shared/contracts/sidecar';
|
||||||
|
|
||||||
export type ChatRole = 'system' | 'user' | 'assistant';
|
export type ChatRole = 'system' | 'user' | 'assistant';
|
||||||
@@ -54,6 +55,7 @@ export interface SessionRecord {
|
|||||||
pendingApprovalQueue?: PendingApprovalRecord[];
|
pendingApprovalQueue?: PendingApprovalRecord[];
|
||||||
pendingUserInput?: PendingUserInputRecord;
|
pendingUserInput?: PendingUserInputRecord;
|
||||||
pendingPlanReview?: PendingPlanReviewRecord;
|
pendingPlanReview?: PendingPlanReviewRecord;
|
||||||
|
pendingMcpAuth?: PendingMcpAuthRecord;
|
||||||
runs: SessionRunRecord[];
|
runs: SessionRunRecord[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ describe('run turn pending helpers', () => {
|
|||||||
onApproval: () => undefined,
|
onApproval: () => undefined,
|
||||||
onUserInput: () => undefined,
|
onUserInput: () => undefined,
|
||||||
onExitPlanMode: () => undefined,
|
onExitPlanMode: () => undefined,
|
||||||
|
onMcpOAuthRequired: () => undefined,
|
||||||
errored: false,
|
errored: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -42,6 +43,7 @@ describe('run turn pending helpers', () => {
|
|||||||
onApproval: () => undefined,
|
onApproval: () => undefined,
|
||||||
onUserInput: () => undefined,
|
onUserInput: () => undefined,
|
||||||
onExitPlanMode: () => undefined,
|
onExitPlanMode: () => undefined,
|
||||||
|
onMcpOAuthRequired: () => undefined,
|
||||||
errored: false,
|
errored: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user