diff --git a/apps/chrome-extension/.gitignore b/apps/chrome-extension/.gitignore index b0e4c33b1..90d11dfbc 100644 --- a/apps/chrome-extension/.gitignore +++ b/apps/chrome-extension/.gitignore @@ -3,4 +3,5 @@ dist/ .env.chrome .env.development .env -.env.production \ No newline at end of file +.env.production +.env.staging \ No newline at end of file diff --git a/apps/chrome-extension/package.json b/apps/chrome-extension/package.json index 927e6a697..dc9725a26 100644 --- a/apps/chrome-extension/package.json +++ b/apps/chrome-extension/package.json @@ -6,6 +6,7 @@ "scripts": { "dev": "vite build --watch --mode development", "build": "vite build", + "build:staging": "vite build --mode staging", "preview": "vite preview" }, "dependencies": { diff --git a/apps/chrome-extension/src/shared/messaging.ts b/apps/chrome-extension/src/shared/messaging.ts index 086054511..fba508bf3 100644 --- a/apps/chrome-extension/src/shared/messaging.ts +++ b/apps/chrome-extension/src/shared/messaging.ts @@ -224,11 +224,7 @@ export type ExtensionMessage = | RequestAnnotationsMessage | RequestToolbarStateMessage; -export const EVA_URL = - typeof chrome !== "undefined" && - chrome.runtime?.getManifest?.()?.version_name === "development" - ? "http://localhost:3000" - : "https://eva-git-staging-vedantb.vercel.app"; +export const EVA_URL = import.meta.env.VITE_EVA_URL; export function isSessionId(value: unknown): value is Id<"sessions"> { return typeof value === "string" && value.length > 0; diff --git a/apps/chrome-extension/src/sidepanel/App.tsx b/apps/chrome-extension/src/sidepanel/App.tsx index c34aaee3d..3216647d6 100644 --- a/apps/chrome-extension/src/sidepanel/App.tsx +++ b/apps/chrome-extension/src/sidepanel/App.tsx @@ -56,9 +56,6 @@ if (!PUBLISHABLE_KEY) { } const EXTENSION_URL = chrome.runtime.getURL("."); -const SYNC_HOST = import.meta.env.DEV - ? "http://localhost:3000" - : "https://eva-git-staging-vedantb.vercel.app"; function getHostFromUrl(url: string): string | null { try { @@ -847,7 +844,7 @@ function AuthenticatedApp() { function SignInScreen() { const handleOpenWebApp = () => { - chrome.tabs.create({ url: SYNC_HOST }); + chrome.tabs.create({ url: EVA_URL }); }; return ( @@ -876,7 +873,7 @@ export default function App() { afterSignOutUrl={`${EXTENSION_URL}/sidepanel.html`} signInFallbackRedirectUrl={`${EXTENSION_URL}/sidepanel.html`} signUpFallbackRedirectUrl={`${EXTENSION_URL}/sidepanel.html`} - syncHost={SYNC_HOST} + syncHost={EVA_URL} > diff --git a/apps/chrome-extension/src/sidepanel/components/ChatPanel.tsx b/apps/chrome-extension/src/sidepanel/components/ChatPanel.tsx index 6adc5edf3..790ab0fc8 100644 --- a/apps/chrome-extension/src/sidepanel/components/ChatPanel.tsx +++ b/apps/chrome-extension/src/sidepanel/components/ChatPanel.tsx @@ -19,8 +19,6 @@ import { Message as AIMessage, MessageContent, MessageResponse, - Reasoning, - ReasoningTrigger, PromptInput, PromptInputTextarea, PromptInputFooter, @@ -209,7 +207,6 @@ export function ChatPanel({ mode: "ask", model, responseLength, - installationId: selectedRepo.installationId, }); } catch (error) { await appendMessage({ @@ -409,33 +406,21 @@ export function ChatPanel({ } > {message.role === "assistant" && !message.content ? ( - (() => { - const steps = parseActivitySteps(streamingActivity); - return steps ? ( - - ) : ( - - - streaming ? "Working..." : "Processing complete" - } - /> - -
-                                {streamingActivity ||
-                                  message.activityLog ||
-                                  "Starting..."}
-                              
-
-
- ); - })() + ) : ( <> {message.role === "assistant" ? ( @@ -453,18 +438,7 @@ export function ChatPanel({ startedAt={message.timestamp} duration={duration} /> - ) : ( - - "View logs"} - /> - -
-                                        {message.activityLog}
-                                      
-
-
- ); + ) : null; })()} {"imageUrl" in message && message.imageUrl && ( {(() => { - const steps = parseActivitySteps(streamingActivity); const loadingEvaIcon = ( ); const lastMsg = messages[messages.length - 1]; - return steps ? ( + return ( - ) : ( - - - streaming ? "Working..." : "Processing complete" - } - /> - -
-                            {streamingActivity || "Starting..."}
-                          
-
-
); })()}
diff --git a/apps/web-v2/package.json b/apps/web-v2/package.json index 0bebb392b..95c85e492 100644 --- a/apps/web-v2/package.json +++ b/apps/web-v2/package.json @@ -47,6 +47,7 @@ "@tabler/icons-react": "^3.33.0", "@tanstack/react-hotkeys": "^0.3.2", "@tanstack/react-router": "^1.120.0", + "@tanstack/router-zod-adapter": "^1.81.5", "@tiptap/core": "^3.20.1", "@tiptap/extension-underline": "^3.20.1", "@tiptap/pm": "^3.20.1", diff --git a/apps/web-v2/src/lib/components/PreviewNavBar.tsx b/apps/web-v2/src/lib/components/PreviewNavBar.tsx index df0535fe4..4b8cbcc74 100644 --- a/apps/web-v2/src/lib/components/PreviewNavBar.tsx +++ b/apps/web-v2/src/lib/components/PreviewNavBar.tsx @@ -23,8 +23,9 @@ function getPathFromUrl(fullUrl: string): string { function buildUrlWithPath(baseUrl: string, path: string): string { try { const parsed = new URL(baseUrl); - parsed.pathname = path.startsWith("/") ? path : `/${path}`; - return parsed.toString(); + if (parsed.protocol === "http:") parsed.protocol = "https:"; + const fullPath = path.startsWith("/") ? path : `/${path}`; + return `${parsed.origin}${fullPath}`; } catch { return baseUrl; } diff --git a/apps/web-v2/src/lib/components/Sidebar.tsx b/apps/web-v2/src/lib/components/Sidebar.tsx index 856b8ea8e..5fad32e42 100644 --- a/apps/web-v2/src/lib/components/Sidebar.tsx +++ b/apps/web-v2/src/lib/components/Sidebar.tsx @@ -31,6 +31,7 @@ import { IconTool, IconX, } from "@tabler/icons-react"; +import { clientEnv } from "@/env/client"; import { api } from "@conductor/backend"; import { Button, Spinner, cn } from "@conductor/ui"; import { ActiveTasksBadge } from "@/lib/components/sidebar/ActiveTasksPopover"; @@ -59,6 +60,7 @@ const KNOWN_SUB_PAGES = new Set([ "testing-arena", "stats", "automations", + "inbox", ]); const CONTEXT_SIDEBAR_BY_NAV_NAME = { @@ -175,99 +177,112 @@ export function Sidebar() { owner && repoName ? { owner, name: repoName, appName } : "skip", ); - const repoNavigation = useMemo( - () => - isRepoRoute && repoBasePath - ? [ - { - label: "BUILD", - groupIcon: IconHammer, - items: [ - { - name: "Projects", - href: `${repoBasePath}/projects`, - icon: IconLayoutKanban, - }, - { - name: "Designs", - href: `${repoBasePath}/designs`, - icon: IconPalette, - }, - ], - }, - { - label: "FIX", - groupIcon: IconTool, - items: [ - { - name: "Quick Tasks", - href: `${repoBasePath}/quick-tasks`, - icon: IconChecklist, - }, - { - name: "Sessions", - href: `${repoBasePath}/sessions`, - icon: IconTerminal2, - }, - ], - }, - { - label: "TEST", - groupIcon: IconTestPipe, - items: [ - { - name: "Documents", - href: `${repoBasePath}/docs`, - icon: IconFileText, - }, - { - name: "Testing Arena", - href: `${repoBasePath}/testing-arena`, - icon: IconFlask, - }, - ], - }, - { - label: "DATA", - groupIcon: IconChartBar, - items: [ - { - name: "Analyse", - href: `${repoBasePath}/analyse`, - icon: IconBrain, - }, - ], - }, - { - label: "SETTINGS", - groupIcon: IconSettings, - items: [ - { - name: "Inbox", - href: `${repoBasePath}/inbox`, - icon: IconInbox, - }, - { - name: "Automations", - href: `${repoBasePath}/automations`, - icon: IconPlayerPlay, - }, - { - name: "Stats", - href: `${repoBasePath}/stats`, - icon: IconChartBar, - }, - { - name: "Settings", - href: `${repoBasePath}/settings/config`, - icon: IconSettings, - }, - ], - }, - ] - : [], - [repoBasePath, isRepoRoute], - ); + const isDev = clientEnv.VITE_ENV === "development"; + + const repoNavigation = useMemo(() => { + if (!isRepoRoute || !repoBasePath) return []; + const allGroups = [ + { + label: "BUILD", + groupIcon: IconHammer, + items: [ + { + name: "Projects", + href: `${repoBasePath}/projects`, + icon: IconLayoutKanban, + }, + { + name: "Designs", + href: `${repoBasePath}/designs`, + icon: IconPalette, + devOnly: true, + }, + ], + }, + { + label: "FIX", + groupIcon: IconTool, + items: [ + { + name: "Quick Tasks", + href: `${repoBasePath}/quick-tasks`, + icon: IconChecklist, + }, + { + name: "Sessions", + href: `${repoBasePath}/sessions`, + icon: IconTerminal2, + }, + ], + }, + { + label: "TEST", + groupIcon: IconTestPipe, + devOnly: true, + items: [ + { + name: "Documents", + href: `${repoBasePath}/docs`, + icon: IconFileText, + devOnly: true, + }, + { + name: "Testing Arena", + href: `${repoBasePath}/testing-arena`, + icon: IconFlask, + devOnly: true, + }, + ], + }, + { + label: "DATA", + groupIcon: IconChartBar, + devOnly: true, + items: [ + { + name: "Analyse", + href: `${repoBasePath}/analyse`, + icon: IconBrain, + devOnly: true, + }, + ], + }, + { + label: "SETTINGS", + groupIcon: IconSettings, + items: [ + { + name: "Inbox", + href: `${repoBasePath}/inbox`, + icon: IconInbox, + }, + { + name: "Automations", + href: `${repoBasePath}/automations`, + icon: IconPlayerPlay, + }, + { + name: "Stats", + href: `${repoBasePath}/stats`, + icon: IconChartBar, + }, + { + name: "Settings", + href: `${repoBasePath}/settings/config`, + icon: IconSettings, + }, + ], + }, + ]; + if (isDev) return allGroups; + return allGroups + .filter((g) => !g.devOnly) + .map((g) => ({ + ...g, + items: g.items.filter((i) => !i.devOnly), + })) + .filter((g) => g.items.length > 0); + }, [repoBasePath, isRepoRoute, isDev]); const { theme, toggleTheme } = useThemeContext(); diff --git a/apps/web-v2/src/lib/components/quick-tasks/QuickTaskCard.tsx b/apps/web-v2/src/lib/components/quick-tasks/QuickTaskCard.tsx index 02540868f..89ad0eee3 100644 --- a/apps/web-v2/src/lib/components/quick-tasks/QuickTaskCard.tsx +++ b/apps/web-v2/src/lib/components/quick-tasks/QuickTaskCard.tsx @@ -40,7 +40,7 @@ import { statusConfig, type TaskStatus, } from "@/lib/components/tasks/TaskStatusBadge"; -import dayjs from "@conductor/shared/dates"; +import dayjs, { compactRelativeTime } from "@conductor/shared/dates"; import { useState } from "react"; import { useRepo } from "@/lib/contexts/RepoContext"; import { DeleteTaskDialog } from "./_components/DeleteTaskDialog"; @@ -204,7 +204,7 @@ export function QuickTaskCard({
- {dayjs(createdAt).fromNow()} + {compactRelativeTime(createdAt)} diff --git a/apps/web-v2/src/lib/history.ts b/apps/web-v2/src/lib/history.ts index 991e5d753..199bb81c5 100644 --- a/apps/web-v2/src/lib/history.ts +++ b/apps/web-v2/src/lib/history.ts @@ -87,7 +87,7 @@ function toDisplayHref(href: string): string { export function createAppHistory() { return createBrowserHistory({ parseLocation() { - const raw = `${window.location.pathname}${window.location.search}${window.location.hash}`; + const raw = `${decodeURIComponent(window.location.pathname)}${window.location.search}${window.location.hash}`; const internal = toInternalHref(raw); const hashIndex = internal.indexOf("#"); diff --git a/apps/web-v2/src/lib/utils/dismissDaytonaWarning.ts b/apps/web-v2/src/lib/utils/dismissDaytonaWarning.ts index f16beef88..24e46d98c 100644 --- a/apps/web-v2/src/lib/utils/dismissDaytonaWarning.ts +++ b/apps/web-v2/src/lib/utils/dismissDaytonaWarning.ts @@ -1,13 +1,16 @@ const dismissed = new Set(); export async function dismissDaytonaWarning(url: string): Promise { - const origin = new URL(url).origin; + const parsed = new URL(url); + if (parsed.protocol === "http:") parsed.protocol = "https:"; + const httpsUrl = parsed.toString(); + const origin = parsed.origin; if (dismissed.has(origin)) return; try { - await fetch(url, { + await fetch(httpsUrl, { method: "HEAD", + mode: "no-cors", headers: { "X-Daytona-Skip-Preview-Warning": "true" }, - credentials: "include", }); dismissed.add(origin); } catch { diff --git a/apps/web-v2/src/routes/_global/home/ReposClient.tsx b/apps/web-v2/src/routes/_global/home/ReposClient.tsx index eb55efd3d..02fa53208 100644 --- a/apps/web-v2/src/routes/_global/home/ReposClient.tsx +++ b/apps/web-v2/src/routes/_global/home/ReposClient.tsx @@ -50,9 +50,10 @@ export function ReposClient() { const groupedRepos = repos ? repos.reduce>((groups, repo) => { - const groupKey = repo.teamId - ? (teams.find((t) => t._id === repo.teamId)?.name ?? "Unknown Team") - : "Personal"; + const team = repo.teamId + ? teams.find((t) => t._id === repo.teamId) + : undefined; + const groupKey = team ? (team.displayName ?? team.name) : "My Team"; if (!groups[groupKey]) { groups[groupKey] = []; } @@ -62,14 +63,14 @@ export function ReposClient() { : {}; const groupNames = Object.keys(groupedRepos).sort((a, b) => { - if (a === "Personal") return -1; - if (b === "Personal") return 1; + if (a === "My Team") return -1; + if (b === "My Team") return 1; return a.localeCompare(b); }); return ( {hasRepos && } diff --git a/apps/web-v2/src/routes/_global/home/_components/EmptyOnboarding.tsx b/apps/web-v2/src/routes/_global/home/_components/EmptyOnboarding.tsx index cb70e9cd5..82ff16398 100644 --- a/apps/web-v2/src/routes/_global/home/_components/EmptyOnboarding.tsx +++ b/apps/web-v2/src/routes/_global/home/_components/EmptyOnboarding.tsx @@ -63,8 +63,8 @@ export function EmptyOnboarding({ connectUrl }: { connectUrl: string }) { Connect your GitHub

- Link your repositories to unlock Eva's AI tools for planning, coding, - and shipping features autonomously. + Link your codebases to unlock Eva's AI tools for planning, coding, and + shipping features autonomously.

@@ -153,7 +153,7 @@ export function RepoSetupClient({ GitHub App Installed

- Select which repositories you want to add to Eva. + Select which codebases you want to add to Eva.

diff --git a/apps/web-v2/src/routes/_global/teams/TeamDetailClient.tsx b/apps/web-v2/src/routes/_global/teams/TeamDetailClient.tsx index b723cba8a..e89877307 100644 --- a/apps/web-v2/src/routes/_global/teams/TeamDetailClient.tsx +++ b/apps/web-v2/src/routes/_global/teams/TeamDetailClient.tsx @@ -43,7 +43,7 @@ export function TeamDetailClient({ teamId }: TeamDetailClientProps) { const isOwner = team.userRole === "owner"; return ( - + { @@ -52,7 +52,7 @@ export function TeamDetailClient({ teamId }: TeamDetailClientProps) { > Members - Repositories + Codebases Environment Variables diff --git a/apps/web-v2/src/routes/_global/teams/TeamsClient.tsx b/apps/web-v2/src/routes/_global/teams/TeamsClient.tsx index 0b8c90c4c..f9e096115 100644 --- a/apps/web-v2/src/routes/_global/teams/TeamsClient.tsx +++ b/apps/web-v2/src/routes/_global/teams/TeamsClient.tsx @@ -74,7 +74,7 @@ export function TeamsClient() {

- Manage your teams and collaborate on repositories + Manage your teams and collaborate on codebases

@@ -144,7 +144,9 @@ export function TeamsClient() {
- {team.name} + + {team.displayName ?? team.name} +
{team.userRole} @@ -162,7 +164,7 @@ export function TeamsClient() {

No teams yet

- Create a team to collaborate on repositories + Create a team to collaborate on codebases

{isApp && ( @@ -100,20 +94,6 @@ export function AuditsClient() { ); } -function ReadOnlyCategoryRow({ category }: { category: Category }) { - return ( -
- -
-

{category.name}

-

- {category.description} -

-
-
- ); -} - function CategoryRow({ category, onToggle, diff --git a/apps/web-v2/src/routes/_repo/$owner/$repo/settings/MonorepoClient.tsx b/apps/web-v2/src/routes/_repo/$owner/$repo/settings/MonorepoClient.tsx index c854264ed..6f679d62e 100644 --- a/apps/web-v2/src/routes/_repo/$owner/$repo/settings/MonorepoClient.tsx +++ b/apps/web-v2/src/routes/_repo/$owner/$repo/settings/MonorepoClient.tsx @@ -172,7 +172,7 @@ export function MonorepoClient() { {repo.owner}/{repo.name} {" "} - for workspace apps and add them as separate repositories. + for workspace apps and add them as separate codebases.

diff --git a/apps/web-v2/src/routes/_repo/$owner/$repo/settings/TeamEnvVarsClient.tsx b/apps/web-v2/src/routes/_repo/$owner/$repo/settings/TeamEnvVarsClient.tsx index c5fd02ee4..eee0c90a3 100644 --- a/apps/web-v2/src/routes/_repo/$owner/$repo/settings/TeamEnvVarsClient.tsx +++ b/apps/web-v2/src/routes/_repo/$owner/$repo/settings/TeamEnvVarsClient.tsx @@ -46,7 +46,7 @@ export function TeamEnvVarsClient() {
{ if (!repo.teamId) return; await upsertTeamVar({ @@ -76,7 +76,8 @@ export function TeamEnvVarsClient() {

- Team: {team.name} + Team:{" "} + {team.displayName ?? team.name}

) => ({ - agent: search.agent === "" || search.agent === "true" ? true : undefined, - }), component: LandingPage, }); function LandingPage() { const { isSignedIn, isLoaded } = useAuth(); const navigate = useNavigate(); - const { agent } = useSearch({ from: "/" }); + const hasAgent = new URLSearchParams(window.location.search).has("agent"); useEffect(() => { if (!isLoaded) return; @@ -26,12 +23,10 @@ function LandingPage() { return; } - if (agent) { - console.log( - "[agent-auth] ?agent detected but HTTP action not built yet — skipping redirect", - ); + if (hasAgent) { + window.location.href = "/api/auth/agent-login"; } - }, [isLoaded, isSignedIn, agent, navigate]); + }, [isLoaded, isSignedIn, hasAgent, navigate]); return (
diff --git a/apps/web-v2/vercel.json b/apps/web-v2/vercel.json index 20a236ae3..c291b59d7 100644 --- a/apps/web-v2/vercel.json +++ b/apps/web-v2/vercel.json @@ -9,6 +9,15 @@ "value": "upgrade-insecure-requests" } ] + }, + { + "source": "/:owner/:repo/:extra/sessions/:path*", + "headers": [ + { + "key": "Content-Security-Policy", + "value": "upgrade-insecure-requests" + } + ] } ] } diff --git a/apps/web-v2/vite.config.ts b/apps/web-v2/vite.config.ts index 2e8d81e8c..f76dfc64b 100644 --- a/apps/web-v2/vite.config.ts +++ b/apps/web-v2/vite.config.ts @@ -1,8 +1,71 @@ -import { defineConfig } from "vite"; +import { defineConfig, loadEnv, type Plugin } from "vite"; import react from "@vitejs/plugin-react"; import { TanStackRouterVite } from "@tanstack/router-plugin/vite"; import path from "path"; +function agentLoginPlugin(): Plugin { + let env: Record; + + return { + name: "agent-login", + configureServer(server) { + env = loadEnv("development", server.config.root, ""); + + server.middlewares.use("/api/auth/agent-login", async (_req, res) => { + const secretKey = env.CLERK_SECRET_KEY; + const agentUserId = env.AGENT_CLERK_USER_ID; + + if (!secretKey || !agentUserId) { + res.writeHead(500, { "Content-Type": "application/json" }); + res.end( + JSON.stringify({ + error: + "CLERK_SECRET_KEY and AGENT_CLERK_USER_ID must be set in .env.local", + }), + ); + return; + } + + const resp = await fetch("https://api.clerk.com/v1/sign_in_tokens", { + method: "POST", + headers: { + Authorization: `Bearer ${secretKey}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + user_id: agentUserId, + expires_in_seconds: 60, + }), + }); + + if (!resp.ok) { + res.writeHead(502, { "Content-Type": "application/json" }); + res.end( + JSON.stringify({ + error: "Failed to create sign-in token", + details: await resp.text(), + }), + ); + return; + } + + const data = await resp.json(); + const token = data.token; + if (typeof token !== "string") { + res.writeHead(502, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ error: "No token in Clerk response" })); + return; + } + + res.writeHead(302, { + Location: `/agent-callback?ticket=${encodeURIComponent(token)}`, + }); + res.end(); + }); + }, + }; +} + export default defineConfig({ plugins: [ TanStackRouterVite({ @@ -10,7 +73,12 @@ export default defineConfig({ routeFileIgnorePattern: "(_components/|_utils\\.ts|Client\\.tsx)", }), react(), + agentLoginPlugin(), ], + server: { + host: "0.0.0.0", + cors: false, + }, resolve: { alias: { "@": path.resolve(__dirname, "./src"), diff --git a/apps/web/app/(global)/home/ReposClient.tsx b/apps/web/app/(global)/home/ReposClient.tsx index 4341cc222..4ec45e2b1 100644 --- a/apps/web/app/(global)/home/ReposClient.tsx +++ b/apps/web/app/(global)/home/ReposClient.tsx @@ -52,9 +52,10 @@ export function ReposClient() { const groupedRepos = repos ? repos.reduce>((groups, repo) => { - const groupKey = repo.teamId - ? (teams.find((t) => t._id === repo.teamId)?.name ?? "Unknown Team") - : "Personal"; + const team = repo.teamId + ? teams.find((t) => t._id === repo.teamId) + : undefined; + const groupKey = team ? (team.displayName ?? team.name) : "My Team"; if (!groups[groupKey]) { groups[groupKey] = []; } @@ -64,14 +65,14 @@ export function ReposClient() { : {}; const groupNames = Object.keys(groupedRepos).sort((a, b) => { - if (a === "Personal") return -1; - if (b === "Personal") return 1; + if (a === "My Team") return -1; + if (b === "My Team") return 1; return a.localeCompare(b); }); return ( {hasRepos && } diff --git a/apps/web/app/(global)/home/_components/EmptyOnboarding.tsx b/apps/web/app/(global)/home/_components/EmptyOnboarding.tsx index cb70e9cd5..82ff16398 100644 --- a/apps/web/app/(global)/home/_components/EmptyOnboarding.tsx +++ b/apps/web/app/(global)/home/_components/EmptyOnboarding.tsx @@ -63,8 +63,8 @@ export function EmptyOnboarding({ connectUrl }: { connectUrl: string }) { Connect your GitHub

- Link your repositories to unlock Eva's AI tools for planning, coding, - and shipping features autonomously. + Link your codebases to unlock Eva's AI tools for planning, coding, and + shipping features autonomously.

@@ -153,7 +153,7 @@ export function RepoSetupClient({ installationId }: RepoSetupClientProps) { GitHub App Installed

- Select which repositories you want to add to Eva. + Select which codebases you want to add to Eva.

diff --git a/apps/web/app/(global)/teams/TeamsClient.tsx b/apps/web/app/(global)/teams/TeamsClient.tsx index ac4e717e1..cf280a56d 100644 --- a/apps/web/app/(global)/teams/TeamsClient.tsx +++ b/apps/web/app/(global)/teams/TeamsClient.tsx @@ -77,7 +77,7 @@ export function TeamsClient() {

- Manage your teams and collaborate on repositories + Manage your teams and collaborate on codebases

@@ -147,7 +147,9 @@ export function TeamsClient() {
- {team.name} + + {team.displayName ?? team.name} +
{team.userRole} @@ -165,7 +167,7 @@ export function TeamsClient() {

No teams yet

- Create a team to collaborate on repositories + Create a team to collaborate on codebases

{isApp && ( @@ -100,20 +94,6 @@ export function AuditsClient() { ); } -function ReadOnlyCategoryRow({ category }: { category: Category }) { - return ( -
- -
-

{category.name}

-

- {category.description} -

-
-
- ); -} - function CategoryRow({ category, onToggle, diff --git a/apps/web/app/(repo)/[owner]/[repo]/settings/env-variables/TeamEnvVarsClient.tsx b/apps/web/app/(repo)/[owner]/[repo]/settings/env-variables/TeamEnvVarsClient.tsx index 1b05638af..bec348939 100644 --- a/apps/web/app/(repo)/[owner]/[repo]/settings/env-variables/TeamEnvVarsClient.tsx +++ b/apps/web/app/(repo)/[owner]/[repo]/settings/env-variables/TeamEnvVarsClient.tsx @@ -47,7 +47,7 @@ export function TeamEnvVarsClient() {
{ if (!repo.teamId) return; await upsertTeamVar({ @@ -77,7 +77,8 @@ export function TeamEnvVarsClient() {

- Team: {team.name} + Team:{" "} + {team.displayName ?? team.name}

{repo.owner}/{repo.name} {" "} - for workspace apps and add them as separate repositories. + for workspace apps and add them as separate codebases.

diff --git a/apps/web/lib/components/Sidebar.tsx b/apps/web/lib/components/Sidebar.tsx index f1f1b2100..caf5dcf2f 100644 --- a/apps/web/lib/components/Sidebar.tsx +++ b/apps/web/lib/components/Sidebar.tsx @@ -175,99 +175,112 @@ export function Sidebar() { owner && repoName ? { owner, name: repoName, appName } : "skip", ); - const repoNavigation = useMemo( - () => - isRepoRoute && repoBasePath - ? [ - { - label: "BUILD", - groupIcon: IconHammer, - items: [ - { - name: "Projects", - href: `${repoBasePath}/projects`, - icon: IconLayoutKanban, - }, - { - name: "Designs", - href: `${repoBasePath}/designs`, - icon: IconPalette, - }, - ], - }, - { - label: "FIX", - groupIcon: IconTool, - items: [ - { - name: "Quick Tasks", - href: `${repoBasePath}/quick-tasks`, - icon: IconChecklist, - }, - { - name: "Sessions", - href: `${repoBasePath}/sessions`, - icon: IconTerminal2, - }, - ], - }, - { - label: "TEST", - groupIcon: IconTestPipe, - items: [ - { - name: "Documents", - href: `${repoBasePath}/docs`, - icon: IconFileText, - }, - { - name: "Testing Arena", - href: `${repoBasePath}/testing-arena`, - icon: IconFlask, - }, - ], - }, - { - label: "DATA", - groupIcon: IconChartBar, - items: [ - { - name: "Analyse", - href: `${repoBasePath}/analyse`, - icon: IconBrain, - }, - ], - }, - { - label: "SETTINGS", - groupIcon: IconSettings, - items: [ - { - name: "Inbox", - href: `${repoBasePath}/inbox`, - icon: IconInbox, - }, - { - name: "Automations", - href: `${repoBasePath}/automations`, - icon: IconPlayerPlay, - }, - { - name: "Stats", - href: `${repoBasePath}/stats`, - icon: IconChartBar, - }, - { - name: "Settings", - href: `${repoBasePath}/settings/config`, - icon: IconSettings, - }, - ], - }, - ] - : [], - [repoBasePath, isRepoRoute], - ); + const isDev = process.env.NODE_ENV === "development"; + + const repoNavigation = useMemo(() => { + if (!isRepoRoute || !repoBasePath) return []; + const allGroups = [ + { + label: "BUILD", + groupIcon: IconHammer, + items: [ + { + name: "Projects", + href: `${repoBasePath}/projects`, + icon: IconLayoutKanban, + }, + { + name: "Designs", + href: `${repoBasePath}/designs`, + icon: IconPalette, + devOnly: true, + }, + ], + }, + { + label: "FIX", + groupIcon: IconTool, + items: [ + { + name: "Quick Tasks", + href: `${repoBasePath}/quick-tasks`, + icon: IconChecklist, + }, + { + name: "Sessions", + href: `${repoBasePath}/sessions`, + icon: IconTerminal2, + }, + ], + }, + { + label: "TEST", + groupIcon: IconTestPipe, + devOnly: true, + items: [ + { + name: "Documents", + href: `${repoBasePath}/docs`, + icon: IconFileText, + devOnly: true, + }, + { + name: "Testing Arena", + href: `${repoBasePath}/testing-arena`, + icon: IconFlask, + devOnly: true, + }, + ], + }, + { + label: "DATA", + groupIcon: IconChartBar, + devOnly: true, + items: [ + { + name: "Analyse", + href: `${repoBasePath}/analyse`, + icon: IconBrain, + devOnly: true, + }, + ], + }, + { + label: "SETTINGS", + groupIcon: IconSettings, + items: [ + { + name: "Inbox", + href: `${repoBasePath}/inbox`, + icon: IconInbox, + }, + { + name: "Automations", + href: `${repoBasePath}/automations`, + icon: IconPlayerPlay, + }, + { + name: "Stats", + href: `${repoBasePath}/stats`, + icon: IconChartBar, + }, + { + name: "Settings", + href: `${repoBasePath}/settings/config`, + icon: IconSettings, + }, + ], + }, + ]; + if (isDev) return allGroups; + return allGroups + .filter((g) => !g.devOnly) + .map((g) => ({ + ...g, + items: g.items.filter((i) => !i.devOnly), + })) + .filter((g) => g.items.length > 0); + }, [repoBasePath, isRepoRoute, isDev]); const { theme, toggleTheme } = useThemeContext(); diff --git a/apps/web/lib/components/quick-tasks/QuickTaskCard.tsx b/apps/web/lib/components/quick-tasks/QuickTaskCard.tsx index 02540868f..89ad0eee3 100644 --- a/apps/web/lib/components/quick-tasks/QuickTaskCard.tsx +++ b/apps/web/lib/components/quick-tasks/QuickTaskCard.tsx @@ -40,7 +40,7 @@ import { statusConfig, type TaskStatus, } from "@/lib/components/tasks/TaskStatusBadge"; -import dayjs from "@conductor/shared/dates"; +import dayjs, { compactRelativeTime } from "@conductor/shared/dates"; import { useState } from "react"; import { useRepo } from "@/lib/contexts/RepoContext"; import { DeleteTaskDialog } from "./_components/DeleteTaskDialog"; @@ -204,7 +204,7 @@ export function QuickTaskCard({
- {dayjs(createdAt).fromNow()} + {compactRelativeTime(createdAt)} diff --git a/apps/web/lib/utils/dismissDaytonaWarning.ts b/apps/web/lib/utils/dismissDaytonaWarning.ts index f16beef88..acd951ddb 100644 --- a/apps/web/lib/utils/dismissDaytonaWarning.ts +++ b/apps/web/lib/utils/dismissDaytonaWarning.ts @@ -6,8 +6,8 @@ export async function dismissDaytonaWarning(url: string): Promise { try { await fetch(url, { method: "HEAD", + mode: "no-cors", headers: { "X-Daytona-Skip-Preview-Warning": "true" }, - credentials: "include", }); dismissed.add(origin); } catch { diff --git a/apps/web/next.config.js b/apps/web/next.config.js index 13f5e1ab2..10a46ac61 100644 --- a/apps/web/next.config.js +++ b/apps/web/next.config.js @@ -1,5 +1,6 @@ /** @type {import('next').NextConfig} */ const nextConfig = { + allowedDevOrigins: ["*.daytonaproxy01.net"], turbopack: { resolveExtensions: [".ts", ".tsx", ".js", ".jsx"], }, diff --git a/internal/plans/todo/deployment-status-tracking.md b/internal/plans/implemented/deployment-status-tracking.md similarity index 100% rename from internal/plans/todo/deployment-status-tracking.md rename to internal/plans/implemented/deployment-status-tracking.md diff --git a/internal/plans/todo/figma-linear-auth.md b/internal/plans/implemented/figma-linear-auth.md similarity index 100% rename from internal/plans/todo/figma-linear-auth.md rename to internal/plans/implemented/figma-linear-auth.md diff --git a/internal/plans/todo/image-screenshot.md b/internal/plans/implemented/image-screenshot.md similarity index 100% rename from internal/plans/todo/image-screenshot.md rename to internal/plans/implemented/image-screenshot.md diff --git a/internal/plans/todo/sandbox-auto-auth.md b/internal/plans/implemented/sandbox-auto-auth.md similarity index 100% rename from internal/plans/todo/sandbox-auto-auth.md rename to internal/plans/implemented/sandbox-auto-auth.md diff --git a/internal/plans/todo/sandbox-mcp-auth.md b/internal/plans/implemented/sandbox-mcp-auth.md similarity index 100% rename from internal/plans/todo/sandbox-mcp-auth.md rename to internal/plans/implemented/sandbox-mcp-auth.md diff --git a/internal/plans/todo/video-clip.md b/internal/plans/implemented/video-clip.md similarity index 100% rename from internal/plans/todo/video-clip.md rename to internal/plans/implemented/video-clip.md diff --git a/packages/backend/convex/_daytona/sessions.ts b/packages/backend/convex/_daytona/sessions.ts index 22ac35812..c1ce76e1b 100644 --- a/packages/backend/convex/_daytona/sessions.ts +++ b/packages/backend/convex/_daytona/sessions.ts @@ -64,6 +64,7 @@ function isRetryableSessionGitError(message: string): boolean { const lower = message.toLowerCase(); return ( (lower.includes("sandbox exec") && lower.includes("timed out")) || + lower.includes("command execution timeout") || lower.includes("fetch failed") || lower.includes("econnreset") || lower.includes("econnrefused") || @@ -89,7 +90,7 @@ async function checkRemoteBranchExistsWithRetry( repoOwner, repoName, branchName, - 10, + 20, ); if (attempt > 1) { logSession( diff --git a/packages/backend/convex/sandboxAuthConfig.ts b/packages/backend/convex/sandboxAuthConfig.ts index b06e60114..fe863d4cf 100644 --- a/packages/backend/convex/sandboxAuthConfig.ts +++ b/packages/backend/convex/sandboxAuthConfig.ts @@ -3,5 +3,9 @@ const SANDBOX_JWT_FALLBACK_ISSUER = "https://elegant-snail-639.convex.site"; export const SANDBOX_JWT_ISSUER = process.env.CONVEX_SITE_URL ?? SANDBOX_JWT_FALLBACK_ISSUER; -export const SANDBOX_JWT_JWKS_DATA_URI = - "data:application/json;base64,eyJrZXlzIjpbeyJrdHkiOiJFQyIsIngiOiIzTmxVWkF3ZXA4OFFVTXJGbFh1MEk1ZEktUWVyWjhTZ21WOG54SkFGWnFnIiwieSI6IkpPNnRpLU13VXMydWpNaGcwRlJGaDNqOF9HU0lvNU5VX28ySWJnVG1QU0UiLCJjcnYiOiJQLTI1NiIsImtpZCI6InNhbmRib3gtMSIsImFsZyI6IkVTMjU2IiwidXNlIjoic2lnIn1dfQ=="; +const jwksJson = process.env.SANDBOX_JWT_JWKS; +if (!jwksJson) { + throw new Error("Missing SANDBOX_JWT_JWKS env var"); +} +const jwksBase64 = btoa(jwksJson); +export const SANDBOX_JWT_JWKS_DATA_URI = `data:application/json;base64,${jwksBase64}`; diff --git a/packages/backend/convex/sessionWorkflow.ts b/packages/backend/convex/sessionWorkflow.ts index 6310863a0..c8263ed1b 100644 --- a/packages/backend/convex/sessionWorkflow.ts +++ b/packages/backend/convex/sessionWorkflow.ts @@ -3,7 +3,7 @@ import { internalMutation, internalQuery } from "./_generated/server"; import { internal } from "./_generated/api"; import { defineEvent, type WorkflowId } from "@convex-dev/workflow"; import { workflow } from "./workflowManager"; -import { authMutation } from "./functions"; +import { authMutation, hasRepoAccess } from "./functions"; import { sessionModeValidator, workflowCompleteValidator } from "./validators"; import { RUN_TIMEOUT_MS } from "./workflowWatchdog"; import { clearStreamingActivity } from "./_taskWorkflow/helpers"; @@ -444,7 +444,8 @@ export const handleCompletion = authMutation({ handler: async (ctx, args) => { const session = await ctx.db.get(args.sessionId); if (!session || !session.activeWorkflowId) return null; - if (session.userId !== ctx.userId) throw new Error("Not authorized"); + if (!(await hasRepoAccess(ctx.db, session.repoId, ctx.userId))) + throw new Error("Not authorized"); await workflow.sendEvent(ctx, { ...sessionCompleteEvent, @@ -482,7 +483,8 @@ export const startExecute = authMutation({ handler: async (ctx, args) => { const session = await ctx.db.get(args.sessionId); if (!session) throw new Error("Session not found"); - if (session.userId !== ctx.userId) throw new Error("Not authorized"); + if (!(await hasRepoAccess(ctx.db, session.repoId, ctx.userId))) + throw new Error("Not authorized"); if ( args.mode !== "ask" && @@ -531,7 +533,8 @@ export const cancelExecution = authMutation({ handler: async (ctx, args) => { const session = await ctx.db.get(args.sessionId); if (!session) throw new Error("Session not found"); - if (session.userId !== ctx.userId) throw new Error("Not authorized"); + if (!(await hasRepoAccess(ctx.db, session.repoId, ctx.userId))) + throw new Error("Not authorized"); if (session.activeWorkflowId) { await workflow.cancel(ctx, session.activeWorkflowId as WorkflowId); diff --git a/packages/backend/convex/teams.ts b/packages/backend/convex/teams.ts index dd34eba75..8c796d4eb 100644 --- a/packages/backend/convex/teams.ts +++ b/packages/backend/convex/teams.ts @@ -68,6 +68,7 @@ export const list = authQuery({ _id: v.id("teams"), _creationTime: v.number(), name: v.string(), + displayName: v.string(), createdBy: v.id("users"), createdAt: v.number(), isPersonal: v.optional(v.boolean()), @@ -84,8 +85,19 @@ export const list = authQuery({ for (const membership of memberships) { const team = await ctx.db.get(membership.teamId); if (team) { + let displayName = team.name; + if (team.isPersonal) { + if (team.createdBy === ctx.userId) { + displayName = "My Team"; + } else { + const owner = await ctx.db.get(team.createdBy); + const ownerName = owner?.firstName ?? owner?.fullName ?? "Unknown"; + displayName = `${ownerName}'s Team`; + } + } teams.push({ ...team, + displayName, userRole: membership.role, }); } @@ -102,6 +114,7 @@ export const get = authQuery({ _id: v.id("teams"), _creationTime: v.number(), name: v.string(), + displayName: v.string(), createdBy: v.id("users"), createdAt: v.number(), isPersonal: v.optional(v.boolean()), @@ -122,8 +135,20 @@ export const get = authQuery({ if (!membership) return null; + let displayName = team.name; + if (team.isPersonal) { + if (team.createdBy === ctx.userId) { + displayName = "My Team"; + } else { + const owner = await ctx.db.get(team.createdBy); + const ownerName = owner?.firstName ?? owner?.fullName ?? "Unknown"; + displayName = `${ownerName}'s Team`; + } + } + return { ...team, + displayName, userRole: membership.role, }; }, diff --git a/packages/backend/convex/users.ts b/packages/backend/convex/users.ts index 3c149e5fe..323cfd460 100644 --- a/packages/backend/convex/users.ts +++ b/packages/backend/convex/users.ts @@ -19,6 +19,7 @@ export const get = authQuery({ v.object({ firstName: v.optional(v.string()), lastName: v.optional(v.string()), + fullName: v.optional(v.string()), lastSeenAt: v.optional(v.number()), }), v.null(), @@ -29,6 +30,7 @@ export const get = authQuery({ return { firstName: user.firstName, lastName: user.lastName, + fullName: user.fullName, lastSeenAt: user.lastSeenAt, }; }, diff --git a/packages/shared/src/components/user-initials.tsx b/packages/shared/src/components/user-initials.tsx index ed30d91ea..9245fc3fc 100644 --- a/packages/shared/src/components/user-initials.tsx +++ b/packages/shared/src/components/user-initials.tsx @@ -16,10 +16,22 @@ export function UserInitials({ }) { const user = useQuery(api.users.get, { id: userId }); if (!user) return null; + const firstLast = + `${user.firstName?.[0] ?? ""}${user.lastName?.[0] ?? ""}`.toUpperCase(); const initials = - `${user.firstName?.[0] ?? ""}${user.lastName?.[0] ?? ""}`.toUpperCase() || - "?"; - const name = `${user.firstName ?? ""} ${user.lastName ?? ""}`.trim(); + firstLast || + (user.fullName + ? user.fullName + .split(/\s+/) + .map((w) => w[0]) + .join("") + .toUpperCase() + .slice(0, 2) + : "?"); + const name = + `${user.firstName ?? ""} ${user.lastName ?? ""}`.trim() || + user.fullName || + ""; const online = !!user.lastSeenAt && Date.now() - user.lastSeenAt < 120_000; const tooltip = online ? `${name} · Online` diff --git a/packages/shared/src/utils/dates.ts b/packages/shared/src/utils/dates.ts index c5edf5354..f4efd0d92 100644 --- a/packages/shared/src/utils/dates.ts +++ b/packages/shared/src/utils/dates.ts @@ -3,4 +3,22 @@ import relativeTime from "dayjs/plugin/relativeTime"; dayjs.extend(relativeTime); +export function compactRelativeTime(date: number | string | Date): string { + const now = dayjs(); + const then = dayjs(date); + const diffSeconds = now.diff(then, "second"); + + if (diffSeconds < 60) return `${diffSeconds}s`; + const diffMinutes = now.diff(then, "minute"); + if (diffMinutes < 60) return `${diffMinutes}m`; + const diffHours = now.diff(then, "hour"); + if (diffHours < 24) return `${diffHours}h`; + const diffDays = now.diff(then, "day"); + if (diffDays < 30) return `${diffDays}d`; + const diffMonths = now.diff(then, "month"); + if (diffMonths < 12) return `${diffMonths}mo`; + const diffYears = now.diff(then, "year"); + return `${diffYears}y`; +} + export default dayjs; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 68c9b8580..cd329c41d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -921,6 +921,9 @@ importers: '@tanstack/react-router': specifier: ^1.120.0 version: 1.168.2(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + '@tanstack/router-zod-adapter': + specifier: ^1.81.5 + version: 1.81.5(@tanstack/react-router@1.168.2(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(zod@4.3.6) '@tiptap/core': specifier: ^3.20.1 version: 3.20.1(@tiptap/pm@3.20.1) @@ -5287,6 +5290,13 @@ packages: resolution: {integrity: sha512-nRcYw+w2OEgK6VfjirYvGyPLOK+tZQz1jkYcmH5AjMamQ9PycnlxZF2aEZtPpNoUsaceX2bHptn6Ub5hGXqNvw==} engines: {node: '>=20.19'} + '@tanstack/router-zod-adapter@1.81.5': + resolution: {integrity: sha512-oJp3QaCI5YwW7H46iuivC8pJLmYboXa1OztncRZNmfVBX69FZ7DodfxdrwNzceGpN3sXZT/f0t4sV05dKsneHg==} + engines: {node: '>=12'} + peerDependencies: + '@tanstack/react-router': '>=1.43.2' + zod: '>=3' + '@tanstack/store@0.9.1': resolution: {integrity: sha512-+qcNkOy0N1qSGsP7omVCW0SDrXtaDcycPqBDE726yryiA5eTDFpjBReaYjghVJwNf1pcPMyzIwTGlYjCSQR0Fg==} @@ -18079,6 +18089,11 @@ snapshots: transitivePeerDependencies: - supports-color + '@tanstack/router-zod-adapter@1.81.5(@tanstack/react-router@1.168.2(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(zod@4.3.6)': + dependencies: + '@tanstack/react-router': 1.168.2(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + zod: 4.3.6 + '@tanstack/store@0.9.1': {} '@tanstack/store@0.9.2': {}