Conversation
There was a problem hiding this comment.
Code Review
This pull request implements a comprehensive rebuild of the tutorial copilot system, shifting it to a silent-by-default, event-driven architecture. Key additions include an intervention level state machine (Silent, Nudge, Guide) driven by progress verdicts, gated custom elements and in-editor code guides, a stage ruler tool for measuring distances and angles, and a programmatic course runner dev harness for runtime verification. Feedback on the documentation highlights a few inconsistencies, specifically the mention of the deprecated <tutorial-progress> tag, mismatched intervention level thresholds compared to the code, and a stray code fence in the verification guide.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| Tutorial course (`TutorialRoot.vue`): `tutorial-progress`, `tutorial-course-success`, | ||
| `tutorial-course-exit-link`, `tutorial-course-abandon-prediction`, | ||
| `tutorial-course-abandon-dismissal`, `api-reference-filter`, `stay-silent`; and — only at | ||
| intervention level ≥ nudge (`tutorial-guidance.ts`) — `guide-modal`, `spotlight-hint`, `api-video`. |
There was a problem hiding this comment.
The documentation mentions <tutorial-progress> here and in lines 315-318. However, the "审查修复变更记录" section (line 247) states that <tutorial-progress/> has been deprecated and replaced by the <user-progress-*> verdicts. To avoid confusion and keep the documentation consistent with the implementation, the description of <tutorial-progress> should be removed from this document.
| * **Level 1 — silent** (the first 5 events since progress): observe only. No guidance, whatever you | ||
| think the user should do. Let them explore, fail, and retry. | ||
| * **Level 2 — nudge** (after 5 events): the user is stuck. Give ONE short hint via | ||
| <guide-modal>...</guide-modal> — plain text, at most 30 characters, no other elements inside, | ||
| pointing the direction (what to check, where to look), NEVER the answer or the code. If the hint did not help, or | ||
| the problem is finding something on screen, point at the exact UI element with | ||
| <spotlight-hint target-id="..." tip="..." /> (everything else is dimmed), or explain the relevant API with | ||
| <api-video api="..." /> when it has an explainer video. | ||
| * **Level 3 — guide** (after 8 events): the nudges did not work. Now guide the concrete code edit | ||
| with the in-editor guides <code-drag-hint> / <code-type-hint> / <code-change-hint> / <code-delete-hint>. (Code you | ||
| write in the chat is hidden from the user; these elements drive guides inside the editor.) |
There was a problem hiding this comment.
The description of intervention levels seems to use simplified thresholds ("after 5 events", "after 8 events"). This might be inconsistent with the actual implementation logic described in docs/develop/tutorial/whats-new.md and the changelog within this file, which mention neutralThreshold = 6, backThreshold = 3, and counters resetting upon escalation. For the LLM to behave correctly and for clarity to human reviewers, the prompt's explanation should accurately reflect the underlying state machine logic. For example, it's unclear how "after 8 events" for Level 3 is reached based on the provided thresholds.
| even if a trailing `<stay-silent />` hides it from the user that round. | ||
| - To inspect the exact request sent TO the model (not the reply), use the `fetch` hook in | ||
| [llm-payload.md](./llm-payload.md#如何在运行时查看真实-payload). | ||
| ``` |
There was a problem hiding this comment.
Review summary
Reviewed the tutorial-refactor / silent-by-default copilot / capability-gated guidance changes across ~92 files (Vue 3 + TS, spx-gui) using code-quality, performance, security, and documentation-accuracy passes. The implementation is generally solid: timers/listeners are cleaned up, guidance levels are enforced as a hard registration boundary (tutorial-guidance.ts), and copilot markdown is sanitized. whats-new.md, course-authoring.md, and verifying-courses.md match the code.
The main issues are documentation drift in llm-payload.md (its main-body "map" and verbatim appendix still describe the pre-refactor model and contradict the shipped code and the sibling whats-new.md), plus one confirmed intervention-state bug on in-course restart. Inline comments cover concrete diff-line findings; broader items are below.
Findings
1. [Bug] Intervention level & counters are not reset on in-course restart — spx-gui/src/components/tutorials/tutorial.ts
restartCurrentCourse() → restartCourse() → startCourse() reassigns this.course.value to the same Course object reference. The TutorialRoot.vue watch(() => tutorial.currentCourse, ...) only fires on a reference change, so on an in-course restart the TutorialIntervention instance is not recreated or reset(). Its levelRef, neutral/back counters, and processedRoundCount all carry over, which (a) contradicts the documented "a course (re)start also resets it", and (b) leaves processedRoundCount larger than the restarted session's rounds.length, so the first verdicts of the restarted course are silently skipped. Note the success-modal "Try again" path is fine — it goes through endCurrentCourse() (sets course.value = null) first, so the null→non-null transition fires the watcher. Only the still-running restartCurrentCourse path is affected. Consider resetting the intervention explicitly on restart. (See inline on tutorial.ts.)
2. [Docs, medium] llm-payload.md intervention thresholds are stale — docs/develop/tutorial/llm-payload.md
The doc states thresholds "nudge=5、guide=8 已按当前代码渲染", but the code uses two verdict counters: neutralThreshold = 6 / backThreshold = 3 (tutorial-intervention.ts:24,26), with no single event count. whats-new.md:34 has the correct values. The verbatim appendix ("三、课程协议全文") likewise reproduces the old event-count ladder and the deprecated <tutorial-progress /> reporting element; it should be regenerated against the current generateTopic in tutorial.ts.
3. [Perf, low] Duplicated per-keystroke full-project serialization — spx-gui/src/components/editor/copilot/user-events.ts:122 and :168
watchCodeChange and watchDiagnostics share the getter () => [stage.code, ...sprites.map(s => s.code)].join('\n'). It re-runs and allocates a full joined copy of all project source on every keystroke — twice (once per watcher) — since the debounce only guards the callbacks, not the getter. The getters are pre-existing, but this PR adds the second watcher over the same shape. Consider one shared computed/version signal both watchers depend on.
4. [Perf, low] Per-keystroke view-zone rebuild for the drag hint — spx-gui/src/components/xgo-code-editor/ui/code-guide/CodeGuideUI.vue:234
dragNewLineZone reads docVersion.value, so the useViewZone effect tears down and rebuilds the (static) drag node on every keystroke while a drag guide is active. Gate on the isNewLineInsertion(...) boolean instead of raw docVersion. Low impact (drag guides are short-lived); the type/change zones are unaffected.
Lower-confidence / worth a look
5. [Robustness] Un-gated onMounted side effects on session restore — spx-gui/src/components/tutorials/tutorial-course-abandon.ts:23,52 (and TutorialCourseSuccess.vue:46)
Unlike the sibling tutorial elements (spotlight-hint.ts, GuideModal.vue, ApiVideo.vue, use-code-guide.ts), the abandon-prediction/dismissal (and success) elements call predictAbandon() / dismissAbandon() / endCurrentCourse() unconditionally in onMounted, without the !(round.isLive && round.isLastRound()) guard. Since copilot sessions are persisted and restored, historical instances re-mount and re-fire on reload, which could spuriously escalate/end a course. These predate the PR but are now relied on within the restore-aware architecture; adopting the same gate would make the element family consistent.
6. [Minor, dev-only hardening] spx-gui/src/apps/xbuilder/pages/devtools/course-runner.vue:190
The ?autorun= handler runs new Function(src)(); it is DEV-gated in router.ts and constrained to startsWith('/'). The prefix check does not reject protocol-relative URLs (//host/...); new URL(autorun, location.origin).origin === location.origin would be a stronger same-origin guard. Not exploitable in production (route absent from prod builds).
Note: the diff patch surfaced a node_modules/.vite/vitest/.../results.json new-file hunk, but it is not in the PR's changed-files list (GET /pulls/3355/files) nor in origin/dev...HEAD, so it is a local diff-generation artifact, not part of this PR — no action needed.
| } | ||
|
|
||
| /** Restart the current course from its initial state. */ | ||
| async restartCurrentCourse(): Promise<void> { |
There was a problem hiding this comment.
Intervention state is not reset on in-course restart. restartCurrentCourse → restartCourse → startCourse sets this.course.value = course to the same Course object reference. The TutorialRoot.vue watch(() => tutorial.currentCourse, ...) fires only on a reference change, so the TutorialIntervention is never recreated/reset() here — its level, neutral/back counters, and processedRoundCount carry over from before the restart. This contradicts the documented "a course (re)start also resets it", and because processedRoundCount is now > the restarted session's rounds.length, the restarted course's first verdicts are silently skipped. (The success-modal "Try again" path is fine — it goes through endCurrentCourse() first, so the null→non-null transition fires the watcher.) Consider resetting the intervention explicitly on restart.
| Editor (`editor/copilot/index.ts`, when editing): `code-link`, and — only while the intervention | ||
| level allows (`editorCopilotCodeGuides` gate) — `code-change-hint`, `code-drag-hint`, | ||
| `code-type-hint`, `code-delete-hint`. | ||
| Tutorial course (`TutorialRoot.vue`): `tutorial-progress`, `tutorial-course-success`, |
There was a problem hiding this comment.
tutorial-progress no longer exists in the code — there are zero matches under spx-gui/src/components/tutorials/; it was replaced by user-progress-ahead / -neutral / -back (user-progress.ts). This registered-elements list is stale (the doc's own changelog at line 247 notes the deprecation). The appendix entry at line 315 and the protocol text at 545/570 reference it too.
| Tutorial course (`TutorialRoot.vue`): `tutorial-progress`, `tutorial-course-success`, | ||
| `tutorial-course-exit-link`, `tutorial-course-abandon-prediction`, | ||
| `tutorial-course-abandon-dismissal`, `api-reference-filter`, `stay-silent`; and — only at | ||
| intervention level ≥ nudge (`tutorial-guidance.ts`) — `guide-modal`, `spotlight-hint`, `api-video`. |
There was a problem hiding this comment.
Incorrect: api-video is registered at Silent (level 1), not "≥ nudge". See tutorial-guidance.ts:45 — [InterventionLevel.Silent]: { elements: [apiVideo], ... }; only guide-modal and spotlight-hint are Nudge-level. whats-new.md:28 documents this correctly ("Silent | api-video only"), so the two docs currently contradict each other. (The protocol only restricts the proactive use of api-video to nudge+, but it is registered — and available for answering — at Silent.)
|
|
||
| #### 2a. Registered custom elements (the emittable tags) | ||
|
|
||
| Global (always, `CopilotRoot.vue`): `page-link`, `highlight-link`. |
There was a problem hiding this comment.
The global registered-elements list omits thinking. CopilotRoot.vue:201-205 registers thinking globally alongside page-link/highlight-link. Relatedly, the appendix at line 357 claims thinking is "代码里现成但没有任何地方注册" (present but never registered) — that is now false and contradicts the doc's own changelog at line 239.
An import that succeeds but breaks every course identically, with a TypeError deep in app code, is an app-backend contract change — not something a package diff can explain. Originally this commit also carried a `references ??= []` bridge in the course API client, added when the redeployed backend stopped sending the field while this branch's `Course` type still promised an array. Its own note called that a bridge "the next upstream merge retires": upstream goplus#3381 removed the field, so rebasing onto dev retires it here, along with the last `references` mention in the tutorial test. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The editor's sprite model pulls animation-referenced costumes out of the wearable list, so a sprite with no standalone costume loads empty and renders nothing in edit mode — while the engine does no such extraction, so the game and the course-runner harness look perfectly fine. Two packages in a row shipped hand-built sprites with exactly this shape; runtime-green-but-editor-invisible is the signature, and the validator now flags it with the fix (keep one costume no animation references, like the original sprites' standalone default). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three additions the next course batch builds on:
- A course opening can now spotlight a sprite on the edit stage
(`{ "spotlight": { "sprite": "Boat" } }`). The stage viewer keeps
invisible, click-through radar anchors over each sprite in focused
mode, so "click the boat" is spotlightable even though the stage is
one canvas — and the click that dismisses the spotlight is the same
click that selects the sprite.
- A spotlight step whose target appears only after the user acts on the
previous one (the selected sprite's name label, say) declares
`"patient": true` instead of inheriting retry-then-skip. Chained,
these teach name insertion end to end.
- The video dialogs play with sound again: hover-card style stayed, the
muting didn't (the hover card is an ambient preview; a dialog is
deliberate viewing, and the story video has a soundtrack). If the
browser blocks unmuted autoplay, playback falls back to muted rather
than freezing — with no controls there is no way to unstick a paused
video.
The API-entry matcher also accepts `name#N` now, pinning an overload
without pinning the engine module version: the spx v2→v3 bump silently
broke a course whose `apis` used a full definition ID (the entry matched
nothing, so the panel stopped narrowing). Full IDs are now discouraged
in the docs, and the xbcs skill's validator warns on them; the focused
UI's ≈0.9:1 stage geometry is documented as an authoring rule alongside
(4:3 maps cover only two thirds of it).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The completion signal fires the frame the sprite touches its goal, and the dialog used to open that same instant — covering the very pickup the user was watching for. The dialog now renders from `revealedCompletion`, which follows `completion` one second later. Only the dialog waits: `completion` itself is still set immediately, because the ambient-event gate and the copilot's "Course completed" event key on it — delaying those would reopen the supersession race around the evaluation comment that the gate exists to prevent. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The focused-UI stage-shape rule (0.9:1 maps) turned out to need more than authoring guidance: the frontend pins the run viewport to 480×360 (an explicit TODO in the project model), and even with that lifted the engine renders the world at 4:3 for a 480×528 fillRatio map — the whole chain needs a designed solution, not a doc rule. Withdrawn until that plan exists; courses stay 4:3 meanwhile. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…atch Upstream goplus#3381 bumped the archive format to v2 (v1 minus `courses[].references`), and the importer compares the version for exact equality — a v1 package uploaded to a v2 deployment dies on "Unsupported course series file format", a message that names no version and does not say which side is stale. The validator hardcoded v1, so it passed the very file the site was about to reject. It now accepts either version, warns when a package declares the older one, and notes leftover `references` under v2. The reference doc says plainly that the version belongs to the target deployment, not to the checkout building the package, and that exporting from the target once is the way to learn which one to write. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… was A course's runtime signal says the learner reached the goal. It cannot say they got there the way the course teaches: walking a square by hand collects the same four mushrooms as `repeat 4`, and a course asking for `turn` can be won by a lucky starting heading. Those courses had to fall back to `judge: "copilot"`, paying an LLM round to look at code. `complete.require` makes that a second tier of the code judge. Once the runtime signal lands, the learner's open document must contain the named tokens as whole words — comments and string literals stripped first, so the course's own "试试 repeat" hint cannot satisfy its own requirement. Both met completes the course; primary met and secondary missed opens a modal that credits the run and names what is still missing, which is the feedback the copilot was being paid to write anyway. Courses 27 / 36 / 39 / 41 move off `judge: "copilot"` accordingly, leaving only the two courses that judge what the learner says rather than what their program does. `xbcs.py` gains the matching authoring check: a requirement whose tokens already appear in the starting code is inert — it makes the course look stricter while judging nothing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… courses Loops move ahead of conditions: repetition is easier for a child to observe than branching, and the loop unit needs nothing the condition unit teaches. `onStart` travels with the conditions, which are the first courses whose code lives in its shell. The condition unit itself now climbs the authoring arc one concept at a time — observe the problem, meet the query (`IsMature` alone), `if`, `if/else`, transfer to a fresh scene — instead of introducing three ideas in one course. The heading-based branches are gone with it; `==` moves to the counting unit, where comparing numbers is what the course is already doing. `distanceTo` gets the same observe-then-write split the boat unit uses, bridged by the Ruler the learner has been measuring with since course 3. The onKey course's `turn 0` / `turn 180` handlers read as absolute directions but `turn` is relative — verified against the engine — so repeated presses drifted; they are `turnTo` now. SKILL.md records two course-runner failure modes that read as broken content: a second loadXbp on one page silently kills the runner, and a hidden browser tab throttles the engine ~20x, so short timeouts truncate multi-pickup runs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… edited The success dialog's evaluation and the retry dialog's hint are prose about code — they name `repeat`, `distanceTo`, `if`. Both now render through the copilot's own MarkdownView, so that code reads in the dialog exactly as it does in the chat, down to the teaching rule that makes a code block unselectable. Nothing new is styled: a third copy of the Markdown CSS is precisely what the existing TODO on the second copy warns against. The evaluation's sanitizer moves into its own module, and keeps line breaks. It used to collapse every run of whitespace, which flattened any list or fenced block into a single line of literal Markdown syntax — invisible while the dialog rendered plain text, load-bearing now. The code area also carries a thumbnail of what it is editing. The document tabs already showed one, but the simplified layout hides them, and that is where it matters: a child writing `Boat.step` needs to know the code in front of them is Lita's. So it appears only when the tabs are gone, and takes a strip of padding instead of floating over the code — verified against a long line, which otherwise runs underneath and hides the character being read. It reads a thumbnail the text document already exposes, so the generic editor learns nothing about sprites. Course hints name their code in backticks accordingly. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… touch The designer's reordering moves the boat and loop units up and drops the distanceTo pair beside the variables it feeds: name a number, change it, get one from distanceTo, hand it to step, store it. That reads better than sitting distanceTo next to the ruler courses, because what makes it worth learning is that it returns something you can use. Auditing it against the course that *teaches* each API — the one playing its video, not merely the first `apis` declaration — turned up one conflict: the watering course leaned on distanceTo three courses before it is taught. It leaned on it to stop short of the mushroom, and that mattered, because a pickup happened only on `onTouchStart`: a learner who walked onto the sprout and waited had already spent that event, and ripening handed them nothing. The course was steering around a dead end in the scene. So the scene loses the dead end instead. Ripening is the other moment a pickup can happen, and the mushrooms now check for it — verified both ways against the engine: standing on the sprout through ripening yields the mushroom, and the reference answer still works. Eighteen sprites across six courses had the same hole, including the `for` unit, where walking to a mushroom before waiting for it is the order a child reaches for first. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The step and turn explainers still showed the rabbit and the carrot, which the courses stopped using when the assets were swapped for the squirrel and the mushroom — so the first two videos a child ever sees disagreed with every course after them. The re-shot takes replace them. turnTo gains a library entry it never had. Course 16 teaches it and asks for its video, and with no entry the demo fallback answered instead: the course that teaches "face the mushroom" opened with a rabbit walking 200 steps. The fallback follows the step video for the same reason, so knowledge points still waiting on their own explainer at least show current artwork. The tests asserted which file each knowledge point resolves to, so re-shooting a video failed them for no defect. They assert the routing now — exact ID, any overload, bare name, fallback — which is what the resolution actually promises. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…nce card The hover card's job is the signature and the description. Embedding the explainer video there meant a video started playing under the cursor every time a learner brushed past an item, competing with the code they were reading — and it pushed the card tall enough to cover much of the panel. A knowledge point is explained in one place now: the opening dialog. The card returns to exactly what it was when this branch started, in the simplified layout too — signature shown, Explain offered, stays open when the pointer moves onto it. The video provider it was fed by is gone with it: that extension point existed only for this card, and an extension point with no consumer is not a seam, it is clutter. What stays is the work that was never about the card: the definition-id handle a course spotlight aims at, and the highlight a drag guide pulses. The editing-document thumbnail grows to the size of the document tabs it stands in for, which is what makes it read as the sprite rather than as decoration. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`UITooltip` declares `class` as a prop that styles the tooltip bubble, so the positioning the caller put on this component never reached the badge — it was merged into the bubble, where Tailwind's merge let `absolute` beat the bubble's own `fixed` and `z-1` beat `z-1000`. The bubble became an absolutely positioned box with `left: 0` and `right: 4px`, stretching 728px across the preview; the badge meanwhile was landing in the right place only by accident of flex order, costing the code area its own width on top of the reserved strip. A plain wrapper takes the caller's class, so placement places and the bubble shrink-wraps its label again. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`turn 90` and `turn Right` are one function — spx's `Direction` is a float64 and `Right` is the constant 90 — so no definition ID or overload can tell the notations apart, and the API reference lists only the word form. This is not an overload to key a video on; it is a notation, which is a teaching point. So the angle form gets an ID of its own, the shape the ruler already uses for a knowledge point that is not an API. Course 5 keeps `turn` for the direction words; course 9 gains `turn-degrees` — a course whose own guidance already told the copilot to send the learner back to "the opening" for that notation, where nothing had ever been. Both takes are re-shot with the current assets, so `turn` stops showing the rabbit. A test pins the two apart: they are one `getApiVideo` name-match away from collapsing into a single video. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ument tab Course authors have been naming code in preludes the way they name it everywhere else — "`turn 90` 表示向右转 90 度" — and the modal was printing the backticks. It renders through the same Markdown view the course dialogs use, at the prelude's own reading size rather than the chat's. The editing-document badge takes the look the document tabs already had for the document you are on: the same 40px tile, primary border, primary tint. It was showing the same image from the same source, so looking like something else was the only thing distinguishing it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ld name Importing the series surfaced it: course 9 opened its new "turn <degrees>" dialog and played the rabbit walking through `turn Right` — the July take. The bucket sends neither `Cache-Control` nor `ETag`, so a browser that had fetched `turn.mp4` before kept those bytes on heuristic freshness and never revalidated. Verified against the network: 568,847 bytes from July in cache, 999,668 bytes from today on the wire. Every other take arrived under a new name and was fine; this one reused an old one, so the URL is what has to change. The authoring guide now says not to replace a video in place — the people it breaks are exactly the ones who watched the previous version. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The badge stood 4px from the panel edge and 16px down; the document tabs it stands in for stand 8px in and 12px down. Their spacing comes from the right aside's own padding (`px-2 py-3`), which the badge has no part in — it is positioned inside the editor box, whose `my-3` already supplies the 12. Measured against the standard layout, all three distances now match: 8px from the edge, 12px from the top, and 8px of air between the code and the badge. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…s title Course 14 teaches stepTo and asks for its video, but the library had no entry for it, so the demo fallback answered: the course that teaches "walk straight to the mushroom" opened with the step explainer instead. Same hole turnTo had. The dialog also stops repeating the API name above the video. Every explainer opens on the line of code it is about, so the title restated what the first frame already says. The stage's sprite-name label loses the border it never asked for: Tailwind's preflight is off here, so a bare `<button>` keeps the user agent's `2px outset`, which read as a ridge around the label. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ript Reported: the dialog credits the learner with an API they never used. Reproduced on course 14 — solved with `turn -41` / `step 213`, no `stepTo` anywhere, and the comment came back "指定目标可以让移动更直接", which is the sentence the course prompt told the copilot to send. It was not improvising; it was reciting. 47 of the 53 courses scripted that sentence, and 11 of those named an API, so 11 courses could tell a learner they used something they had not. Two things were missing. The copilot had no view of the code at that moment, so asked for a comment it described the reference answer — telling it to "read their code first" only produced a confident paragraph about `stepTo Mushroom` while the editor held `step 213`. It now receives the code with the event: the fact, rather than an instruction to go find it. And the courses no longer script the sentence at all. The protocol gives rules — one sentence, only constructs the code contains, say what it accomplished, never invite more work — and the teaching point stays where it belongs, in `## 目标`. Course 14 also gains the secondary goal it was missing: reaching the mushroom by measuring is a fine answer to the goal, but it is not the lesson, so it now asks to try `stepTo` instead of congratulating the wrong thing. The skill documents how to author both tiers, including the two ways to get it wrong. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The comment rules said never to invite more work, which two courses ask for in as many words: watering and naming a number both end by mentioning the construct a learner who solved it another way did not use. That is a remark about what they did, not homework, and it is how those courses teach without a secondary goal — so the rule now says not to assign work, and names the exception rather than contradicting it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…seen Course 14 opened without its stepTo video, and the course was fine: the knowledge point was marked watched, from testing that course earlier the same day. Showing a concept once is the intent, but the marker is per knowledge point rather than per video file, and the library is still being shot — the reshot `step` and `turn` takes can never reach anyone who saw the takes they replaced. It also makes a course hard to review, since the second visit plays no opening at all. Watching is still recorded, so this is one constant away from coming back once the library settles. A test holds the current behaviour, and will be the thing that fails when someone flips it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…cted The name label is how you get a sprite's name into your code, but it only appeared on the selected sprite — and selecting a sprite switches the editor to that sprite's file. So the one name you could insert was the name of the file you were already in. Writing `stepTo Mushroom` in Lita's code, the very case course 14 teaches, was the case the label could not serve. Hovering now shows the same label on whatever sprite is under the pointer, and clicking it inserts without selecting, so the editor stays where it is. Hit-testing goes through Konva's own hit graph, which respects z-order and the artwork's transparency rather than a bounding box. Leaving the sprite does not hide the label at once: the pointer has to cross the gap between the sprite and the label to click it, and a label that vanished on the way would be one you could see but never press. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The label lives inside the stage container, so moving across it bubbled a mousemove up to the stage's hover tracking, which hit-tested that point, found no sprite under the label, and started hiding the thing the pointer was sitting on. Entering it cancelled the countdown once; the next move restarted it. The label swallows its own mousemove now, and starts the countdown when the pointer actually leaves. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The label still vanished now and then once the pointer reached it. Swallowing its own mousemove was not enough: the stage's hover tracking is throttled, so a call scheduled while the pointer was still crossing the gap could run after it had arrived, hit-test that stale point, find no sprite, and start hiding the label the pointer was resting on — and with the label swallowing events, nothing was left to cancel the countdown. Whether that trailing call landed before or after the pointer arrived is what made it intermittent. The pointer being on a label is now a fact the hide path checks, rather than something inferred from whichever event happens to arrive last. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Point the `repeat` knowledge point at `repeat.webm` instead of the shared demo take. It is the first WebM in the library; `<video>` picks the decoder from the bucket's `video/webm` content type, so nothing else changes. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The moment a code-judged course's primary goal is reached, the same text is read three times over: the secondary goal is checked against it token by token, it is quoted to the copilot as the evidence for its completion comment, and it stays in the learner's file as the answer they keep. A run's worth of stray indentation went into all three. It is formatted once, before the secondary goal is judged, so the three agree. The edit goes through the editor's history like the Format button's does, so it is one undoable action rather than a silent rewrite. Formatting is a courtesy — an unparsable file or a missing editor must never hold up completion — so a failure is only warned about. And since the format is async, the completion signal now carries the run it belongs to: a learner who hits run again in that window does not get the previous run's verdict. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1e5f30d to
f2613b0
Compare
goplus/builder-backend#329 raised the server-enforced course prompt length limit from 4000 to 12000 (merged 2026-08-12). This repo's copy of the xbcs-package skill still validated and documented the old 4000 limit; goplus/xbuilder-courses already carries the fix. Port it back here so both copies agree with what the deployment enforces. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Rebuilds the tutorial experience around a different premise: a course is a playground the copilot
watches over, not a script it reads out. Based on the prototype in #3330.
Two new documents describe this in full — what this branch adds
(the change surface and the reasoning behind each mechanism) and
the Code: Lita series (what the 28 courses teach).
The core idea
The copilot is silent by default. It perceives everything through real editor events but says
nothing until the user is stuck, has drifted, or asks. How much it may do is not a matter of prompt
etiquette: an intervention level decides which guidance tools are even registered, so a tool
it must not use at this level does not exist for it.
api-videoonly+ guide-modal,spotlight-hint+ in-editor code guidesThe level moves on the model's own per-round verdict (
<user-progress-ahead/>/neutral/back),counted by the system. A typed question temporarily raises it — someone who asks deserves an answer.
Prompt rules alone proved unreliable with the backend model. The order that works is mechanism
first (unregister the tool), per-round reminder second, protocol prose last.
What's new
Waking on runtime output (not just errors) is what makes coding courses completable at all: the
win condition is a log line.
```jsoncblock in the prompt declares hidden panels andwhether the copilot starts open. Author intent, applied once, not a runtime copilot decision.
sprite also reads the signed turn angle, which is exactly the number
turnexpects. Threecourses are built on it.
<stay-silent/>hides the whole round; only typed rounds appear inchat.
/devtools/course-runner, dev-only) — drives the real WASM runtimeprogrammatically, so a course can be verified without clicking through the editor.
Editor extension points
Features drive the editor through owned extension points; the editor never imports tutorial code:
workspace-layout(focused mode, hideable areas, optional tools),leave-confirm,editor-reload,API-reference filtering, and the code-guides gate.
Testing
193 unit tests pass, 13 test files added — intervention levels and verdict counting, guidance
registration, course config parsing, content visibility, ruler math, workspace layout reset, code
guides, and the course-start flow.
Beyond that, all 28 courses of the Code: Lita series had their reference answers executed against the
real runtime via the harness, checking that every radish in each level is collected: 27 of 27
courses with code pass (course 1 has no code). That exercise surfaced three engine constraints now
documented and designed around — the Main-execution timeout on bare top-level code, inline array
literals failing to compile, and
onTouchStartonly firing on the transition into contact.Not included
The ~25MB of demo/story video assets under
spx-gui/public/tutorial-*/are intentionally left out ofgit; the
stepexplainer already points at a hosted URL and the rest should follow. Course contentitself lives in the backend, imported from a course-series file.
🤖 Generated with Claude Code