Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions spx-gui/src/components/editor/copilot/ApiReferenceFilter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { z } from 'zod'
import { defineComponent, 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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

不同的 API definition 可能有相同的 name(不过 ID 是各自唯一的),比如 say wordsay word, seconds 的 name 都是 say,通过 name 控制的话这俩要么都留要么都不留,是你预期的吗?

}

export default defineComponent<Props>(
(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())
})
}

// 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
}
},
{
name: 'ApiReferenceFilter',
props: {
names: {
type: String,
default: ''
}
}
}
)
19 changes: 19 additions & 0 deletions spx-gui/src/components/editor/copilot/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这边是把 apiReferenceFilter 的控制放在 copilot 这里,而不是 tutorial 中,考虑的是?

我感觉放 tutorial 中可能好点,主要是暂时没想到在 tutorial 外 copilot 需要控制 apiReferenceFilter 的合理用例

watch(
() => copilot.currentSession,
() => codeEditor.setAPIReferenceFilter(null)
)
)

watch(
() => editorCtx.state.runtime,
(editorRuntime, _, onCleanup) => {
Expand Down
3 changes: 3 additions & 0 deletions spx-gui/src/components/tutorials/tutorial.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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:

Expand Down
7 changes: 7 additions & 0 deletions spx-gui/src/components/xgo-code-editor/api-reference.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
11 changes: 10 additions & 1 deletion spx-gui/src/components/xgo-code-editor/code-editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<APIReferenceFilter | null>(null)
this.completionProviderRef = shallowRef(
new CompletionProvider(params.lspClient, documentBase, params.project.classFramework)
)
Expand Down Expand Up @@ -135,6 +136,14 @@ export class CodeEditor extends Disposable {
this.apiReferenceProviderRef.value = provider
}

private apiReferenceFilterRef: ShallowRef<APIReferenceFilter | null>
get apiReferenceFilter() {
return this.apiReferenceFilterRef.value
}
setAPIReferenceFilter(filter: APIReferenceFilter | null) {
this.apiReferenceFilterRef.value = filter
}

private completionProviderRef: ShallowRef<ICompletionProvider>
get completionProvider() {
return this.completionProviderRef.value
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ const props = defineProps<{

const itemsForDisplay = computed<DefinitionDocumentationItem[] | null>((oldValue) => {
// Ignore intermediate empty data to keep UI stable
return props.controller.items ?? oldValue ?? null
return props.controller.filteredItems ?? oldValue ?? null
})
Comment thread
Ethanlita marked this conversation as resolved.

const loaded = ref(false)
Expand Down
15 changes: 15 additions & 0 deletions spx-gui/src/components/xgo-code-editor/ui/api-reference/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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() {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

建议 filter 后的数据叫 items(未 filter 的数据可以叫别的名字,另外定义为 private field),支持 filter 是 APIReferenceController 的内部逻辑,不需要让消费方(如 APIReferenceUI.vue)感知到

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
}
Expand Down