Conversation
* add separate ai generation entry * show asset library suggestions in AssetGenModal * details * more details * refactor: split AssetGenModal into dedicated SpriteGenModal and BackdropGenModal * fix: add ongoing asset generation repeatedly when collapse * revert removed TODO comment * optimize tip * fix: backdrop gen modal close flow
Agent-Logs-Url: https://github.com/goplus/builder/sessions/382560f4-afcd-4e75-b90a-009f81603c3e Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: nighca <1492263+nighca@users.noreply.github.com>
* Fix case-sensitivity issues around backend unique identifiers * Fix route param names & username update issue
There was a problem hiding this comment.
Code Review
This pull request introduces a comprehensive refactoring of identifier resolution and user authentication state management, establishing a clear distinction between unresolved and canonical identifiers. It also enhances the AI asset generation workflow by integrating library suggestions and refactors components to use a more robust signed-in state query. Feedback was provided to improve the username update process by streamlining the redirection to the sign-in page to avoid a flickering signed-out state.
| const handleUsernameUpdated = useMessageHandle( | ||
| async (newUsername: string) => { | ||
| await router.replace({ | ||
| params: { | ||
| nameInput: newUsername | ||
| }, | ||
| query: route.query, | ||
| hash: route.hash | ||
| }) | ||
| message.success( | ||
| i18n.t({ | ||
| en: 'Username updated successfully. Redirecting to the sign-in page...', | ||
| zh: '用户名更新成功。正在重定向到登录页面...' | ||
| }) | ||
| ) | ||
| await timeout(2000) | ||
| initiateSignIn() | ||
| }, | ||
| { | ||
| en: 'Failed to redirect after username update', | ||
| zh: '用户名更新后重定向失败' | ||
| } | ||
| ).fn |
There was a problem hiding this comment.
The current implementation of handleUsernameUpdated first replaces the route and then, after a delay, initiates the sign-in process. This can create a confusing user experience, as the new user page might briefly render in a signed-out state before the redirect to the login page occurs.
A more seamless approach would be to avoid the intermediate router.replace call. Instead, you can construct the full URL for the new user page and pass it as the redirectUri to initiateSignIn. This ensures the user is redirected to the correct page after successfully signing in, without showing an intermediate page state.
const handleUsernameUpdated = useMessageHandle(
async (newUsername: string) => {
message.success(
i18n.t({
en: 'Username updated successfully. Redirecting to the sign-in page...',
zh: '用户名更新成功。正在重定向到登录页面...'
})
)
await timeout(2000)
const newRoute = router.resolve({
name: route.name!,
params: { nameInput: newUsername },
query: route.query,
hash: route.hash
})
const redirectUri = new URL(newRoute.href, window.location.origin).href
initiateSignIn(redirectUri)
},
{
en: 'Failed to redirect after username update',
zh: '用户名更新后重定向失败'
}
).fn
| } | ||
|
|
||
| return { keyword: _keyword, suggestions, isLoading, selected, toggle } | ||
| } |
There was a problem hiding this comment.
The composable doesn't clean up the in-flight AbortController or cancel the pending debounced call on component unmount. If the component is destroyed while a search request is in-flight, the resolved callback will still mutate suggestions / isLoading on an unmounted component (and hold a reference in the module-level closure).
Consider adding an onUnmounted (or getCurrentScope-based cleanup) to abort and cancel:
import { onScopeDispose } from 'vue'
// at the end of useAssetSuggestions:
onScopeDispose(() => {
doSearch.cancel()
abortCtrl?.abort()
})| if (!signedInState.isSignedIn) return | ||
| if (usedCopilotUsersRef.value.includes(signedInState.user.username)) return | ||
| copilot.open() | ||
| }) |
There was a problem hiding this comment.
onMounted is now async and awaits untilLoaded. If the component unmounts before that promise settles, copilot.open() will still be called. There's no way to cancel untilLoaded here because no abort signal is passed.
Consider guarding with a mounted flag or passing an abort signal tied to onBeforeUnmount:
onMounted(async () => {
const ac = new AbortController()
onBeforeUnmount(() => ac.abort())
try {
const signedInState = await untilLoaded(signedInStateQuery, ac.signal)
if (!signedInState.isSignedIn) return
if (usedCopilotUsersRef.value.includes(signedInState.user.username)) return
copilot.open()
} catch {
// aborted on unmount — ignore
}
})| const signedInState = await untilLoaded(this.signedInStateQuery, signal) | ||
| const ownedBySignedInUser = signedInState.isSignedIn && signedInState.user.username === projectData.owner | ||
| return !ownedBySignedInUser | ||
| } |
There was a problem hiding this comment.
The preferPublished closure defers the ownership check until after the cloud request starts resolving. If the user's sign-in state changes between loadProject being called and the preferPublished callback being invoked (e.g., token expires mid-load, or the user signs in during the async window), the check will use the new state rather than the state at the start of loading. This could flip the preferPublishedContent decision unexpectedly.
Capturing the state once at the top of loadProject and using it synchronously inside the closure would be safer:
async loadProject(ownerInput, projectNameInput, helpers, reporter, signal) {
// Resolve auth state once, upfront, before starting the cloud load
const signedInState = await untilLoaded(this.signedInStateQuery, signal)
signal.throwIfAborted()
const preferPublished = (projectData: ProjectData) => {
const ownedBySignedInUser =
signedInState.isSignedIn && signedInState.user.username === projectData.owner
return !ownedBySignedInUser
}
...
}This also simplifies preferPublished back to a synchronous function, matching the original intent.
| const saving = new Saving(this.project, this.cloudHelpers, this.localCacheHelper, this.isOnline, signal) | ||
| const signal = getCleanupSignal(onCleanup) | ||
| const saving = new Saving(this.project, this.cloudHelpers, this.localCacheHelper, this.isOnline, signal) | ||
|
|
There was a problem hiding this comment.
The watcher now tracks both exportFiles() and this.mode, which means the auto-save logic re-runs whenever the signed-in state changes (since mode reads from signedInStateQuery). This is intentional for reactivity — when the user logs in and becomes owner, auto-save should activate. However, if mode changes without any dirty files, the guard if (mode === EditingMode.EffectFree || !this.dirty) correctly prevents a spurious save, so the behavior is safe. The test added for this exact case (should not trigger auto-save when mode changes without dirty files) confirms it. Just flagging in case reviewers want to leave a short comment here explaining why this.mode is a watcher source rather than a one-time check.
|
Solid set of changes. The shift from |
No description provided.