Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
9 changes: 9 additions & 0 deletions components/backend/handlers/sessions.go
Original file line number Diff line number Diff line change
Expand Up @@ -893,6 +893,15 @@ func CreateSession(c *gin.Context) {
// Runner token provisioning is handled by the operator when creating the pod.
// This ensures consistent behavior whether sessions are created via API or kubectl.

// Trigger async display name generation when initialPrompt is provided
// but no explicit displayName was set. The AG-UI proxy skips the
// initialPrompt message, so sessions created with only an initialPrompt
// (e.g., from the new-session page) would never get a generated name.
if strings.TrimSpace(req.InitialPrompt) != "" && strings.TrimSpace(req.DisplayName) == "" {
sessionCtx := ExtractSessionContext(created.Object["spec"].(map[string]interface{}))
GenerateDisplayNameAsync(project, name, req.InitialPrompt, sessionCtx)
}

c.JSON(http.StatusCreated, gin.H{
"message": "Agentic session created successfully",
"name": name,
Expand Down
4 changes: 2 additions & 2 deletions components/frontend/src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { Metadata } from "next";
import { GeistSans } from "geist/font/sans";
import { GeistMono } from "geist/font/mono";
import "./globals.css";
import { Navigation } from "@/components/navigation";
import { NavigationWrapper } from "@/components/navigation-wrapper";
import { QueryProvider } from "@/components/providers/query-provider";
import { ThemeProvider } from "@/components/providers/theme-provider";
import { SyntaxThemeProvider } from "@/components/providers/syntax-theme-provider";
Expand Down Expand Up @@ -44,7 +44,7 @@ export default function RootLayout({
<SyntaxThemeProvider />
<FeatureFlagProvider>
<QueryProvider>
<Navigation feedbackUrl={feedbackUrl} />
<NavigationWrapper feedbackUrl={feedbackUrl} />
<main className="flex-1 bg-background overflow-auto">{children}</main>
<CommandPalette />
<Toaster />
Expand Down
10 changes: 8 additions & 2 deletions components/frontend/src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,14 @@ import { Loader2 } from "lucide-react";
export default function HomeRedirect() {
const router = useRouter();
useEffect(() => {
// Redirect to RFE workflows as the new main interface
router.replace("/projects");
const lastProject = typeof window !== "undefined"
? localStorage.getItem("selectedProject")
: null;
if (lastProject) {
router.replace(`/projects/${encodeURIComponent(lastProject)}`);
} else {
router.replace("/projects");
}
}, [router]);

return (
Expand Down
71 changes: 15 additions & 56 deletions components/frontend/src/app/projects/[name]/keys/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,9 @@ import { Badge } from '@/components/ui/badge';
import { Label } from '@/components/ui/label';
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { ProjectSubpageHeader } from '@/components/project-subpage-header';
import { ErrorMessage } from '@/components/error-message';
import { EmptyState } from '@/components/empty-state';
import { DestructiveConfirmationDialog } from '@/components/confirmation-dialog';
import {
Breadcrumb,
BreadcrumbList,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbPage,
BreadcrumbSeparator,
} from '@/components/ui/breadcrumb';
import Link from 'next/link';

import { useKeys, useCreateKey, useDeleteKey } from '@/services/queries';
import { toast } from 'sonner';
Expand Down Expand Up @@ -115,47 +105,7 @@ export default function ProjectKeysPage() {
}

return (
<div className="container mx-auto p-6">
<Breadcrumb className="mb-4">
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink asChild>
<Link href="/projects">Projects</Link>
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbLink asChild>
<Link href={`/projects/${projectName}`}>{projectName}</Link>
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>Keys</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
<ProjectSubpageHeader
title={
<>
<KeyRound className="w-6 h-6" />
Access Keys
</>
}
description={<>Create and manage API keys for non-user access</>}
actions={
<>
<Button onClick={() => setShowCreate(true)}>
<Plus className="w-4 h-4 mr-2" />
Create Key
</Button>
<Button variant="outline" onClick={() => refetch()} disabled={isLoading}>
<RefreshCw className={`w-4 h-4 mr-2 ${isLoading ? 'animate-spin' : ''}`} />
Refresh
</Button>
</>
}
/>
<div className="h-full overflow-auto p-6">

{/* Error state */}
{error && <ErrorMessage error={error} onRetry={() => refetch()} />}
Expand All @@ -174,11 +124,20 @@ export default function ProjectKeysPage() {

<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<KeyRound className="w-5 h-5" />
Access Keys ({keys.length})
</CardTitle>
<CardDescription>API keys scoped to this project</CardDescription>
<div className="flex items-center justify-between">
<div>
<CardTitle className="flex items-center gap-2">
Access Keys
</CardTitle>
<CardDescription>Create and manage API keys for non-user access</CardDescription>
</div>
<div className="flex items-center gap-2">
<Button size="sm" onClick={() => setShowCreate(true)}>
<Plus className="w-4 h-4 mr-2" />
Create Key
</Button>
</div>
</div>
</CardHeader>
<CardContent>
{keys.length > 0 ? (
Expand Down
141 changes: 141 additions & 0 deletions components/frontend/src/app/projects/[name]/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
"use client";

import { useEffect, useMemo } from "react";
import { useParams, useRouter, usePathname } from "next/navigation";
import { PanelLeft, Plug, LogOut } from "lucide-react";
import Link from "next/link";
import { useVersion } from "@/services/queries/use-version";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Button } from "@/components/ui/button";
import { ThemeToggle } from "@/components/theme-toggle";
import { UserBubble } from "@/components/user-bubble";
import { cn } from "@/lib/utils";
import { useLocalStorage } from "@/hooks/use-local-storage";
import { SessionsSidebar } from "./sessions/[sessionName]/components/sessions-sidebar";

export default function ProjectLayout({
children,
}: {
children: React.ReactNode;
}) {
const params = useParams();
const router = useRouter();
const pathname = usePathname();
const projectName = params?.name as string;

// Extract session name from URL: /projects/{name}/sessions/{sessionName}
const currentSessionName = useMemo(() => {
if (!pathname) return "";
const match = pathname.match(/\/sessions\/([^/]+)/);
return match ? decodeURIComponent(match[1]) : "";
}, [pathname]);
const [sidebarVisible, setSidebarVisible] = useLocalStorage(
"session-sidebar-visible",
true
);
const { data: version } = useVersion();

const handleLogout = () => {
window.location.href = '/oauth/sign_out';
};

// Persist last visited project for redirect on next visit
useEffect(() => {
if (projectName) {
try { localStorage.setItem("selectedProject", projectName); } catch {}
}
}, [projectName]);

if (!projectName) return null;

return (
<div className="absolute inset-0 overflow-hidden bg-background flex flex-col">
<div className="flex-grow overflow-hidden bg-card flex">
{/* Left sidebar */}
<div
className={cn(
"h-full overflow-hidden border-r transition-[width] duration-200 ease-in-out flex-shrink-0",
sidebarVisible ? "w-[300px]" : "w-0 border-r-0"
)}
>
<div className="h-full w-[280px]">
<SessionsSidebar
projectName={projectName}
currentSessionName={currentSessionName}
collapsed={false}
onCollapse={() => setSidebarVisible(false)}
/>
</div>
</div>

{/* Main content */}
<div className="flex-1 min-w-0 flex flex-col h-full">
{/* Content header with nav items */}
<div className="flex-shrink-0 border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
<div className="flex h-14 items-center justify-between gap-3 px-4">
{/* Left: branding when sidebar is collapsed */}
<div className="flex items-center gap-2">
{!sidebarVisible && (
<>
<Button
variant="ghost"
size="sm"
onClick={() => setSidebarVisible(true)}
className="h-8 w-8 p-0"
title="Show sessions sidebar"
>
<PanelLeft className="h-4 w-4" />
</Button>
<Link href="/" className="flex items-end gap-2">
<span className="text-lg font-bold">Ambient Code Platform</span>
{version && (
<span className="text-[0.65rem] text-muted-foreground/60 pb-0.5">
{version}
</span>
)}
</Link>
</>
)}
</div>

{/* Right: nav items */}
<div className="flex items-center gap-3">
<ThemeToggle />
<Button
variant="ghost"
size="sm"
onClick={() => router.push('/integrations')}
className="text-muted-foreground hover:text-foreground"
>
<Plug className="w-4 h-4 mr-1" />
Integrations
</Button>
<DropdownMenu>
<DropdownMenuTrigger className="outline-none">
<UserBubble />
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onSelect={handleLogout}>
<LogOut className="w-4 h-4 mr-2" />
Logout
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
</div>

{/* Page content */}
<div className="flex-1 overflow-auto">
{children}
</div>
</div>
</div>
</div>
);
}
98 changes: 98 additions & 0 deletions components/frontend/src/app/projects/[name]/new/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
"use client";

import { useCallback, useState } from "react";
import { useParams, useRouter } from "next/navigation";
import { toast } from "sonner";
import { NewSessionView } from "../sessions/[sessionName]/components/new-session-view";
import { CustomWorkflowDialog } from "../sessions/[sessionName]/components/modals/custom-workflow-dialog";
import { useCreateSession } from "@/services/queries";
import { useOOTBWorkflows } from "@/services/queries/use-workflows";

export default function NewSessionPage() {
const params = useParams();
const router = useRouter();
const projectName = params?.name as string;

const { data: ootbWorkflows = [] } = useOOTBWorkflows(projectName);
const createSessionMutation = useCreateSession();
const [customWorkflowDialogOpen, setCustomWorkflowDialogOpen] = useState(false);
const [customWorkflow, setCustomWorkflow] = useState<{ gitUrl: string; branch: string; path: string } | null>(null);

const handleCreateNewSession = useCallback(
(config: {
prompt: string;
runner: string;
model: string;
workflow?: string;
repos?: Array<{ url: string }>;
}) => {
const workflowConfig = config.workflow === "custom" && customWorkflow
? { gitUrl: customWorkflow.gitUrl, branch: customWorkflow.branch, path: customWorkflow.path }
: config.workflow
? ootbWorkflows.find((w) => w.id === config.workflow)
: undefined;

createSessionMutation.mutate(
{
projectName,
data: {
initialPrompt: config.prompt,
runnerType: config.runner,
llmSettings: { model: config.model },
...(workflowConfig
? {
activeWorkflow: {
gitUrl: workflowConfig.gitUrl,
branch: workflowConfig.branch || "main",
path: workflowConfig.path,
},
}
: {}),
...(config.repos && config.repos.length > 0
? {
repos: config.repos.map((r) => ({ url: r.url })),
}
: {}),
},
},
{
onSuccess: (session) => {
router.push(
`/projects/${encodeURIComponent(projectName)}/sessions/${session.metadata.name}`
);
},
onError: (err) => {
toast.error(
err instanceof Error
? err.message
: "Failed to create session"
);
},
}
);
},
[projectName, ootbWorkflows, customWorkflow, createSessionMutation, router]
);

if (!projectName) return null;

return (
<div className="h-full overflow-auto">
<NewSessionView
projectName={projectName}
onCreateSession={handleCreateNewSession}
ootbWorkflows={ootbWorkflows}
onLoadCustomWorkflow={() => setCustomWorkflowDialogOpen(true)}
isSubmitting={createSessionMutation.isPending}
/>
<CustomWorkflowDialog
open={customWorkflowDialogOpen}
onOpenChange={setCustomWorkflowDialogOpen}
onSubmit={(url, branch, path) => {
setCustomWorkflow({ gitUrl: url, branch: branch || "main", path: path || "" });
setCustomWorkflowDialogOpen(false);
}}
/>
</div>
);
}
Loading
Loading