Skip to content

Commit 202b216

Browse files
committed
sv
1 parent 084ac0b commit 202b216

33 files changed

+329
-147
lines changed

spx-gui/AGENTS.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,30 @@ Keep import statements in order:
3535
- Enum names and enum members
3636
- Vue component names
3737

38+
### Identifier Resolution
39+
40+
When working with backend unique string identifiers such as `username`, project owner, and project name, distinguish unresolved identifiers from canonical identifiers.
41+
42+
* Treat values from route params, query params, and manual user input as unresolved identifiers.
43+
44+
* Treat values from JWT token fields such as `username` as unresolved identifiers unless the current task explicitly establishes a stronger guarantee.
45+
46+
* Treat backend-issued values from HTTP API responses as canonical identifiers.
47+
48+
* Prefer naming unresolved values explicitly, such as `ownerInput`, `projectNameInput`, or similar names that make the unresolved nature obvious.
49+
50+
* In models and resolved state, names like `owner`, `name`, and `username` should refer to canonical values.
51+
52+
* Keep normal strict and case-sensitive equality (`===` / `!==`) for consuming identifiers. Do not spread ad hoc case-normalization logic across comparison sites.
53+
54+
* Resolve unresolved identifiers at clear boundaries before consuming them. Typical resolution boundaries include project loading, user loading, and other backend-backed fetches.
55+
56+
* 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.
57+
58+
* Downstream logic should consume canonical values for behavior-sensitive checks. This includes ownership checks, permission checks, project reuse checks, local-cache decisions, and similar logic.
59+
60+
* Cache keys and similar identity-scoping data may intentionally use unresolved identifiers when that preserves stable session scoping.
61+
3862
## TypeScript Testing
3963

4064
* Use `describe` to group related tests.

