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

  1. User navigates to a challenge page
  2. EditorStore initializes from the challenge data via initFromChallenge()
  3. If fileStubs is enabled (default), empty files are created at expected paths
  4. If a draft exists in localStorage, it is restored instead
  5. Reference solution files are stored for later diff comparison
  6. Monaco editor renders the active file with appropriate language highlighting

Editing Code

  1. User types in the Monaco editor
  2. setFileContent() updates the file in the store
  3. Draft auto-saves via debounced localStorage persistence
  4. User can create, rename, or delete files via the file tree
  5. User can open multiple tabs and reorder them
  6. User can toggle split pane to view two files side-by-side (removed)

Multi-File Navigation

  1. File tree panel lists all files in the challenge
  2. Clicking a file switches the active tab
  3. Tab bar shows open files with close buttons
  4. Tabs can be reordered by drag
  5. 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

FileRole
libs/editor/src/index.tsPublic API: createEditorStore, types, draft storage, language mapping
libs/editor/src/lib/editor-store.tsZustand vanilla store factory with all state and actions
libs/editor/src/lib/draft-storage.tslocalStorage draft persistence (save/load/clear)
libs/editor/src/lib/language.tsFile extension to Monaco language mapping
apps/web/src/components/challenge/challenge-view.tsxMain challenge component orchestrating editor + results
apps/web/src/components/challenge/dockable-layout.tsx3-panel CSS Grid layout
apps/web/src/components/challenge/monaco-wrapper.tsxMonaco editor wrapper with theme and keybindings
apps/web/src/components/challenge/file-tree.tsxFile list for multi-file challenges
apps/web/src/components/challenge/tab-bar.tsxEditor tabs with close and reorder

Patterns and Decisions

  • Zustand vanilla store -- createStore() instead of create() keeps the store framework-agnostic. A React context bridge provides hooks.
  • Monaco lazy loading -- next/dynamic with ssr: false avoids server-side rendering of the editor.
  • Monaco not hoisted -- monaco-editor is excluded from pnpm hoisting. Types must come from @monaco-editor/react (OnMount, EditorProps), never from monaco-editor directly.
  • monaco-emacs shim -- monaco-emacs calls require('monaco-editor') which resolves to the AMD bundle. next.config.js aliases monaco-editor$ to a shim that re-exports window.monaco.
  • FileStubs default -- initFromChallenge() defaults fileStubs to true, creating empty files at expected paths. When false, the editor starts blank.

Test Coverage

Unit Tests

CriterionTest FileTest Description
EDIT-01libs/editor/src/lib/editor-store.spec.tsstarts with initial state
EDIT-02libs/editor/src/lib/editor-store.spec.tsinitializes from a challenge with file stubs; starts with empty files when fileStubs=false
EDIT-03libs/editor/src/lib/editor-store.spec.tsstores reference solution files on init
EDIT-04libs/editor/src/lib/editor-store.spec.tscreates a new file and activates it; no-ops on duplicate
EDIT-05libs/editor/src/lib/editor-store.spec.tsrenames a file and updates activeFilePath
EDIT-06libs/editor/src/lib/editor-store.spec.tsdeletes a file and selects adjacent
EDIT-07libs/editor/src/lib/editor-store.spec.tssets file content
EDIT-08libs/editor/src/lib/editor-store.spec.tsreturns all file entries
EDIT-09libs/editor/src/lib/editor-store.spec.tsopenTab adds path and activates; activates without duplicating
EDIT-10libs/editor/src/lib/editor-store.spec.tscloseTab removes and selects adjacent; on last tab sets null
EDIT-11libs/editor/src/lib/editor-store.spec.tsreorderTabs swaps positions
EDIT-15libs/editor/src/lib/editor-store.spec.tssubmit snapshots files and switches to results
EDIT-16libs/editor/src/lib/editor-store.spec.tsretry restores submitted files; full submit-retry cycle
EDIT-17libs/editor/src/lib/editor-store.spec.tssetResultsCodeView changes view; submit resets to submitted
EDIT-20libs/editor/src/lib/language.spec.tsmaps JS, TS, Python, HTML, CSS, JSON; plaintext fallback

E2E Tests

CriterionTest FileTest Description
EDIT-18apps/web/e2e/challenge-flow.spec.tsrenders 3-panel layout with prompt and editor
EDIT-04, EDIT-19apps/web/e2e/multi-file.spec.tscreate new file via file tree
EDIT-19apps/web/e2e/multi-file.spec.tsswitch between files using file tree

Open Questions

  • None at this time.