From 92186ef30e25c4c86c93744dda76d97c27343c15 Mon Sep 17 00:00:00 2001 From: Ethanlita Date: Tue, 23 Jun 2026 16:52:04 +0800 Subject: [PATCH 1/2] feat(editor): let copilot narrow the API references panel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a generic, reactive API reference filter on CodeEditor that the copilot can drive via a new `api-reference-filter` custom element (matching APIs by name). The filter is a synchronous derived view over the already-loaded items, so it updates reactively without re-running the async provider, and is cleared automatically when the copilot session ends or switches (e.g. a tutorial course finishes) — keeping the generic editor unaware of tutorials. Tutorial courses instruct the copilot to narrow the panel to the union of APIs the whole course uses, so learners are not distracted by unrelated APIs. Co-Authored-By: Claude Opus 4.8 --- .../editor/copilot/ApiReferenceFilter.ts | 73 +++++++++++++++++++ .../src/components/editor/copilot/index.ts | 19 +++++ spx-gui/src/components/tutorials/tutorial.ts | 3 + .../xgo-code-editor/api-reference.ts | 7 ++ .../components/xgo-code-editor/code-editor.ts | 11 ++- .../ui/api-reference/APIReferenceUI.vue | 2 +- .../xgo-code-editor/ui/api-reference/index.ts | 15 ++++ 7 files changed, 128 insertions(+), 2 deletions(-) create mode 100644 spx-gui/src/components/editor/copilot/ApiReferenceFilter.ts diff --git a/spx-gui/src/components/editor/copilot/ApiReferenceFilter.ts b/spx-gui/src/components/editor/copilot/ApiReferenceFilter.ts new file mode 100644 index 000000000..ad180cef4 --- /dev/null +++ b/spx-gui/src/components/editor/copilot/ApiReferenceFilter.ts @@ -0,0 +1,73 @@ +import { z } from 'zod' +import { defineComponent, onMounted, watch } from 'vue' +import { useCodeEditorRef, parseDefinitionName } from '@/components/xgo-code-editor' + +export const tagName = 'api-reference-filter' + +export const isRaw = false + +export const description = 'Narrow the "API References" panel to only the listed APIs.' + +export const detailedDescription = `Narrow the "API References" panel (left of the code editor) to only the listed \ +APIs, to focus the user during a guided step. \`names\` is a comma-separated list of API names, as named in the spx \ +API reference (e.g. \`onStart\`, \`say\`, \`move\`). Use \`names=""\` to show all APIs again. Re-emit this element with \ +a new list whenever the relevant APIs change or you previously made a mistake — the latest one wins. For example, \ +<${tagName} names="onStart, say" /> keeps only "onStart" and "say" visible.` + +export const attributes = z.object({ + names: z + .string() + .describe('Comma-separated API names to keep visible, e.g., `onStart, say, move`. Empty string shows all.') +}) + +export type Props = { + /** Comma-separated API names to keep visible. Empty string shows all. */ + names: string +} + +export default defineComponent( + (props) => { + const codeEditorRef = useCodeEditorRef() + + function apply() { + const codeEditor = codeEditorRef.value + if (codeEditor == null) return + const names = props.names + .split(',') + .map((name) => name.trim().toLowerCase()) + .filter((name) => name !== '') + if (names.length === 0) { + codeEditor.setAPIReferenceFilter(null) + return + } + const allow = new Set(names) + codeEditor.setAPIReferenceFilter((item) => { + const fullName = item.definition.name + if (fullName == null) return false + // Match either the receiver-qualified name (e.g. `Sprite.say`) or the bare method name (`say`), + // so the model can use the plain names it knows from the spx API reference skill. + const [, method] = parseDefinitionName(fullName) + return allow.has(fullName.toLowerCase()) || allow.has(method.toLowerCase()) + }) + } + + onMounted(apply) + // The editor may become available later than this element; apply again once it is ready. + watch(codeEditorRef, (codeEditor) => { + if (codeEditor != null) apply() + }) + + return function render() { + return null + } + }, + { + name: 'ApiReferenceFilter', + props: { + names: { + type: String, + default: '' + } + } + } +) diff --git a/spx-gui/src/components/editor/copilot/index.ts b/spx-gui/src/components/editor/copilot/index.ts index 3960c2b43..cae294504 100644 --- a/spx-gui/src/components/editor/copilot/index.ts +++ b/spx-gui/src/components/editor/copilot/index.ts @@ -20,6 +20,7 @@ import { } from '../spx-code-editor' import * as codeLink from './CodeLink' import * as codeChange from './CodeChange.vue' +import * as apiReferenceFilter from './ApiReferenceFilter' import CodeBlock from './CodeBlock.vue' class Retriever { @@ -286,6 +287,15 @@ export function useSpxEditorCopilot(): void { d.addDisposer(copilot.registerTool(new GetSpriteContentTool(retriever))) d.addDisposer(copilot.registerTool(new GetProjectCodeTool(retriever))) d.addDisposer(copilot.registerTool(new GetCodeDiagnosticsTool(codeEditor))) + d.addDisposer( + copilot.registerCustomElement({ + tagName: apiReferenceFilter.tagName, + description: apiReferenceFilter.detailedDescription, + attributes: apiReferenceFilter.attributes, + isRaw: apiReferenceFilter.isRaw, + component: apiReferenceFilter.default + }) + ) d.addDisposer( copilot.registerCustomElement({ tagName: codeLink.tagName, @@ -316,6 +326,15 @@ export function useSpxEditorCopilot(): void { }) ) + // The API reference filter is transient state scoped to a copilot session: clear it whenever the + // session ends or switches (e.g. a tutorial course finishes), so a filter never leaks past its context. + d.addDisposer( + watch( + () => copilot.currentSession, + () => codeEditor.setAPIReferenceFilter(null) + ) + ) + watch( () => editorCtx.state.runtime, (editorRuntime, _, onCleanup) => { diff --git a/spx-gui/src/components/tutorials/tutorial.ts b/spx-gui/src/components/tutorials/tutorial.ts index 6b7cc129f..3edc858d1 100644 --- a/spx-gui/src/components/tutorials/tutorial.ts +++ b/spx-gui/src/components/tutorials/tutorial.ts @@ -130,6 +130,8 @@ First do some preparation: * Clearly define the course completion criteria. +* If the course involves writing spx code, proactively narrow the "API References" panel (left of the code editor) at the start, before guiding the first coding step. Keep ALL the APIs the course uses anywhere — the union across every step, decided from the course goal and the reference project's code (the standard answer) — not just the current step's APIs, so the user can always find every API they will need throughout the course. Set this once and keep it stable for the whole course; only change it if the course genuinely needs a different set. This is expected for every coding course — do not wait for the user to ask. + Then guide the user through each step. For each step: 1. If extra information required, use appropriate tool to gather it. @@ -156,6 +158,7 @@ When coding tasks are involved: * Before offering coding suggestions, ensure you understand the current code. If not, use appropriate tools to review it first. * Avoid giving complete solution code directly. Instead, guide the user step-by-step with hints and explanations. * Prefer to insert code by dragging corresponding items (if available) from "API References" into the code editor over providing manual code snippets. +* Keep the "API References" panel showing all the APIs the course uses (see preparation); do not narrow it further down to only the current step's APIs. When tool result received: diff --git a/spx-gui/src/components/xgo-code-editor/api-reference.ts b/spx-gui/src/components/xgo-code-editor/api-reference.ts index 0c3c40405..20c93e056 100644 --- a/spx-gui/src/components/xgo-code-editor/api-reference.ts +++ b/spx-gui/src/components/xgo-code-editor/api-reference.ts @@ -7,6 +7,13 @@ import type { BaseContext, DefinitionDocumentationItem } from './common' export type APIReferenceItem = DefinitionDocumentationItem +/** + * Predicate to narrow which API reference items are shown in the panel. + * `null` (no filter) means show all items. The editor stays agnostic about how + * the predicate is built; consumers decide the rule. + */ +export type APIReferenceFilter = (item: APIReferenceItem) => boolean + export type APIReferenceContext = BaseContext export type APICategoryViewInfo = { diff --git a/spx-gui/src/components/xgo-code-editor/code-editor.ts b/spx-gui/src/components/xgo-code-editor/code-editor.ts index 0b3962f44..c8e499c6e 100644 --- a/spx-gui/src/components/xgo-code-editor/code-editor.ts +++ b/spx-gui/src/components/xgo-code-editor/code-editor.ts @@ -5,7 +5,7 @@ import type { History } from '@/components/editor/history' import type { IXGoProject } from './project' import { type IDocumentBase, DocumentBase } from './document-base' import type { ILSPClient } from './lsp/types' -import { EmptyAPIReferenceProvider } from './api-reference' +import { EmptyAPIReferenceProvider, type APIReferenceFilter } from './api-reference' import { type ICodeEditorUIController, type IDiagnosticsProvider, @@ -80,6 +80,7 @@ export class CodeEditor extends Disposable { this.inlayHintProviderRef = shallowRef(new InlayHintProvider(params.lspClient)) this.inputHelperProviderRef = shallowRef(new InputHelperProvider(params.lspClient, () => this.resourceAdapter)) this.apiReferenceProviderRef = shallowRef(new EmptyAPIReferenceProvider()) + this.apiReferenceFilterRef = shallowRef(null) this.completionProviderRef = shallowRef( new CompletionProvider(params.lspClient, documentBase, params.project.classFramework) ) @@ -135,6 +136,14 @@ export class CodeEditor extends Disposable { this.apiReferenceProviderRef.value = provider } + private apiReferenceFilterRef: ShallowRef + get apiReferenceFilter() { + return this.apiReferenceFilterRef.value + } + setAPIReferenceFilter(filter: APIReferenceFilter | null) { + this.apiReferenceFilterRef.value = filter + } + private completionProviderRef: ShallowRef get completionProvider() { return this.completionProviderRef.value diff --git a/spx-gui/src/components/xgo-code-editor/ui/api-reference/APIReferenceUI.vue b/spx-gui/src/components/xgo-code-editor/ui/api-reference/APIReferenceUI.vue index e3f0d2e25..a7a608eab 100644 --- a/spx-gui/src/components/xgo-code-editor/ui/api-reference/APIReferenceUI.vue +++ b/spx-gui/src/components/xgo-code-editor/ui/api-reference/APIReferenceUI.vue @@ -36,7 +36,7 @@ const props = defineProps<{ const itemsForDisplay = computed((oldValue) => { // Ignore intermediate empty data to keep UI stable - return props.controller.items ?? oldValue ?? null + return props.controller.filteredItems ?? oldValue ?? null }) const loaded = ref(false) diff --git a/spx-gui/src/components/xgo-code-editor/ui/api-reference/index.ts b/spx-gui/src/components/xgo-code-editor/ui/api-reference/index.ts index 6d891c8c7..9b580dfd7 100644 --- a/spx-gui/src/components/xgo-code-editor/ui/api-reference/index.ts +++ b/spx-gui/src/components/xgo-code-editor/ui/api-reference/index.ts @@ -27,6 +27,21 @@ export class APIReferenceController extends Disposable { return this.itemsMgr.result.data } + /** + * Items after applying `codeEditor.apiReferenceFilter`. This is a synchronous derivation + * over the already-loaded `items`, so filter changes update the UI reactively without + * re-running the async provider. Falls back to the full list when the filter matches + * nothing, to avoid leaving the panel empty. + */ + get filteredItems() { + const items = this.items + if (items == null) return null + const filter = this.ui.codeEditor.apiReferenceFilter + if (filter == null) return items + const filtered = items.filter(filter) + return filtered.length > 0 ? filtered : items + } + get error() { return this.itemsMgr.result.error } From 0312546780ac812bd59dc2fe738b0bc1672ae797 Mon Sep 17 00:00:00 2001 From: Ethanlita Date: Tue, 23 Jun 2026 17:20:32 +0800 Subject: [PATCH 2/2] fix(editor): re-apply API reference filter when names prop changes Replace the on-mount + codeEditorRef watch with a single immediate watch on both the editor ref and the `names` prop, so the filter updates when the copilot revises the element in place, not only on mount. Co-Authored-By: Claude Opus 4.8 --- .../components/editor/copilot/ApiReferenceFilter.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/spx-gui/src/components/editor/copilot/ApiReferenceFilter.ts b/spx-gui/src/components/editor/copilot/ApiReferenceFilter.ts index ad180cef4..7211af5e9 100644 --- a/spx-gui/src/components/editor/copilot/ApiReferenceFilter.ts +++ b/spx-gui/src/components/editor/copilot/ApiReferenceFilter.ts @@ -1,5 +1,5 @@ import { z } from 'zod' -import { defineComponent, onMounted, watch } from 'vue' +import { defineComponent, watch } from 'vue' import { useCodeEditorRef, parseDefinitionName } from '@/components/xgo-code-editor' export const tagName = 'api-reference-filter' @@ -51,11 +51,9 @@ export default defineComponent( }) } - onMounted(apply) - // The editor may become available later than this element; apply again once it is ready. - watch(codeEditorRef, (codeEditor) => { - if (codeEditor != null) apply() - }) + // Re-apply on initial render, when the editor becomes available (it may mount later than this + // element), and when the copilot updates the element with a new `names` prop. + watch([codeEditorRef, () => props.names], apply, { immediate: true }) return function render() { return null