[eric] remove vendored 9router (now fetched from npm at build), remove Copilot, and fix subs + tools

This commit is contained in:
ciregenz
2026-04-25 02:26:08 -07:00
parent 744a14dc47
commit f5cc3a36fb
510 changed files with 1736 additions and 73260 deletions
+64 -31
View File
@@ -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} />;
};
+49 -22
View File
@@ -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);