Studio Shell
The application around the editing component: the menubar, the lesson model,
panel navigation, view modes, identity and realm assertion, the target-silo
selector, the source-normalization pipeline, and the diagnostics surface.
Verified against POC build 213 (20260910T123348).
The shell is instructor-studio/js/app.js, with api.js for authenticated
fetch and canvas.js for mounting component assemblies. For the editing
component itself see Editor Component; for the
programmatic surfaces the shell exposes see
Formats and Contracts.
Contents
- Menus and labels
- The lesson model
- Panel navigation
- View modes
- Identity and realm assertion
- Target silo
- Spaces
- Source normalization
- Generate Lesson wizard
- Version badge and diagnostics
Menus and labels
This table is the canonical record of every user-visible control in the Studio. Both this guide and the instructor tutorials cite it rather than paraphrasing the source independently — the two document sets drifted apart once already, and a single owned vocabulary is the fix.
File
| Label | Shortcut | Behavior |
|---|---|---|
| Current lesson | — | Static row showing the open lesson, or “No lesson selected” |
| New… | mod+N |
Empty lesson |
| Generate… | — | Generate Lesson wizard |
| Open… | mod+O |
Realm/folder browser |
| Save | mod+S |
Writes the authoring tree |
| Save as… | mod+shift+S |
Location modal, including New folder… |
| Publish… | — | Publish wizard — disabled while the lesson is unsaved or modified since the last save |
| Assign… | — | Assign wizard — gated on a challenge-bearing publication |
| Reference Library & Spaces… | — | Spaces modal |
Edit
| Label | Shortcut |
|---|---|
| Undo | mod+Z |
| Redo | ⌘⇧Z on Mac, Ctrl+Y elsewhere |
| Cut | mod+X |
| Copy | mod+C |
| Paste | mod+V |
Modifier glyphs are platform-resolved at render time (⌘/⇧/⌥ on Mac,
Ctrl+/Shift+/Alt+ elsewhere). These items drive the focused component
through its public API; the editor also binds its own equivalents (see
Editor Component).
Lesson
Whole-lesson ĀYŌDÈ Intelligence. Each label’s verb is recomputed every time the menu opens — “Generate” when the target panel does not exist, “Update” when it does.
| Label | Target panel |
|---|---|
| Generate/Update summary panel | summary |
| Generate/Update learning objectives panel | objectives |
| Generate/Update key takeaways panel | takeaways |
See ĀYŌDÈ Intelligence Operations.
Library
| Label | Behavior |
|---|---|
| Upload resource… | File upload into the Reference Library |
| Upload resource from URL… | Fetch-and-upload by URL |
Menubar right side
| Control | Values |
|---|---|
| View mode toggle | Instructor View · Student View |
| Target silo | Development · Production |
| Identity chip | The signed-in username; opens the identity dialog |
The lesson model
A lesson is a tree of typed panels, nested up to five levels
(MAX_PANEL_LEVELS). Every panel owns its MyST source and carries a
frontmatter title.
panel.type |
Nav chip | Student-visible? |
|---|---|---|
myst |
Content | Yes |
mission |
Mission | Yes |
challenge |
Challenge | Yes — the student attempts it |
file |
File | No |
“Panel” is the category for student-visible tabs. Content, Mission and Challenge are all Panels — the student sees or attempts them. A File deliberately is not: it is bundled, compiled or executed behind the scenes and never rendered to the student directly. The editor toolbar badge mirrors the focused tab’s chip, using the same four-way vocabulary.
Challenge panels additionally carry an assessment model (panel.challenge)
joined to published shells by an authored, minted-once challengeRef; see
Challenges and Grading.
Panel navigation
The left pane lists the panel tree. Each row carries, left to right: outdent
‹, a drag handle ☰, indent ›, an outline number (2.1.3), the panel
title, its kind chip, and a delete button. A + Panel button sits below the
tree.
| Affordance | Behavior |
|---|---|
| Click | Select the panel |
| Double-click | Rename in place |
‹ / › |
Outdent / indent. ‹ is hidden at the outermost level; › is hidden at the innermost level and on a first child, so an indent can never orphan a row |
☰ drag |
Reorder; a panel cannot be dropped into its own descendants |
| Delete | Removes the panel and its children |
+ Panel |
Menu: New Content · New Mission |
| Right-click | Menu: New challenge · Open… · Save · Save as… |
New challenge is gated on the selected panel (not the clicked row) having a mission ancestor — where a mission counts as its own ancestor — and the new challenge is appended to that mission’s children. Challenges therefore cannot be created at the top level or nested inside one another.
The pane is resizable by a pointer-capture drag handle between 12 and 22 rem,
with a 320 px content floor reserved for the editor. The width persists in
localStorage and survives the ‹‹/›› drawer collapse, which also makes
the handle inert. Dragging can never blur the editor, so
normalization cannot fire mid-drag. (Student Lab
mirrors the affordance on its progress pane: 16–34 rem, 384 px floor.)
View modes
Instructor View shows the authoring surface. Student View switches the mounted components into student mode — the editor collapses to preview-only, and challenge panels present their response controls instead of their options pane. Entering student mode re-fetches components with no-cache and deals a fresh choice pool, so a stale student view cannot persist across a switch.
Identity and realm assertion
Sign-in is Cognito SRP, direct against the user pool, from the identity chip. The dialog shows the decoded claims; a quick-switch affordance moves between saved identities without a full sign-in. Saved identities are silo-independent in principle — only the session bound to one is not.
Every API call carries a bearer token plus an asserted realm EID. Two behaviors are worth knowing when reading error paths:
- Lesson-domain 403 fallback. A 403 from the lesson domain is retried against the assertion locus, and the authorization detail is extracted into the diagnostic log rather than surfaced raw.
- Rate limiting.
invokeAPIhonors 429 by waiting for the response envelope’sretryAfterand retrying, rather than failing the operation.
API errors surface the envelope’s userMessage, with the deepest embedded
error detail preferred over the outermost wrapper, and never a raw JSON dump.
Target silo
A menubar selector chooses Development or Production as the target silo. The choice persists, and the selector is colour-highlighted so the active silo is unmistakable.
Switching silos discards state loaded from the old one. The Studio holds the richest state of the four POCs, and all of it is silo-bound:
- the Authoring, Publishing and Reference Library space selections — both in memory and persisted, so the next reload cannot silently re-hydrate an old silo’s realm EIDs;
- the in-memory lesson and panel editor state, so a save or publish can never carry an old silo’s lesson identity or component source URI into the new one;
- three single-slot module caches (component mirrors, and the resolved MyST-editor and challenge-config assemblies) that otherwise resolve once from a catalog and are never invalidated.
Clearing the token also clears the active identity key as a side effect, since there is no session against the new silo yet. Clearing the Reference Library selection then forces the Spaces modal, which is the same branch boot takes on a first load — so the post-switch experience matches first-run rather than needing its own path.
Running locally, the Caddyfile proxies silo-scoped paths
(/__silo/development/..., /__silo/production/...) so switching the
selector keeps the two silos’ data genuinely isolated.
Spaces
Three persisted realm selections, set from File ▸ Reference Library & Spaces… through a realm-tree picker:
| Space | Role |
|---|---|
| Reference Library | Where uploaded reference material lives, and the RAG corpus AI operations draw on |
| Authoring Space | Where the authoring lesson tree is saved |
| Publishing Space | Where published lessons and their assets are written |
Each persists in localStorage under its own key. With no Reference Library
set the modal is forced — its close is a no-op until a library is
actually picked — which is how both first boot and a silo switch guarantee
the Studio has somewhere to read from.
Source normalization
An ordered, idempotent, single-flight pipeline normalizes panel sources before they are persisted. Two transforms run, in this order:
glossary-termify— writes explicit{term}`…`roles into content-panel sources.remove-redundant-whitespace— applies the editor component’swsEditsoracle. Despite the name this also unwraps hard-wrapped paragraphs and strips embedded HTML; see what the tidy pass actually removes.
The order matters: term-ification runs first because freshly written
{term} role bodies parse as inline code, which the whitespace pass is
required to leave alone.
Triggers: editor focus loss, panel switch, before every save, before every publish, and the explicit menu commands. Challenge panels, mission panels and the Glossary panel are excluded from the focus-loss trigger.
Invariants:
- Each transform receives a shared per-run context (parsed AST, the current
term list, the editor API) and degrades gracefully — a transform that
cannot run returns the source unchanged rather than guessing. If the AST
bundle failed to load,
wsEditsreturnsnulland the pass is skipped entirely; protected spans would otherwise be unknowable. - Dirty-marking, re-pointing the visible editor via
setContent, and change notification are centralized in the pipeline, never per transform. - Overlapping triggers serialize into one promise chain. Every transform is idempotent, so a second run over normalized source changes nothing — it neither dirty-marks nor re-points.
Consequence: persisted and published sources are always normalized and
always carry their {term} roles. Saving a hard-wrapped document, or one
containing inert HTML, deliberately changes its bytes and therefore its
content hash relative to what the author typed.
Generate Lesson wizard
File ▸ Generate… chains three real AI configurators into a drafted lesson: objectives (synchronous) → outline (async) → lesson text (async) → straight into the MyST editor.
| Step | Collects |
|---|---|
| 1. Select Authoring Space | Target realm for the drafted lesson |
| 2. Curriculum Alignment | Region → subregion → standard authority → standard, drilled to a leaf |
| 3. Structure | Depth (introductory / intermediate / advanced), scope, duration in minutes, and which components to include — objectives, warm-up, instruction, practice, assessment |
| 4. Lesson Objectives | Generated knowledge/reasoning/performance objectives, for review |
| 5. Lesson Outline | Generated outline, for review |
| 6. Lesson Text | Generated lesson, loaded into the editor |
Depth maps to a grade band for the prompts: introductory → middle school, intermediate → high school, advanced → undergraduate.
| Stage | Configurator |
|---|---|
| Objectives | curriculum-standards-to-knowledge-reasoning-performance-v2 |
| Outline | knowledge-reasoning-performance-to-lesson-outline-v2 |
| Lesson | lesson-outline-to-lesson-v2 |
Version badge and diagnostics
The bottom-right badge is the only on-page confirmation of which build is running, and it is assembled from four CSS custom properties:
| Property | Set by | Meaning |
|—|—|—|
| --poc-version | css/styles.css | build N · timestamp — bumped on every change |
| --poc-js | app.js, at module start | ` · js — proves the module executed, independently of the build string |
| –poc-local | app.js, when overridden | · LOCAL COMPONENTS |
| –poc-cdn` | the content-delivery probe | CDN state |
Because --poc-version comes from CSS, the build number still shows if the
module fails to execute — and the absence of ` · js` is then the diagnosis.
| Gesture | Effect |
|---|---|
| Click | Copies the structured boot/error log to the clipboard and flashes the badge; reports the line count, or an explicit failure if the clipboard is unavailable |
| Right-click | Opens a one-item menu: Use local components for testing, with a ✓ when active |
The local-component override is URL state only —
?useLocalComponentForTesting=1, deliberately never persisted — so the
right-click menu toggles it by rewriting the URL and reloading. Navigation
is the toggle. There is no fallback in either direction: a component that
cannot load from its selected source is a hard, visible failure rather than a
silent downgrade.
window.__studioDev exposes headless hooks (panel trees, the assembled
publish package, editor state) for verifying publish invariants without
driving the UI.