mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-26 19:44:51 +02:00
[eric] remove vendored 9router (now fetched from npm at build), remove Copilot, and fix subs + tools
This commit is contained in:
@@ -49,7 +49,7 @@ function isValidEmail(email: string): boolean {
|
||||
const SUBSCRIPTION_PROVIDERS = [
|
||||
{ id: 'openswarm-pro', name: 'OpenSwarm Pro', desc: 'One subscription — no setup, no Claude account needed', color: '#6366F1', preview: false, recommended: true },
|
||||
{ id: 'claude', name: 'Claude', desc: 'Use your own Claude Pro/Max subscription', color: '#E8927A', preview: false },
|
||||
{ id: 'gemini-cli', name: 'Gemini', desc: 'Gemini 3 Pro, 3 Flash, 2.5 Pro & Flash', color: '#4285F4', preview: false },
|
||||
{ id: 'antigravity', name: 'Gemini', desc: 'Gemini 3 Pro, 3 Flash, 2.5 Pro & Flash', color: '#4285F4', preview: false },
|
||||
{ id: 'codex', name: 'ChatGPT', desc: 'GPT-5.4, GPT-5.4 Mini, GPT-5.3 Codex', color: '#74AA9C', preview: false },
|
||||
];
|
||||
|
||||
@@ -441,58 +441,91 @@ const OnboardingModal: React.FC = () => {
|
||||
} else if (data.flow === 'authorization_code') {
|
||||
const popup = window.open(data.auth_url, 'oauth_connect', 'width=600,height=700');
|
||||
|
||||
// Poll status as primary detection (works in Electron where postMessage may not)
|
||||
// Centralized exchange + cleanup so all three detection paths
|
||||
// (postMessage, Electron IPC, status polling) can trigger it.
|
||||
let exchanged = false;
|
||||
const runExchange = async (code: string, state?: string) => {
|
||||
if (exchanged) return;
|
||||
exchanged = true;
|
||||
if (msgHandlerRef.current) {
|
||||
window.removeEventListener('message', msgHandlerRef.current);
|
||||
msgHandlerRef.current = null;
|
||||
}
|
||||
if (ipcUnsub) { ipcUnsub(); ipcUnsub = null; }
|
||||
if (pollTimerRef.current) { clearInterval(pollTimerRef.current); pollTimerRef.current = null; }
|
||||
if (popup && !popup.closed) popup.close();
|
||||
try {
|
||||
await fetch(`${API_BASE}/agents/subscriptions/exchange`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
provider: providerId,
|
||||
code,
|
||||
redirect_uri: data.redirect_uri,
|
||||
code_verifier: data.code_verifier,
|
||||
state: state || data.state,
|
||||
}),
|
||||
});
|
||||
} catch {}
|
||||
trackEvent('onboarding.provider_connected', { provider: providerId });
|
||||
dismiss();
|
||||
};
|
||||
|
||||
// Poll status as one detection path (works when the connection
|
||||
// gets created server-side without a client-side callback).
|
||||
const statusPoller = setInterval(async () => {
|
||||
try {
|
||||
const sr = await fetch(`${API_BASE}/agents/subscriptions/status`);
|
||||
const sd = await sr.json();
|
||||
const connections = sd.providers?.connections || [];
|
||||
if (connections.some((p: any) => p.provider === providerId && p.isActive)) {
|
||||
clearInterval(statusPoller);
|
||||
pollTimerRef.current = null;
|
||||
if (msgHandlerRef.current) {
|
||||
window.removeEventListener('message', msgHandlerRef.current);
|
||||
msgHandlerRef.current = null;
|
||||
if (!exchanged) {
|
||||
exchanged = true;
|
||||
if (msgHandlerRef.current) {
|
||||
window.removeEventListener('message', msgHandlerRef.current);
|
||||
msgHandlerRef.current = null;
|
||||
}
|
||||
if (ipcUnsub) { ipcUnsub(); ipcUnsub = null; }
|
||||
clearInterval(statusPoller);
|
||||
pollTimerRef.current = null;
|
||||
trackEvent('onboarding.provider_connected', { provider: providerId });
|
||||
dismiss();
|
||||
}
|
||||
trackEvent('onboarding.provider_connected', { provider: providerId });
|
||||
dismiss();
|
||||
}
|
||||
} catch {}
|
||||
}, 2000);
|
||||
pollTimerRef.current = statusPoller;
|
||||
|
||||
// Also listen for postMessage from callback page (faster when it works)
|
||||
// postMessage from the popup's /callback page (faster when the
|
||||
// popup isn't cross-origin).
|
||||
const msgHandler = async (event: MessageEvent) => {
|
||||
const d = event.data;
|
||||
const callbackData = d?.type === 'oauth_callback' ? d.data : d;
|
||||
if (callbackData?.code) {
|
||||
window.removeEventListener('message', msgHandler);
|
||||
msgHandlerRef.current = null;
|
||||
if (pollTimerRef.current) { clearInterval(pollTimerRef.current); pollTimerRef.current = null; }
|
||||
if (popup && !popup.closed) popup.close();
|
||||
try {
|
||||
await fetch(`${API_BASE}/agents/subscriptions/exchange`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
provider: providerId,
|
||||
code: callbackData.code,
|
||||
redirect_uri: data.redirect_uri,
|
||||
code_verifier: data.code_verifier,
|
||||
state: callbackData.state || data.state,
|
||||
}),
|
||||
});
|
||||
} catch {}
|
||||
trackEvent('onboarding.provider_connected', { provider: providerId });
|
||||
dismiss();
|
||||
}
|
||||
if (callbackData?.code) await runExchange(callbackData.code, callbackData.state);
|
||||
};
|
||||
window.addEventListener('message', msgHandler);
|
||||
msgHandlerRef.current = msgHandler;
|
||||
|
||||
// Electron IPC fallback — main.js captures child webContents
|
||||
// navigating to localhost:20128/callback?code=... and forwards
|
||||
// the parsed params here. This is the REQUIRED path for Claude
|
||||
// OAuth in Electron (cross-origin redirects sever opener chain,
|
||||
// so postMessage can't fire). Without this listener, the
|
||||
// onboarding flow would only see the connection via the
|
||||
// 2-second status poll — but the code never gets exchanged
|
||||
// because the CLI callback page in the popup never reaches us.
|
||||
let ipcUnsub: (() => void) | null = null;
|
||||
const ow = (window as any).openswarm;
|
||||
if (ow && typeof ow.onOauthCallback === 'function') {
|
||||
ipcUnsub = ow.onOauthCallback(async (cb: { code?: string; state?: string; error?: string }) => {
|
||||
if (cb?.code) await runExchange(cb.code, cb.state);
|
||||
});
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
if (pollTimerRef.current) { clearInterval(pollTimerRef.current); pollTimerRef.current = null; }
|
||||
if (msgHandlerRef.current) { window.removeEventListener('message', msgHandlerRef.current); msgHandlerRef.current = null; }
|
||||
if (ipcUnsub) { ipcUnsub(); ipcUnsub = null; }
|
||||
setConnecting(null);
|
||||
}, 180000);
|
||||
|
||||
|
||||
@@ -1201,6 +1201,22 @@ const ChatInput = forwardRef<ChatInputHandle, Props>(({ onSend, disabled, mode,
|
||||
Thinking Level
|
||||
</Typography>
|
||||
</MenuItem>
|
||||
{/* Gemini 3 preview models conflict with web search when
|
||||
thinking is on — Gemini's API rejects with "thought
|
||||
signature is not valid" the next turn after a tool call.
|
||||
Surface a note here so users hit on search issues know
|
||||
which toggle to flip. */}
|
||||
{(() => {
|
||||
const isGemini3 = typeof model === 'string' && (model.includes('gemini-3') || (allModelOptions.flat.find((m: any) => m.value === model)?.label || '').toLowerCase().includes('gemini 3'));
|
||||
if (!isGemini3 || thinkingLevel === 'off') return null;
|
||||
return (
|
||||
<MenuItem disabled sx={{ opacity: '1 !important', py: 0.6, px: 1.5, minHeight: 'auto', pointerEvents: 'none', mx: 0.5, my: 0.25, borderRadius: 1, bgcolor: 'rgba(245, 158, 11, 0.06)', border: '1px solid rgba(245, 158, 11, 0.18)' }}>
|
||||
<Typography sx={{ fontSize: '0.66rem', color: c.text.muted, lineHeight: 1.4, whiteSpace: 'normal', maxWidth: 240 }}>
|
||||
Web search breaks on Gemini 3 preview while thinking is on. Set to <strong>Off</strong> if you need search.
|
||||
</Typography>
|
||||
</MenuItem>
|
||||
);
|
||||
})()}
|
||||
{levels.map((lvl) => (
|
||||
<MenuItem
|
||||
key={lvl.value}
|
||||
|
||||
@@ -1160,8 +1160,10 @@ const GenericMcpCard: React.FC<{ data: Record<string, any> }> = ({ data }) => {
|
||||
};
|
||||
|
||||
const McpResultCard: React.FC<{ parsed: ParsedMcpResult; compact?: boolean }> = ({ parsed, compact }) => {
|
||||
const c = useClaudeTokens();
|
||||
const tc = useTermColors();
|
||||
const { service, action, data } = parsed;
|
||||
const { TC_BODY } = useCardColors();
|
||||
const { service, action, data, rawText } = parsed;
|
||||
|
||||
if (data.error || data.is_error) {
|
||||
return (
|
||||
@@ -1177,6 +1179,35 @@ const McpResultCard: React.FC<{ parsed: ParsedMcpResult; compact?: boolean }> =
|
||||
if (service === 'calendar') return <CalendarCard data={data} hideHeader={compact} />;
|
||||
if (service === 'drive' || service === 'sheets') return <DriveCard data={data} />;
|
||||
|
||||
// Plain-text MCP results (our openswarm-web DDG search, fetch, etc.) arrive
|
||||
// as `[{type:"text", text:"..."}]` which the parser extracts into `rawText`
|
||||
// but leaves `data` empty. Render the rawText directly so users see the
|
||||
// actual tool output instead of "(empty response)". Display is capped —
|
||||
// the model still receives the full payload, only the UI preview is
|
||||
// trimmed so a 250 KB fetch doesn't blow up the chat bubble.
|
||||
const hasData = data && Object.keys(data).length > 0;
|
||||
if (!hasData && rawText && rawText.trim()) {
|
||||
const DISPLAY_CAP = 6000;
|
||||
const preview = rawText.length > DISPLAY_CAP
|
||||
? rawText.slice(0, DISPLAY_CAP) + `\n… (${rawText.length - DISPLAY_CAP} more chars — model received full output)`
|
||||
: rawText;
|
||||
return (
|
||||
<Box sx={{ px: 1.5, py: 1 }}>
|
||||
<span style={{
|
||||
color: TC_BODY,
|
||||
fontSize: '0.72rem',
|
||||
fontFamily: c.font.sans,
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordBreak: 'break-word',
|
||||
display: 'block',
|
||||
lineHeight: 1.55,
|
||||
}}>
|
||||
{preview}
|
||||
</span>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return <GenericMcpCard data={data} />;
|
||||
};
|
||||
|
||||
|
||||
@@ -84,7 +84,12 @@ const DEFAULT_MODEL_FALLBACK = [
|
||||
// ── Subscription Provider Card ──
|
||||
const SUBSCRIPTION_PROVIDERS = [
|
||||
{ id: 'claude', name: 'Claude Pro / Max', desc: 'Sonnet 4.6, Opus 4.6, Haiku 4.5', color: '#E8927A', preview: false },
|
||||
{ id: 'gemini-cli', name: 'Gemini Advanced', desc: 'Gemini 3 Pro, 3 Flash, 2.5 Pro, 2.5 Flash', color: '#4285F4', preview: false },
|
||||
// We route "Gemini" through Antigravity OAuth — same Google sign-in,
|
||||
// but a different backend lane with a much higher preview quota than
|
||||
// Gemini CLI's Code Assist free tier (which 429s after ~5 req/min).
|
||||
// Users with Google AI Pro/Ultra automatically get "priority" limits
|
||||
// on the Antigravity side; no extra action required from them.
|
||||
{ id: 'antigravity', name: 'Gemini Advanced', desc: 'Gemini 3 Pro, 3 Flash, 2.5 Pro, 2.5 Flash', color: '#4285F4', preview: false },
|
||||
{ id: 'codex', name: 'ChatGPT Plus / Pro', desc: 'GPT-5.4, GPT-5.4 Mini, GPT-5.3 Codex', color: '#74AA9C', preview: false },
|
||||
];
|
||||
|
||||
@@ -708,33 +713,54 @@ const SubscriptionCards: React.FC = () => {
|
||||
}, 2000);
|
||||
setPollTimer(statusPoller);
|
||||
|
||||
// postMessage listener — only wired up for the Electron popup flow
|
||||
// since the system-browser flow has no opener relationship.
|
||||
// Shared exchange helper — called from whichever relay path
|
||||
// (postMessage or Electron IPC) delivers the code first.
|
||||
let exchanged = false;
|
||||
const runExchange = async (code: string, state?: string) => {
|
||||
if (exchanged) return;
|
||||
exchanged = true;
|
||||
window.removeEventListener('message', msgHandler);
|
||||
if (ipcUnsub) ipcUnsub();
|
||||
clearInterval(statusPoller);
|
||||
setPollTimer(null);
|
||||
if (popup && !popup.closed) popup.close();
|
||||
try {
|
||||
await fetch(`${API_BASE}/agents/subscriptions/exchange`, {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
provider: providerId, code,
|
||||
redirect_uri: data.redirect_uri, code_verifier: data.code_verifier,
|
||||
state: state || data.state,
|
||||
}),
|
||||
});
|
||||
} catch {}
|
||||
setConnecting(null);
|
||||
fetchStatus();
|
||||
refreshPickerModels();
|
||||
};
|
||||
|
||||
// postMessage listener — works when the popup's /callback page can
|
||||
// reach window.opener. Silently no-ops on Anthropic flows where the
|
||||
// opener chain is severed by cross-origin redirects.
|
||||
const msgHandler = async (event: MessageEvent) => {
|
||||
const d = event.data;
|
||||
const callbackData = d?.type === 'oauth_callback' ? d.data : d;
|
||||
if (callbackData?.code) {
|
||||
window.removeEventListener('message', msgHandler);
|
||||
clearInterval(statusPoller);
|
||||
setPollTimer(null);
|
||||
if (popup && !popup.closed) popup.close();
|
||||
try {
|
||||
await fetch(`${API_BASE}/agents/subscriptions/exchange`, {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
provider: providerId, code: callbackData.code,
|
||||
redirect_uri: data.redirect_uri, code_verifier: data.code_verifier,
|
||||
state: callbackData.state || data.state,
|
||||
}),
|
||||
});
|
||||
} catch {}
|
||||
setConnecting(null);
|
||||
fetchStatus();
|
||||
refreshPickerModels();
|
||||
}
|
||||
if (callbackData?.code) await runExchange(callbackData.code, callbackData.state);
|
||||
};
|
||||
if (!useExternal) window.addEventListener('message', msgHandler);
|
||||
|
||||
// Electron IPC fallback — main.js captures any child webContents
|
||||
// navigating to localhost:20128/callback?code=... and forwards the
|
||||
// parsed params here, so we exchange the code even when opener
|
||||
// postMessage fails. No-op in non-Electron contexts.
|
||||
let ipcUnsub: (() => void) | null = null;
|
||||
const ow = (window as any).openswarm;
|
||||
if (ow && typeof ow.onOauthCallback === 'function') {
|
||||
ipcUnsub = ow.onOauthCallback(async (cb: { code?: string; state?: string; error?: string }) => {
|
||||
if (cb?.code) await runExchange(cb.code, cb.state);
|
||||
});
|
||||
}
|
||||
|
||||
// Timeout: 3 minutes for popup flow (was 30s — too short for 2FA /
|
||||
// slow networks, and on Windows postMessage from the callback popup
|
||||
// can silently fail due to COOP / opener severing, leaving the only
|
||||
@@ -748,6 +774,7 @@ const SubscriptionCards: React.FC = () => {
|
||||
clearInterval(statusPoller);
|
||||
setPollTimer(null);
|
||||
if (!useExternal) window.removeEventListener('message', msgHandler);
|
||||
if (ipcUnsub) ipcUnsub();
|
||||
setConnecting(null);
|
||||
}, timeoutMs);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user