ed243594d2
Wave 3 (Task 6-3-01): /build/[competencyId] rewritten as a real build surface —
variant statement + starter files in a live namespace sandbox, workspace file tree +
editor with save, Run/Test buttons executing bounded commands in-sandbox with
read-only TerminalFrame output (CUT-2), live TelemetryStatus pulse, Lab feedback over
the live trace, honest 503-busy/403-429-denied states with retry.
Wave 4 (Task 6-4-01): /defend/[competencyId] rewritten — DefenseSession: start ->
examiner question -> typed answers (mic capture w/ MediaRecorder consent + denied
fallback; browser-SR first-class per CUT-1) -> finish -> verdict + integrity signals
-> Grade My Work renders real rubric bars from the trace digest. Dead mock components
disposed (oral-defense-interface, assessor-results-panel, proctor-banner — G-5 class).
Wave 5 (Task 6-5-01): test_e2e_credential_flow — REAL uvicorn + REAL namespaces:
variant -> sandbox -> in-sandbox exec -> trace -> grade (seed stamped) -> coaching ->
defense -> verdict -> proctor; corpus-fixture scan of all payloads. E2E caught a real
bug: the sandboxes API dropped task_id (every HTTP-created sandbox was capture-less)
— fixed. README E2E + manual browser pass documented.
pnpm build 4/4; typecheck FULL TURBO; backend suite 397 green; ruff clean.
---ci---
phase: 6
milestone: v0.3
status: execute
requirements: {covered: [REQ-3-007, REQ-3-008], partial: []}
---/ci---
78 lines
2.6 KiB
TypeScript
78 lines
2.6 KiB
TypeScript
'use client';
|
|
|
|
import { useEffect, useState } from 'react';
|
|
import { FileText, Folder } from 'lucide-react';
|
|
|
|
/**
|
|
* Real workspace file tree (REQ-3-008): files come from the sandbox workdir
|
|
* via the engine client; selecting a file loads its content into the editor.
|
|
*/
|
|
export function FileTree({
|
|
files,
|
|
activePath,
|
|
onSelect,
|
|
disabled,
|
|
}: {
|
|
files: string[];
|
|
activePath: string | null;
|
|
onSelect: (path: string) => void;
|
|
disabled?: boolean;
|
|
}) {
|
|
const [dirs, setDirs] = useState<Record<string, string[]>>({});
|
|
|
|
useEffect(() => {
|
|
// Group by top-level segment; the flat workspace is v0.3's shape.
|
|
const grouped: Record<string, string[]> = {};
|
|
for (const f of files) {
|
|
const seg = f.includes('/') ? f.split('/')[0] + '/' : '';
|
|
(grouped[seg] ??= []).push(f);
|
|
}
|
|
setDirs(grouped);
|
|
}, [files]);
|
|
|
|
return (
|
|
<ul
|
|
role="tree"
|
|
aria-label="Workspace files"
|
|
className="space-y-0.5 text-xs text-slate-700 dark:text-slate-300"
|
|
>
|
|
{Object.entries(dirs).map(([group, paths]) => (
|
|
<li key={group || '__root__'} role="treeitem">
|
|
{group ? (
|
|
<span className="flex items-center gap-1 py-0.5 font-medium text-slate-600 dark:text-slate-400">
|
|
<Folder className="h-3.5 w-3.5" aria-hidden />
|
|
{group}
|
|
</span>
|
|
) : null}
|
|
<ul className={group ? 'ml-4 space-y-0.5' : 'space-y-0.5'}>
|
|
{paths.map((path) => {
|
|
const name = path.split('/').pop() ?? path;
|
|
const active = activePath === path;
|
|
return (
|
|
<li key={path}>
|
|
<button
|
|
type="button"
|
|
onClick={() => onSelect(path)}
|
|
disabled={disabled}
|
|
aria-current={active ? 'true' : undefined}
|
|
className={`flex w-full items-center gap-1 rounded px-1.5 py-1 text-left transition-colors disabled:opacity-50 ${
|
|
active
|
|
? 'bg-primary-100 text-primary-900 dark:bg-primary-900/40 dark:text-primary-100'
|
|
: 'hover:bg-slate-100 dark:hover:bg-slate-800'
|
|
}`}
|
|
>
|
|
<FileText className="h-3.5 w-3.5 shrink-0" aria-hidden />
|
|
<span className="truncate">{name}</span>
|
|
</button>
|
|
</li>
|
|
);
|
|
})}
|
|
</ul>
|
|
</li>
|
|
))}
|
|
{files.length === 0 && (
|
|
<li className="px-1 py-2 text-slate-500 dark:text-slate-400">No files yet.</li>
|
|
)}
|
|
</ul>
|
|
);
|
|
} |