Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/web/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.vercel
38 changes: 38 additions & 0 deletions apps/web/app/api/nova/local-projects/from-github/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { auth } from "@super/auth/server"
import { headers } from "next/headers"
import { NextRequest, NextResponse } from "next/server"
import { z } from "zod"

import { upsertGithubLocalProject } from "@/modules/nova/local-projects/github"

export const runtime = "nodejs"
export const dynamic = "force-dynamic"

const bodySchema = z.object({
fullName: z.string().trim().min(3).max(200).regex(/^[^/]+\/[^/]+$/),
})

export async function POST(request: NextRequest) {
const session = await auth.api.getSession({ headers: await headers() })
if (!session?.user?.id) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
}

const parsed = bodySchema.safeParse(await request.json().catch(() => null))
if (!parsed.success) {
return NextResponse.json({ error: "Invalid repository", issues: parsed.error.issues }, { status: 400 })
}

try {
const project = await upsertGithubLocalProject(session.user.id, parsed.data.fullName)
if (!project) {
return NextResponse.json({ error: "Could not create project from repository" }, { status: 400 })
}
return NextResponse.json({ project }, { status: 201 })
} catch (error) {
return NextResponse.json(
{ error: error instanceof Error ? error.message : "Failed to index GitHub repository" },
{ status: 400 },
)
}
}
1 change: 1 addition & 0 deletions apps/web/app/api/nova/sessions/[sessionId]/turn/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ export async function POST(
model: parsed.data.model,
provider: parsed.data.provider,
effort: parsed.data.effort,
mode: parsed.data.mode ?? "agent",
harnessToken: harnessAuth.token,
signal: request.signal,
references: parsed.data.references,
Expand Down
21 changes: 19 additions & 2 deletions apps/web/modules/nova-web/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,16 @@ export function listSessions() {
return requestJson<{ sessions: AgentSessionSummary[] }>("/api/nova/sessions")
}

export function createSession(objective: string, options?: { localProjectId?: string | null }) {
export function createSession(
objective: string,
options?: { localProjectId?: string | null; mode?: "agent" | "plan" | "chat" },
) {
return requestJson<{ session: AgentSessionSummary }>("/api/nova/sessions", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
objective,
mode: "chat",
mode: options?.mode ?? "agent",
surface: "web",
localProjectId: options?.localProjectId ?? undefined,
}),
Expand Down Expand Up @@ -71,6 +74,20 @@ export function createLocalProject(input: {
})
}

export function createLocalProjectFromGithub(fullName: string) {
return requestJson<{ project: LocalProjectDto }>("/api/nova/local-projects/from-github", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ fullName }),
})
}

export type ConnectedRepoOption = {
owner: string
name: string
fullName: string
}

export function updateLocalProject(
projectId: string,
input: {
Expand Down
36 changes: 20 additions & 16 deletions apps/web/modules/nova-web/components/home.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
"use client"

import { useState } from "react"
import { ChevronDown, Cloud, FolderGit2 } from "lucide-react"
import { ChevronDown, Cloud } from "lucide-react"

import { Composer } from "@/modules/nova-web/components/composer"
import { ProjectPicker, type ProjectSelection } from "@/modules/nova-web/components/project-picker"
import type { LocalAttachment } from "@/modules/nova/attachments/contracts"
import type { LocalWorkspaceState } from "@/modules/nova-web/local-workspace"
import type { LocalProjectDto } from "@/modules/nova-web/api"
import type { NovaReference } from "@/modules/nova/references/contracts"
import type {
HarnessProvider,
Expand Down Expand Up @@ -49,8 +50,9 @@ export function EmptyHome({
onModelChange,
onEffortChange,
onModeChange,
onLocalProjectChange,
localProjectLabel,
onProjectChange,
onManageProjects,
localProject,
}: {
onStart: (value: string, references?: NovaReference[], localFiles?: LocalAttachment[]) => void
busy?: boolean
Expand All @@ -61,8 +63,9 @@ export function EmptyHome({
onModelChange: (selection: { provider: HarnessProvider; model: string }) => void
onEffortChange: (effort: NovaEffort) => void
onModeChange: (mode: NovaAgentMode) => void
onLocalProjectChange?: (project: LocalWorkspaceState) => void
localProjectLabel?: string | null
onProjectChange?: (selection: ProjectSelection) => void
onManageProjects?: () => void
localProject?: LocalProjectDto | null
}) {
const [draft, setDraft] = useState("")
const [references, setReferences] = useState<NovaReference[]>([])
Expand All @@ -87,14 +90,12 @@ export function EmptyHome({
<DotField />
<div className="relative z-[1] flex flex-1 flex-col items-center justify-center px-5 pb-24">
<div className="mb-3 flex items-center gap-3 text-[12px] text-[#5c5c5c]">
<button
type="button"
className="flex items-center gap-1.5 rounded-md px-1.5 py-1 transition hover:bg-white/[0.04] hover:text-[#a0a0a0]"
>
<FolderGit2 className="size-3.5" strokeWidth={1.75} />
{localProjectLabel ? localProjectLabel : "Local project"}
<ChevronDown className="size-3 opacity-70" />
</button>
<ProjectPicker
value={localProject}
disabled={busy}
onChange={(selection) => onProjectChange?.(selection)}
onManageProjects={onManageProjects}
/>
<button
type="button"
className="flex items-center gap-1.5 rounded-md px-1.5 py-1 transition hover:bg-white/[0.04] hover:text-[#a0a0a0]"
Expand All @@ -113,7 +114,6 @@ export function EmptyHome({
onReferencesChange={setReferences}
localFiles={localFiles}
onLocalFilesChange={setLocalFiles}
onLocalProjectChange={onLocalProjectChange}
onSubmit={() => submit()}
large
disabled={busy}
Expand All @@ -124,7 +124,11 @@ export function EmptyHome({
onModelChange={onModelChange}
onEffortChange={onEffortChange}
onModeChange={onModeChange}
placeholder="Ask to make changes, open a local folder, @mention files"
placeholder={
localProject
? `Work on ${localProject.displayName}…`
: "Select a project above, then ask Nova to make changes"
}
/>
<div className="mt-4 flex flex-wrap justify-center gap-2">
{STARTERS.map((starter) => (
Expand Down
27 changes: 27 additions & 0 deletions apps/web/modules/nova-web/components/nova-icon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { cn } from "@/lib/utils"

/**
* Nova / Supercode desktop dock mark — same asset as supercode-desktop AppIcon.
*/
export function NovaIcon({
size = "md",
className,
alt = "Nova",
}: {
size?: "sm" | "md" | "lg"
className?: string
alt?: string
}) {
const dim = size === "sm" ? "size-5" : size === "lg" ? "size-9" : "size-7"
return (
// eslint-disable-next-line @next/next/no-img-element
<img
src="/brand/nova-icon.png"
alt={alt}
width={size === "sm" ? 20 : size === "lg" ? 36 : 28}
height={size === "sm" ? 20 : size === "lg" ? 36 : 28}
draggable={false}
className={cn(dim, "shrink-0 select-none object-contain", className)}
/>
)
}
Loading
Loading