spx-gui/src/components/agent-copilot/CopilotProvider.vue

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ export function useAgentCopilotCtx() {
8484
* Handles initialization of Copilot, MCP connections, and UI rendering.
8585
*/
8686
import { reactive, inject, ref, provide, onBeforeUnmount, computed, type Ref, watch } from 'vue'
87-
import { computedShallowReactive } from '@/utils/utils'
87+
import { computedShallowReactive, untilNotNull } from '@/utils/utils'
8888
import { useI18n } from '@/utils/i18n'
8989
import { Copilot } from './copilot'
9090
import { CopilotController, type ICopilot } from './index'
@@ -95,7 +95,7 @@ import { ToolRegistry } from './mcp/registry'
9595
import { Collector } from './mcp/collector'
9696
import CopilotUI from './CopilotUI.vue'
9797
import { z } from 'zod'
98-
import { getSignedInUsername } from '@/stores/user'
98+
import { isSignedIn, useSignedInUser } from '@/stores/user'
9999
import { createProjectToolDescription, CreateProjectArgsSchema } from './mcp/definitions'
100100
import { getProject, Visibility } from '@/apis/project'
101101
import { useRouter } from 'vue-router'
@@ -114,6 +114,7 @@ const visible = ref(false)
114114
const mcpDebuggerVisible = ref(false)
115115
const showEnvPanel = ref(false)
116116
const router = useRouter()
117+
const signedInUser = useSignedInUser()
117118
118119
const toggleEnvPanel = () => {
119120
showEnvPanel.value = !showEnvPanel.value
@@ -180,17 +181,18 @@ async function createProject(options: CreateProjectOptions) {
180181
const projectName = options.projectName
181182
182183
// Check if user is signed in
183-
const signedInUsername = getSignedInUsername()
184-
if (signedInUsername == null) {
184+
if (!isSignedIn()) {
185185
return {
186186
success: false,
187187
message: 'Please sign in to create a project'
188188
}
189189
}
190190
191+
const { username } = await untilNotNull(signedInUser)
192+
191193
try {
192194
// Check if project already exists
193-
const project = await getProject(signedInUsername, options.projectName)
195+
const project = await getProject(username, options.projectName)
194196
if (project != null) {
195197
return {
196198
success: false,
@@ -201,7 +203,7 @@ async function createProject(options: CreateProjectOptions) {
201203
// Handle error checking project existence
202204
}
203205
204-
const project = new SpxProject(signedInUsername, projectName)
206+
const project = new SpxProject(username, projectName)
205207
project.setVisibility(Visibility.Private)
206208
207209
try {

spx-gui/src/components/agent-copilot/UserMessage.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ const props = defineProps<{
77
content: string
88
}>()
99
10-
const { data: signedInUser } = useSignedInUser()
10+
const signedInUser = useSignedInUser()
1111
const avatarUrl = useAvatarUrl(() => signedInUser.value?.avatar)
1212
</script>
1313

spx-gui/src/components/asset/gen/sprite/SpriteGen.vue

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { useI18n } from '@/utils/i18n'
55
import { useNetwork } from '@/utils/network'
66
import type { SpriteGen } from '@/models/spx/gen/sprite-gen'
77
import type { Sprite } from '@/models/spx/sprite'
8-
import { getSignedInUsername } from '@/stores/user'
8+
import { useSignedInUser } from '@/stores/user'
99
import { cloudHelpers } from '@/models/common/cloud'
1010
import { provideLocalEditorCtx } from '@/components/editor/EditorContextProvider.vue'
1111
import { EditorState } from '@/components/editor/editor-state'
@@ -24,7 +24,7 @@ const emit = defineEmits<{
2424
}>()
2525
2626
const i18n = useI18n()
27-
const signedInUsername = computed(() => getSignedInUsername())
27+
const signedInUser = useSignedInUser()
2828
const { isOnline } = useNetwork()
2929
// Local cache is not really used in sprite gen, so a dummy implementation is sufficient.
3030
const localCache: ILocalCache = {
@@ -34,7 +34,7 @@ const localCache: ILocalCache = {
3434
}
3535
// We should override the state to avoid history operations caused by animation / costume changes within sprite gen.
3636
const editorStateInGen = useComputedDisposable(
37-
() => new EditorState(i18n, props.gen.previewProject, isOnline, signedInUsername.value, cloudHelpers, localCache)
37+
() => new EditorState(i18n, props.gen.previewProject, isOnline, signedInUser, cloudHelpers, localCache)
3838
)
3939
const editorCtxInGen = computedShallowReactive(() => ({
4040
project: props.gen.previewProject,

spx-gui/src/components/asset/library/AssetSaveModal.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ const emit = defineEmits<{
3939
resolved: []
4040
}>()
4141
42-
const { data: signedInUser } = useSignedInUser()
42+
const signedInUser = useSignedInUser()
4343
4444
const user = computed(() => {
4545
const u = signedInUser.value

spx-gui/src/components/common/ListResultWrapper.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { UILoading, UIError, UIEmpty } from '@/components/ui'
1313
import type { ByPage } from '@/apis/common'
1414
1515
const props = defineProps<{
16-
queryRet: QueryRet<T>
16+
queryRet: QueryRet<T | null>
1717
height?: number
1818
/** Type of the list content */
1919
contentType?: 'project'

spx-gui/src/components/community/ProjectsSection.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ type Context = 'home' | 'user' | 'project'
4646
4747
defineProps<{
4848
linkTo?: string | null
49-
queryRet: QueryRet<unknown[]>
49+
queryRet: QueryRet<unknown[] | null>
5050
context: Context
5151
numInRow: number
5252
}>()

spx-gui/src/components/community/project/ReleaseHistory.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { UITimeline, UITimelineItem, UILoading, UIError } from '@/components/ui'
66
import TextView from '../TextView.vue'
77
88
defineProps<{
9-
queryRet: QueryRet<ProjectRelease[]>
9+
queryRet: QueryRet<ProjectRelease[] | null>
1010
}>()
1111
</script>
1212

spx-gui/src/components/community/user/FollowButton.vue

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<script setup lang="ts">
22
import { computed } from 'vue'
3-
import { getSignedInUsername } from '@/stores/user'
3+
import { useSignedInUser } from '@/stores/user'
44
import { UIButton } from '@/components/ui'
55
import { useMessageHandle } from '@/utils/exception'
66
import { useEnsureSignedIn } from '@/utils/user'
@@ -11,8 +11,9 @@ const props = defineProps<{
1111
name: string
1212
}>()
1313
14+
const signedInUser = useSignedInUser()
1415
const followable = computed(() => {
15-
const signedInUsername = getSignedInUsername()
16+
const signedInUsername = signedInUser.value?.username ?? null
1617
return signedInUsername != null && props.name !== signedInUsername
1718
})
1819

spx-gui/src/components/community/user/UserHeader.vue

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { useRoute, useRouter } from 'vue-router'
44
import { type User } from '@/apis/user'
55
import { useAvatarUrl } from '@/stores/user/avatar'
66
import { useMessageHandle } from '@/utils/exception'
7-
import { getSignedInUsername } from '@/stores/user'
7+
import { useSignedInUser } from '@/stores/user'
88
import { UIButton, UIImg, useModal } from '@/components/ui'
99
import CommunityCard from '@/components/community/CommunityCard.vue'
1010
import TextView from '../TextView.vue'
@@ -21,7 +21,8 @@ const props = defineProps<{
2121
2222
const router = useRouter()
2323
const route = useRoute()
24-
const isSignedInUser = computed(() => props.user.username === getSignedInUsername())
24+
const signedInUser = useSignedInUser()
25+
const isSignedInUser = computed(() => props.user.username === signedInUser.value?.username)
2526
const avatarUrl = useAvatarUrl(() => props.user.avatar)
2627
const coverImgUrl = computed(() => getCoverImgUrl(props.user.username))
2728

0 commit comments

Comments
 (0)