diff --git a/spx-gui/AGENTS.md b/spx-gui/AGENTS.md index 3f28bd99e5..11ad9facd4 100644 --- a/spx-gui/AGENTS.md +++ b/spx-gui/AGENTS.md @@ -35,6 +35,28 @@ Keep import statements in order: - Enum names and enum members - Vue component names +### Identifier Resolution + +When working with backend unique string identifiers such as `username`, project owner, and project name, distinguish unresolved identifiers from canonical identifiers. + +* Route params, query params, manual user input, and JWT-derived identifiers are unresolved unless the current task establishes a stronger guarantee. + +* Backend-issued values from HTTP API responses are canonical. + +* Prefer explicit unresolved names with an `Input` suffix. + +* In models and resolved state, names like `owner`, `name`, and `username` should refer to canonical values. + +* Keep normal strict and case-sensitive equality (`===` / `!==`) for consuming identifiers. Do not spread ad hoc case-normalization logic across comparison sites. + +* Resolve unresolved identifiers at clear boundaries before consuming them. Typical resolution boundaries include project loading, user loading, and other backend-backed fetches. + +* Avoid storing unresolved identifiers on long-lived models as if they were already canonical. Prefer passing unresolved identifiers as load or resolve parameters, then writing canonical values onto the model after resolution. + +* Downstream logic should consume canonical values for behavior-sensitive checks such as ownership checks, permission checks, project reuse checks, and local-cache decisions. + +* Cache keys and similar identity-scoping data may intentionally use unresolved identifiers when that preserves stable session scoping. + ## TypeScript Testing * Use `describe` to group related tests. diff --git a/spx-gui/src/apis/asset.ts b/spx-gui/src/apis/asset.ts index 3d20e2d25c..c5fa240e15 100644 --- a/spx-gui/src/apis/asset.ts +++ b/spx-gui/src/apis/asset.ts @@ -93,8 +93,8 @@ export type ListAssetParams = PaginationParams & { sortOrder?: 'asc' | 'desc' } -export function listAsset(params?: ListAssetParams) { - return client.get('/assets/list', params) as Promise> +export function listAsset(params?: ListAssetParams, signal?: AbortSignal) { + return client.get('/assets/list', params, { signal }) as Promise> } export function getAsset(id: string) { diff --git a/spx-gui/src/components/agent-copilot/CopilotProvider.vue b/spx-gui/src/components/agent-copilot/CopilotProvider.vue index 13d7434f51..00b083d749 100644 --- a/spx-gui/src/components/agent-copilot/CopilotProvider.vue +++ b/spx-gui/src/components/agent-copilot/CopilotProvider.vue @@ -95,7 +95,8 @@ import { ToolRegistry } from './mcp/registry' import { Collector } from './mcp/collector' import CopilotUI from './CopilotUI.vue' import { z } from 'zod' -import { getSignedInUsername } from '@/stores/user' +import { untilLoaded } from '@/utils/query' +import { useSignedInStateQuery } from '@/stores/user' import { createProjectToolDescription, CreateProjectArgsSchema } from './mcp/definitions' import { getProject, Visibility } from '@/apis/project' import { useRouter } from 'vue-router' @@ -114,6 +115,7 @@ const visible = ref(false) const mcpDebuggerVisible = ref(false) const showEnvPanel = ref(false) const router = useRouter() +const signedInStateQuery = useSignedInStateQuery() const toggleEnvPanel = () => { showEnvPanel.value = !showEnvPanel.value @@ -179,18 +181,19 @@ const initBasicTools = async () => { async function createProject(options: CreateProjectOptions) { const projectName = options.projectName - // Check if user is signed in - const signedInUsername = getSignedInUsername() - if (signedInUsername == null) { + const signedInState = await untilLoaded(signedInStateQuery) + if (!signedInState.isSignedIn) { return { success: false, message: 'Please sign in to create a project' } } + const { username } = signedInState.user + try { // Check if project already exists - const project = await getProject(signedInUsername, options.projectName) + const project = await getProject(username, options.projectName) if (project != null) { return { success: false, @@ -201,7 +204,7 @@ async function createProject(options: CreateProjectOptions) { // Handle error checking project existence } - const project = new SpxProject(signedInUsername, projectName) + const project = new SpxProject(username, projectName) project.setVisibility(Visibility.Private) try { diff --git a/spx-gui/src/components/agent-copilot/UserMessage.vue b/spx-gui/src/components/agent-copilot/UserMessage.vue index 73b1a286cf..4526176190 100644 --- a/spx-gui/src/components/agent-copilot/UserMessage.vue +++ b/spx-gui/src/components/agent-copilot/UserMessage.vue @@ -7,7 +7,7 @@ const props = defineProps<{ content: string }>() -const { data: signedInUser } = useSignedInUser() +const signedInUser = useSignedInUser() const avatarUrl = useAvatarUrl(() => signedInUser.value?.avatar) diff --git a/spx-gui/src/components/asset/gen/backdrop/BackdropGen.vue b/spx-gui/src/components/asset/gen/backdrop/BackdropGen.vue index e392130ae1..545973c221 100644 --- a/spx-gui/src/components/asset/gen/backdrop/BackdropGen.vue +++ b/spx-gui/src/components/asset/gen/backdrop/BackdropGen.vue @@ -1,7 +1,9 @@