Next.js UI for managing Agentic Sessions and Projects. In local development it proxies API calls to the backend and forwards incoming auth/context headers; it does not spoof identities.
- Node.js 20+ and npm
- Go 1.24+ (to run the backend locally)
- oc/kubectl configured to your OpenShift/Kubernetes cluster
Run the backend locally while targeting your cluster.
- Install CRDs to your cluster
oc apply -f ../manifests/crd.yaml
oc apply -f ../manifests/projectsettings-crd.yaml- Create/label a project namespace (example: my-project)
oc new-project my-project || oc project my-project
oc label namespace my-project ambient-code.io/managed=true --overwrite
oc annotate namespace my-project \
ambient-code.io/display-name="My Project" --overwrite- Start the backend (defaults to port 8080)
cd ../backend
export KUBECONFIG="$HOME/.kube/config" # or your kubeconfig path
go run .
# Health: curl http://localhost:8080/healthRecommended: Use integrated local development environment:
# From repository root - single command setup
make kind-up
# Access: http://localhost:8080Alternative: Standalone frontend development:
# From this directory, install and run:
npm ci
export BACKEND_URL=http://localhost:8080/api # Adjust for your backend
npm run dev
# Open http://localhost:3000cd components/frontend
# Install dependencies
npm install
# Development server
npm run dev
# Build
npm run build
# Production server
npm start
# Linting
npm run lintPre-commit checklist:
- Run
npm run build- must pass with 0 errors, 0 warnings - See
DESIGN_GUIDELINES.mdfor comprehensive frontend development standards
The frontend acts as a BFF (Backend-for-Frontend) OIDC confidential client. Users authenticate via Keycloak, and the frontend stores the OIDC session in an encrypted httpOnly cookie. On each API request, the frontend extracts the JWT from the session and forwards it as Authorization: Bearer <jwt> to the backend.
In the Kind dev cluster, Keycloak is deployed automatically with make kind-up. Log in with developer / developer.
Legacy mode (when SSO_ENABLED is not set): the frontend falls back to forwarding X-Forwarded-* headers from an OAuth proxy sidecar.
BACKEND_URL(default:http://localhost:8080/api) — backend API for server-side routesFEEDBACK_URL(optional) — feedback link in the mastheadGITHUB_APP_SLUG(required for GitHub integration) — GitHub App slug (e.g.ambient-code)GITHUB_CALLBACK_URL(optional) — explicit callback URL for GitHub App OAuth. Used when multiple clusters share one GitHub App. Falls back to<current origin>/api/auth/github/user/callback. In production, set viafrontend-configConfigMap (key:github-callback-url).SSO_ISSUER_URL— Keycloak OIDC issuer URL (e.g.http://keycloak-service:8080/realms/ambient-code)SSO_CLIENT_ID— OIDC confidential client ID (e.g.ambient-frontend)SSO_CLIENT_SECRET— OIDC client secretSSO_ENABLED— set totrueto enable SSO auth (disables OAuth proxy header forwarding)SSO_REDIRECT_URI— OIDC callback URL (e.g.http://localhost:11646/api/auth/sso/callback)SESSION_SECRET— encryption key for the session cookie (min 32 chars)SSO_PUBLIC_ISSUER_URL(Kind only) — public Keycloak URL when it differs fromSSO_ISSUER_URL
Legacy dev helpers (when SSO is off): OC_USER, OC_EMAIL, OC_TOKEN, ENABLE_OC_WHOAMI=1
Backend directly (requires headers):
curl -i http://localhost:8080/api/projects/my-project/agentic-sessions \
-H "X-OpenShift-Project: my-project" \
-H "X-Forwarded-User: dev" \
-H "X-Forwarded-Groups: ambient-project:my-project:admin"Through the frontend route (forwards headers to backend):
curl -i http://localhost:3000/api/projects/my-project/agentic-sessions \
-H "X-OpenShift-Project: my-project"- 400 “Project is required …”
- Use path
/api/projects/{project}/…or includeX-OpenShift-Project.
- Use path
- 403 “Project is not managed by Ambient”
- Ensure namespace is labeled
ambient-code.io/managed=true.
- Ensure namespace is labeled
- Missing auth header
- In dev, provide
Authorization: Bearer <token>(or useOC_TOKEN/ENABLE_OC_WHOAMI).
- In dev, provide
- Do not spoof identities. Forward real headers from your OAuth/ingress proxy.
- Provide a project selection mechanism and forward it as
X-OpenShift-Project(or use project path in API URLs).
GitHubConnection.tsx: GitHub App installation and fork management- OAuth flow for per-user GitHub App installations
- Fork selection with visual interface
- Automatic fork creation capability
- Real-time connection status
RepoBrowser.tsx: Full repository navigation- File tree browsing with breadcrumb navigation
- File content display with syntax awareness
- Branch/ref switching support
- Size formatting and file type detection
SessionsDashboard.tsx: Live session management- Real-time WebSocket connections for session updates
- Grouped PR display (spec repo + submodule PRs)
- Live message streaming with partial reassembly
- Visual status indicators for all session states
- Multi-runner support (Claude, OpenAI, local execution)
rfe-workflows.tsx: Complete RFE workflow interface- Workspace creation and management
- Tabbed interface for different views
- RBAC integration with access level display
- Session creation and monitoring
- Type-safe backend communication via
apiClient - WebSocket support for real-time session updates
- Comprehensive error handling with user-friendly messages
- RBAC enforcement with access level checking
- Live Session Monitoring: WebSocket connections with automatic reconnection
- Multi-repo PR Management: Handle spec repo and submodule PRs separately
- GitHub App Integration: Streamlined per-user installation flow
- Repository Browsing: Full file tree navigation with content preview
- Runner Support: Claude Code, OpenAI, and local execution runners
- Access Control: Role-based permissions (view/edit/admin)
- Modern Interface: Tailwind CSS with shadcn/ui components
- Responsive Design: Mobile-friendly responsive layout
- Accessibility: Full keyboard navigation and screen reader support
- Real-time Updates: Live status indicators and message streaming
- Error Handling: Comprehensive error states with recovery actions
The frontend provides a complete user interface for the RFE (Request For Enhancement) workflow system, integrating GitHub repositories, AI runners, and real-time collaboration features.
466 tests across 26 files. Primary coverage metric (~74%).
# Run all tests
npx vitest run
# With coverage report
npx vitest run --coverage
open coverage/index.html
# Watch mode
npx vitest
# Single file
npx vitest run src/utils/__tests__/export-chat.test.tsConfig: vitest.config.ts — uses jsdom, Istanbul coverage, @/ path alias.
Place tests in __tests__/ next to the source:
src/
components/
chat/
ChatInputBox.tsx
__tests__/
ChatInputBox.test.tsx ← test file here
hooks/
use-session-queue.ts
__tests__/
use-session-queue.test.ts ← test file here
Pure function test:
import { describe, it, expect } from 'vitest';
import { convertEventsToMarkdown } from '../export-chat';
describe('convertEventsToMarkdown', () => {
it('renders text messages', () => {
const events = [
{ type: 'TEXT_MESSAGE_START', role: 'user' },
{ type: 'TEXT_MESSAGE_CONTENT', delta: 'Hello' },
{ type: 'TEXT_MESSAGE_END' },
];
const md = convertEventsToMarkdown(makeExport(events), makeSession());
expect(md).toContain('Hello');
});
});Component test:
import { render, screen, fireEvent } from '@testing-library/react';
import { describe, it, expect, vi } from 'vitest';
import { MyComponent } from '../MyComponent';
it('handles click', () => {
const onClick = vi.fn();
render(<MyComponent onClick={onClick} />);
fireEvent.click(screen.getByText('Click me'));
expect(onClick).toHaveBeenCalled();
});Hook test:
import { renderHook, act } from '@testing-library/react';
it('updates state', () => {
const { result } = renderHook(() => useMyHook());
act(() => result.current.setValue('new'));
expect(result.current.value).toBe('new');
});Mocking:
// Module mock
vi.mock('@/services/api/sessions', () => ({
createSession: vi.fn().mockResolvedValue({ name: 'test' }),
}));
// Function stub
const onSend = vi.fn();
// DOM API spy
vi.spyOn(document, 'createElement').mockReturnValue(mockEl);
// React Query wrapper
const wrapper = ({ children }) => (
<QueryClientProvider client={new QueryClient()}>
{children}
</QueryClientProvider>
);
const { result } = renderHook(() => useMyQuery(), { wrapper });