Phase 4: Editor Shell
Status: Complete Spec ID prefix:
EDITPhase: 4 Completed: 2026-02-20
Overview
The editor shell provides the interactive coding environment where users solve challenges. Built on a Zustand vanilla store (@nthtime/editor), it manages file state, tab order, run state, and view mode transitions. The web frontend wraps the store in React context and renders a 3-panel CSS Grid layout (prompt | editor | output) with a lazy-loaded Monaco editor. The store is framework-agnostic -- the CLI reuses the same verification logic without the Monaco UI.
Dependencies
- [DSST-02] (Challenge type with referenceSolution)
- [DSST-05] (VerificationResult)
- [DSST-07] (UserSettings for fileStubs)
- [VRFY-06] (WASM grammar loading for browser verification)
User Flows
Opening a Challenge
- User navigates to a challenge page
- EditorStore initializes from the challenge data via
initFromChallenge() - If fileStubs is enabled (default), empty files are created at expected paths
- If a draft exists in localStorage, it is restored instead
- Reference solution files are stored for later diff comparison
- Monaco editor renders the active file with appropriate language highlighting
Editing Code
- User types in the Monaco editor
setFileContent()updates the file in the store- Draft auto-saves via debounced localStorage persistence
- User can create, rename, or delete files via the file tree
- User can open multiple tabs and reorder them
User can toggle split pane to view two files side-by-side(removed)
Multi-File Navigation
- File tree panel lists all files in the challenge
- Clicking a file switches the active tab
- Tab bar shows open files with close buttons
- Tabs can be reordered by drag
Split pane mode shows two editors side-by-side(removed)
Acceptance Criteria
Store Initialization
- [ ] EDIT-01 --
createEditorStore()returns a Zustand vanilla store with initial state (no files, null activeFilePath, idle runState). - [ ] EDIT-02 --
initFromChallenge()populates files from challenge data. When fileStubs is true (default), empty files are created at expected paths. When false, no files are created. - [ ] EDIT-03 --
initFromChallenge()stores the reference solution files for later diff/solution view.
File Operations
- [ ] EDIT-04 --
createFile()adds a new file, opens a tab, and activates it. Duplicate paths are no-ops. - [ ] EDIT-05 --
renameFile()updates the file path in files, tabOrder, and activeFilePath. Renaming to an existing path is a no-op. - [ ] EDIT-06 --
deleteFile()removes the file and tab, selecting an adjacent file. - [ ] EDIT-07 --
setFileContent()updates the content of an existing file. - [ ] EDIT-08 --
getAllFileEntries()returns all files as a FileEntry array.
Tab Management
- [ ] EDIT-09 --
openTab()adds a path to tabOrder and activates it without duplicating existing tabs. - [ ] EDIT-10 --
closeTab()removes from tabOrder and selects an adjacent tab. Closing the last tab sets activeFilePath to null. - [ ] EDIT-11 --
reorderTabs()swaps tab positions by index.
View Mode and Submit/Retry
- [ ] EDIT-15 --
submit()snapshots current files to submittedFiles and switches viewMode to 'results'. - [ ] EDIT-16 --
retry()restores submitted files as current files and switches viewMode back to 'editing'. - [ ] EDIT-17 --
setResultsCodeView()switches between 'submitted', 'solution', and 'diff' views.submit()resets it to 'submitted'.
Layout
- [ ] EDIT-18 -- The challenge page renders a 3-panel layout: prompt panel (left), editor panel (center), and output/results panel (right).
- [ ] EDIT-19 -- Multi-file challenges show a file tree for navigation and tab bar for open files.
Language Mapping
- [ ] EDIT-20 --
getMonacoLanguage()maps file extensions to Monaco language IDs (js->javascript, ts->typescript, py->python, html->html, css->css, json->json) with plaintext fallback.
Technical Context
Key Files
| File | Role |
|---|---|
libs/editor/src/index.ts | Public API: createEditorStore, types, draft storage, language mapping |
libs/editor/src/lib/editor-store.ts | Zustand vanilla store factory with all state and actions |
libs/editor/src/lib/draft-storage.ts | localStorage draft persistence (save/load/clear) |
libs/editor/src/lib/language.ts | File extension to Monaco language mapping |
apps/web/src/components/challenge/challenge-view.tsx | Main challenge component orchestrating editor + results |
apps/web/src/components/challenge/dockable-layout.tsx | 3-panel CSS Grid layout |
apps/web/src/components/challenge/monaco-wrapper.tsx | Monaco editor wrapper with theme and keybindings |
apps/web/src/components/challenge/file-tree.tsx | File list for multi-file challenges |
apps/web/src/components/challenge/tab-bar.tsx | Editor tabs with close and reorder |
Patterns and Decisions
- Zustand vanilla store --
createStore()instead ofcreate()keeps the store framework-agnostic. A React context bridge provides hooks. - Monaco lazy loading --
next/dynamicwithssr: falseavoids server-side rendering of the editor. - Monaco not hoisted --
monaco-editoris excluded from pnpm hoisting. Types must come from@monaco-editor/react(OnMount,EditorProps), never frommonaco-editordirectly. - monaco-emacs shim --
monaco-emacscallsrequire('monaco-editor')which resolves to the AMD bundle.next.config.jsaliasesmonaco-editor$to a shim that re-exportswindow.monaco. - FileStubs default --
initFromChallenge()defaults fileStubs to true, creating empty files at expected paths. When false, the editor starts blank.
Test Coverage
Unit Tests
| Criterion | Test File | Test Description |
|---|---|---|
| EDIT-01 | libs/editor/src/lib/editor-store.spec.ts | starts with initial state |
| EDIT-02 | libs/editor/src/lib/editor-store.spec.ts | initializes from a challenge with file stubs; starts with empty files when fileStubs=false |
| EDIT-03 | libs/editor/src/lib/editor-store.spec.ts | stores reference solution files on init |
| EDIT-04 | libs/editor/src/lib/editor-store.spec.ts | creates a new file and activates it; no-ops on duplicate |
| EDIT-05 | libs/editor/src/lib/editor-store.spec.ts | renames a file and updates activeFilePath |
| EDIT-06 | libs/editor/src/lib/editor-store.spec.ts | deletes a file and selects adjacent |
| EDIT-07 | libs/editor/src/lib/editor-store.spec.ts | sets file content |
| EDIT-08 | libs/editor/src/lib/editor-store.spec.ts | returns all file entries |
| EDIT-09 | libs/editor/src/lib/editor-store.spec.ts | openTab adds path and activates; activates without duplicating |
| EDIT-10 | libs/editor/src/lib/editor-store.spec.ts | closeTab removes and selects adjacent; on last tab sets null |
| EDIT-11 | libs/editor/src/lib/editor-store.spec.ts | reorderTabs swaps positions |
| EDIT-15 | libs/editor/src/lib/editor-store.spec.ts | submit snapshots files and switches to results |
| EDIT-16 | libs/editor/src/lib/editor-store.spec.ts | retry restores submitted files; full submit-retry cycle |
| EDIT-17 | libs/editor/src/lib/editor-store.spec.ts | setResultsCodeView changes view; submit resets to submitted |
| EDIT-20 | libs/editor/src/lib/language.spec.ts | maps JS, TS, Python, HTML, CSS, JSON; plaintext fallback |
E2E Tests
| Criterion | Test File | Test Description |
|---|---|---|
| EDIT-18 | apps/web/e2e/challenge-flow.spec.ts | renders 3-panel layout with prompt and editor |
| EDIT-04, EDIT-19 | apps/web/e2e/multi-file.spec.ts | create new file via file tree |
| EDIT-19 | apps/web/e2e/multi-file.spec.ts | switch between files using file tree |
Open Questions
- None at this time.