Files
nextcraft/apps/web/components/learner/sandbox-terminal.tsx
T
CIAgent ed243594d2 feat(P06): real build + defense surfaces, E2E credential flow (Waves 3-5)
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---
2026-09-12 17:54:30 +00:00

54 lines
1.6 KiB
TypeScript

'use client';
import { TerminalFrame } from '@nextcraft/ui';
import type { TerminalLine } from '@nextcraft/ui';
import { TelemetryStatus } from '@nextcraft/ui';
import type { ExecResult } from '../../lib/engine-client';
/**
* Read-only exec-output panel (CUT-2): Run/Test results stream here —
* this is NOT an interactive shell. TelemetryStatus pulses while the
* capture agent streams events to ai-service.
*/
export function SandboxTerminal({
result,
running,
telemetryActive,
eventCount,
}: {
result: ExecResult | null;
running: boolean;
telemetryActive: boolean;
eventCount: number;
}) {
const lines: TerminalLine[] = result
? [
{ text: `$ ${result.cmd.join(' ')}` },
...result.stdout.split('\n').filter(Boolean).map((t) => ({ text: t })),
...result.stderr
.split('\n')
.filter(Boolean)
.map((t) => ({ text: t, error: true })),
{
text: `exit ${result.returncode} (${result.duration_s.toFixed(2)}s)`,
error: result.returncode !== 0,
},
]
: [];
return (
<div className="space-y-2">
<div className="flex items-center justify-between">
<TelemetryStatus active={telemetryActive} eventCount={eventCount} compact />
<span className="text-[10px] uppercase tracking-wide text-slate-400 dark:text-slate-500">
read-only output (interactive shell lands in v0.4)
</span>
</div>
<TerminalFrame
lines={lines}
status={running ? 'streaming' : result ? 'finished' : 'idle'}
title="Sandbox output"
/>
</div>
);
}