diff --git a/web/playwright.config.ts b/web/playwright.config.ts new file mode 100644 index 0000000..7a4516b --- /dev/null +++ b/web/playwright.config.ts @@ -0,0 +1,21 @@ +import { defineConfig, devices } from '@playwright/test'; + +export default defineConfig({ + testDir: './tests/e2e', + fullyParallel: false, + retries: 0, + workers: 1, + reporter: [['list']], + use: { + headless: true, + actionTimeout: 15_000, + navigationTimeout: 30_000, + ignoreHTTPSErrors: true, + }, + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], +}); diff --git a/web/src/App.tsx b/web/src/App.tsx index c3142f9..47106ea 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -11,6 +11,7 @@ import { useApprovalsQueue } from '@/state/useApprovalsQueue'; import { useAgentDefinitions } from '@/state/useAgentDefinitions'; import { useSessionFull } from '@/state/useSessionFull'; import { MonitorRail } from '@/monitors/MonitorRail'; +import { NewSessionModal } from '@/modals/NewSessionModal'; const UI_VERSION = 'v2.0.0-rc1'; const RUNTIME_VERSION_FALLBACK = 'unknown'; @@ -32,6 +33,7 @@ const paneStyle: CSSProperties = { export function App() { const [activeSid, setActiveSid] = useState(null); + const [newSessionOpen, setNewSessionOpen] = useState(false); const uiHints = useUiHints(); const sessionList = useSessionList(); @@ -65,7 +67,7 @@ export function App() { health={health} approvalsCount={approvals.count} onSearch={() => {/* Phase 6: open search overlay */}} - onNew={() => {/* Phase 6: open NewSessionModal */}} + onNew={() => setNewSessionOpen(true)} onApprovalsClick={() => {/* Phase 6: open approvals view */}} /> + setActiveSid(sid)} + /> ); } diff --git a/web/src/modals/NewSessionModal.tsx b/web/src/modals/NewSessionModal.tsx new file mode 100644 index 0000000..f6b4feb --- /dev/null +++ b/web/src/modals/NewSessionModal.tsx @@ -0,0 +1,153 @@ +import { useState, useEffect } from 'react'; +import type { CSSProperties } from 'react'; +import { Modal } from '@/components/Modal'; +import { apiFetch, ApiClientError } from '@/api/client'; + +interface NewSessionModalProps { + open: boolean; + onOpenChange: (open: boolean) => void; + environments: string[]; + onCreated: (sid: string) => void; +} + +interface StartResponse { + session_id: string; +} + +const labelStyle: CSSProperties = { + display: 'block', + fontFamily: 'var(--ff-mono)', + fontSize: 10, + color: 'var(--ink-3)', + letterSpacing: '0.14em', + textTransform: 'uppercase', + marginBottom: 4, +}; + +const fieldWrap: CSSProperties = { marginBottom: 16 }; + +const inputStyle: CSSProperties = { + width: '100%', + height: 32, + padding: '0 10px', + fontFamily: 'var(--ff-sans)', + fontSize: 13, + color: 'var(--ink-1)', + background: 'var(--bg-elev)', + border: '1px solid var(--hair)', + borderRadius: 0, + outline: 'none', + boxSizing: 'border-box', +}; + +const textareaStyle: CSSProperties = { + ...inputStyle, + height: 'auto', + minHeight: 96, + padding: '8px 10px', + resize: 'vertical', + fontFamily: 'var(--ff-sans)', + lineHeight: 1.5, +}; + +const selectStyle: CSSProperties = { + ...inputStyle, + height: 32, + appearance: 'auto', +}; + +export function NewSessionModal({ open, onOpenChange, environments, onCreated }: NewSessionModalProps) { + const [query, setQuery] = useState(''); + const [environment, setEnvironment] = useState(environments[0] ?? 'dev'); + const [submitting, setSubmitting] = useState(false); + const [error, setError] = useState(null); + + useEffect(() => { + if (!open) { + setQuery(''); + setError(null); + setSubmitting(false); + setEnvironment(environments[0] ?? 'dev'); + } + }, [open, environments]); + + const canSubmit = query.trim().length > 0 && !submitting; + + async function handleSubmit() { + if (!canSubmit) return; + setSubmitting(true); + setError(null); + try { + const res = await apiFetch('/sessions', { + method: 'POST', + json: { query: query.trim(), environment, submitter: { id: 'operator' } }, + }); + onCreated(res.session_id); + onOpenChange(false); + } catch (e) { + if (e instanceof ApiClientError) { + setError(`${e.code}: ${e.message}`); + } else { + setError(String(e)); + } + setSubmitting(false); + } + } + + return ( + { void handleSubmit(); }, + disabled: !canSubmit, + }} + > +
+ +