+ {modes.map((mode) => {
const isActive = activeModeId === mode.id;
const isSubmitting = submittingId === mode.id;
const error = errorByMode[mode.id];
diff --git a/frontend/components/AdminTabs/index.js b/frontend/components/AdminTabs/index.js
index 4775db1..a08b740 100644
--- a/frontend/components/AdminTabs/index.js
+++ b/frontend/components/AdminTabs/index.js
@@ -8,3 +8,4 @@ export { default as SkillsTab } from "./SkillsTab.jsx";
export { default as SessionsTab } from "./SessionsTab.jsx";
export { default as AdvancedTab } from "./AdvancedTab.jsx";
export { default as SandboxTab } from "./SandboxTab.jsx";
+export { default as AccountTab } from "./AccountTab.jsx";
diff --git a/frontend/components/AssistantMessage.jsx b/frontend/components/AssistantMessage.jsx
index 9a19965..ed96829 100644
--- a/frontend/components/AssistantMessage.jsx
+++ b/frontend/components/AssistantMessage.jsx
@@ -1,4 +1,5 @@
import React, { useState } from "react";
+import { apiUrl } from "../utils/api.js";
import PlanView from "./PlanView.jsx";
import RunnableCodeBlock, { splitFences } from "./RunnableCodeBlock.jsx";
import ExecutionPlanCard from "./ExecutionPlanCard.jsx";
@@ -35,7 +36,7 @@ export default function AssistantMessage({
const body = ep.file
? { language: ep.language, code: null }
: { language: ep.language, code: ep.inline_code, timeout_sec: ep.timeout_sec };
- const res = await fetch("/api/sandbox/run", {
+ const res = await fetch(apiUrl("/api/sandbox/run"), {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
diff --git a/frontend/components/AuthPage.jsx b/frontend/components/AuthPage.jsx
index e066e55..e6c0541 100644
--- a/frontend/components/AuthPage.jsx
+++ b/frontend/components/AuthPage.jsx
@@ -1,6 +1,7 @@
import React, { useEffect, useRef, useState } from "react";
import "./auth.css";
import { resolveBackendUrl } from "../utils/backend.js";
+import { getSessionToken, setSessionToken } from "../utils/api.js";
// Premium /auth — GitPilot account (email/password) + "Continue with GitHub".
// GitPilot account = who you are; GitHub = repository access (the device flow).
@@ -13,11 +14,23 @@ import { resolveBackendUrl } from "../utils/backend.js";
const api = (path) => `${resolveBackendUrl()}${path}`;
+// The account session also travels in a header (not just the cross-site cookie)
+// so email/password sign-in survives the Vercel-frontend / HF-backend split.
+function sessionHeaders() {
+ const t = getSessionToken();
+ return t ? { "X-GitPilot-Session": t } : {};
+}
+
+// Persist the portable session token returned by account endpoints.
+function rememberSession(data) {
+ if (data && data.session_token) setSessionToken(data.session_token);
+}
+
async function post(path, body) {
const r = await fetch(api(path), {
method: "POST",
- headers: { "content-type": "application/json" },
- credentials: "include", // receive/send the HttpOnly session cookie
+ headers: { "content-type": "application/json", ...sessionHeaders() },
+ credentials: "include", // also send/receive the session cookie when same-origin
body: JSON.stringify(body || {}),
});
let data = {};
@@ -26,7 +39,11 @@ async function post(path, body) {
}
async function getJSON(path, opts = {}) {
- const r = await fetch(api(path), { credentials: "include", ...opts });
+ const r = await fetch(api(path), {
+ credentials: "include",
+ ...opts,
+ headers: { ...sessionHeaders(), ...(opts.headers || {}) },
+ });
let data = {};
try { data = await r.json(); } catch { /* empty body */ }
return { ok: r.ok, status: r.status, data };
@@ -40,10 +57,19 @@ const Eye = ({ off }) => (off
:
);
-export default function AuthPage({ onAuthenticated, backendReady = false }) {
+export default function AuthPage({ onAuthenticated, backendReady = false, connectMode = false }) {
const params = new URLSearchParams(window.location.search);
+ const isVerifyLink =
+ !!params.get("token") && window.location.pathname.includes("verify-email");
+ // connectMode / ?view=github: jump straight to the GitHub device flow (used by
+ // the in-workspace "Connect GitHub" button for already-signed-in accounts).
+ const wantsGithub = !!params.get("code") || params.get("view") === "github" || connectMode;
const [mode, setMode] = useState(params.get("mode") === "signup" ? "signup" : "signin");
- const [view, setView] = useState("email"); // "email" | "github"
+ // Pick the opening view: GitHub flow → github; an email confirmation link
+ // (?token= on /verify-email) → verify; otherwise email.
+ const [view, setView] = useState(
+ wantsGithub ? "github" : isVerifyLink ? "verify" : "email"
+ ); // "email" | "github" | "verify"
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [name, setName] = useState("");
@@ -59,18 +85,45 @@ export default function AuthPage({ onAuthenticated, backendReady = false }) {
const stopPolling = useRef(false);
const ghStarted = useRef(false);
- // Email-verification links land here with ?token=...
+ // Email-verification state (dedicated screen, not a silent redirect)
+ const [verifyPhase, setVerifyPhase] = useState("verifying"); // "verifying" | "success" | "error"
+ const [verifyMsg, setVerifyMsg] = useState("");
+ const [verifyUser, setVerifyUser] = useState(null);
+ const [resendBusy, setResendBusy] = useState(false);
+ const verifyStarted = useRef(false);
+
+ // Exchange the email-confirmation token once, then show success/error in place.
useEffect(() => {
+ if (view !== "verify" || verifyStarted.current) return;
+ verifyStarted.current = true;
const token = params.get("token");
- if (token && window.location.pathname.includes("verify-email")) {
- (async () => {
- const r = await post("/api/account/verify-email", { token });
- if (r.ok && typeof onAuthenticated === "function") onAuthenticated({ user: r.data });
- else setNote({ kind: "err", text: r.data.detail || "Invalid or expired link." });
- })();
- }
+ setVerifyPhase("verifying");
+ (async () => {
+ const r = await post("/api/account/verify-email", { token });
+ // Strip the token from the URL so a refresh can't replay a stale link.
+ window.history.replaceState({}, document.title, "/auth");
+ if (r.ok) {
+ rememberSession(r.data);
+ setVerifyUser(r.data);
+ setVerifyPhase("success");
+ } else {
+ setVerifyMsg(r.data.detail || "This confirmation link is invalid or has expired.");
+ setVerifyPhase("error");
+ }
+ })();
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, []);
+ }, [view]);
+
+ const resendVerification = async () => {
+ if (!email) { setNote({ kind: "err", text: "Enter your email to resend the link." }); return; }
+ setResendBusy(true);
+ try {
+ await post("/api/account/resend-verification", { email });
+ setNote({ kind: "ok", text: "If your account needs confirmation, a new link is on its way." });
+ } finally {
+ setResendBusy(false);
+ }
+ };
// ── GitHub authorization (device + web OAuth), all inside the same card ──
function finishGitHub(data) {
@@ -88,6 +141,10 @@ export default function AuthPage({ onAuthenticated, backendReady = false }) {
if (typeof onAuthenticated === "function") {
onAuthenticated({ access_token: data.access_token, user: data.user });
}
+ // Linked from inside the workspace → reload into it with the new GitHub token.
+ if (connectMode) {
+ window.location.href = "/";
+ }
}
async function pollDevice(deviceCode, interval) {
@@ -196,7 +253,10 @@ export default function AuthPage({ onAuthenticated, backendReady = false }) {
try {
if (mode === "signin") {
const r = await post("/api/account/login", { email, password });
- if (r.ok && typeof onAuthenticated === "function") onAuthenticated({ user: r.data });
+ if (r.ok) {
+ rememberSession(r.data);
+ if (typeof onAuthenticated === "function") onAuthenticated({ user: r.data });
+ }
else if (r.status === 403) setNote({ kind: "err", text: "Verify your email before signing in." });
else setNote({ kind: "err", text: r.data.detail || "Invalid email or password." });
} else {
@@ -217,13 +277,78 @@ export default function AuthPage({ onAuthenticated, backendReady = false }) {
setNote({ kind: "ok", text: "If an account exists, we'll send a reset link." });
};
+ // ── Email-confirmation card (landed from the verify-email link) ──
+ if (view === "verify") {
+ return (
+
+
← Back to home
+
+
GP
+
+ {verifyPhase === "verifying" && (
+ <>
+
Confirming your email…
+
Activating your GitPilot account. This only takes a moment.
+
+ >
+ )}
+
+ {verifyPhase === "success" && (
+ <>
+
✓
+
Email confirmed
+
+ Welcome{verifyUser?.name ? `, ${verifyUser.name}` : ""}! Your GitPilot
+ account is active.
+
+
+ >
+ )}
+
+ {verifyPhase === "error" && (
+ <>
+
Confirmation link problem
+
{verifyMsg}
+
Links expire after 15 minutes. Enter your email to get a fresh one.
+ {note &&
{note.text}
}
+
+ setEmail(e.target.value)}
+ placeholder="Email"
+ />
+
+
+
+ >
+ )}
+
+
© {new Date().getFullYear()} GitPilot Inc.
+
+
+ );
+ }
+
// ── GitHub authorization card (same shell, no nested rectangle) ──
if (view === "github") {
return (
← Back to home
-
+ {connectMode
+ ?
+ :
}
GP
{ghPhase === "connecting" && (
diff --git a/frontend/components/ChatPanel.jsx b/frontend/components/ChatPanel.jsx
index 5424b78..de39b4e 100644
--- a/frontend/components/ChatPanel.jsx
+++ b/frontend/components/ChatPanel.jsx
@@ -1,4 +1,5 @@
import { resolveBackendUrl } from "../utils/backend.js";
+import { apiUrl } from "../utils/api.js";
// frontend/components/ChatPanel.jsx
import React, { useEffect, useRef, useState } from "react";
import AssistantMessage from "./AssistantMessage.jsx";
@@ -255,7 +256,7 @@ export default function ChatPanel({
const url = `/api/repos/${repo.owner}/${repo.name}/file`
+ `?path=${encodeURIComponent(path)}`
+ `&ref=${encodeURIComponent(branch)}`;
- const res = await fetch(url, { headers: getHeaders() });
+ const res = await fetch(apiUrl(url), { headers: getHeaders() });
const data = await res.json().catch(() => ({}));
if (!res.ok) {
setCanvasError(data.detail || `Could not load ${path} (HTTP ${res.status})`);
@@ -303,7 +304,7 @@ export default function ChatPanel({
const url = `/api/repos/${repo.owner}/${repo.name}/file`
+ `?path=${encodeURIComponent(path)}`
+ `&ref=${encodeURIComponent(branch)}`;
- const res = await fetch(url, { headers: getHeaders() });
+ const res = await fetch(apiUrl(url), { headers: getHeaders() });
const data = await res.json().catch(() => ({}));
return { res, data };
};
@@ -412,7 +413,7 @@ export default function ChatPanel({
if (metadata && typeof metadata === "object" && Object.keys(metadata).length > 0) {
body.metadata = metadata;
}
- fetch(`/api/sessions/${sid}/message`, {
+ fetch(apiUrl(`/api/sessions/${sid}/message`), {
method: "POST",
headers: getHeaders(),
body: JSON.stringify(body),
@@ -498,7 +499,7 @@ export default function ChatPanel({
let res;
try {
- res = await fetch("/api/chat/plan", {
+ res = await fetch(apiUrl("/api/chat/plan"), {
method: "POST",
headers: getHeaders(),
body: JSON.stringify({
@@ -713,7 +714,7 @@ export default function ChatPanel({
// - If already on AI branch -> currentBranch (backend updates existing)
const branch_name = safeCurrent === safeDefault ? undefined : safeCurrent;
- const res = await fetch("/api/chat/execute", {
+ const res = await fetch(apiUrl("/api/chat/execute"), {
method: "POST",
headers: getHeaders(),
body: JSON.stringify({
diff --git a/frontend/components/ContextMeter.jsx b/frontend/components/ContextMeter.jsx
index acd60ea..8504dfc 100644
--- a/frontend/components/ContextMeter.jsx
+++ b/frontend/components/ContextMeter.jsx
@@ -17,6 +17,7 @@
//