refactor(workflow-generator): scope /create to new-app creation only

The /create command was sniffing window.location.pathname for /app/<id>/
workflow and injecting that into the generator modal as currentAppId, so
"Apply to current draft" would appear when the user fired cmd+k from
inside Studio. The problem: the URL doesn't actually tell us the app's
mode (Workflow vs Advanced-Chat both live under /workflow), so we were
passing the user-picked mode as the "current" mode — which produced a
dead-end if the user picked the wrong one from the submenu.

Refining the current draft is now handled by a dedicated Studio toolbar
button (next commit). /create stays simple: pick workflow / chatflow,
modal opens with no current-app context, the only Apply action is
"Create new app". One responsibility per entry point, zero mode-mismatch
state possible.

Tests updated: the four register-handler cases now assert
openGenerator is called with just { mode } regardless of pathname; the
"nested workflow sub-paths" test is removed (that behaviour is removed
by design).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
crazywoola
2026-05-29 21:49:54 +08:00
parent d2a429acc1
commit f671a741da
2 changed files with 24 additions and 70 deletions

View File

@ -67,27 +67,20 @@ describe('/create slash command', () => {
createCommand.unregister?.()
})
// Default path: when the user is not in a workflow Studio page, the
// handler must open the modal without a currentAppId so the modal only
// offers "Create new app".
it('should open the generator with no current-app context outside Studio', async () => {
Object.defineProperty(window, 'location', {
writable: true,
value: { pathname: '/apps' },
})
// /create is scoped to new-app creation — it MUST always open the modal
// with just the requested mode, never with currentAppId. Refining the
// current Studio draft is handled by the Studio toolbar button instead.
it('should open the generator with only the requested mode (no current-app context)', async () => {
await executeCommand('create.open', { mode: 'workflow' })
expect(mockOpenGenerator).toHaveBeenCalledWith({
mode: 'workflow',
currentAppId: null,
currentAppMode: null,
})
expect(mockOpenGenerator).toHaveBeenCalledWith({ mode: 'workflow' })
})
// Studio path: the handler must read the app id straight out of the URL
// and pass it through so the modal can show "Apply to current draft".
it('should capture the current app id when invoked from a workflow Studio URL', async () => {
// Critical guarantee: even when invoked from a workflow Studio URL, the
// handler must NOT sniff the URL to inject currentAppId — that branch
// produced a mode-mismatch dead-end when the user picked the "wrong"
// mode from the submenu while inside Studio.
it('should NOT capture currentAppId even when invoked from a Studio URL', async () => {
Object.defineProperty(window, 'location', {
writable: true,
value: { pathname: '/app/abc-123/workflow' },
@ -95,46 +88,16 @@ describe('/create slash command', () => {
await executeCommand('create.open', { mode: 'advanced-chat' })
expect(mockOpenGenerator).toHaveBeenCalledWith({
mode: 'advanced-chat',
currentAppId: 'abc-123',
currentAppMode: 'advanced-chat',
})
expect(mockOpenGenerator).toHaveBeenCalledWith({ mode: 'advanced-chat' })
})
// Defensive fallback: if a caller forgets to pass a mode (or passes none),
// the handler must still open the generator with a safe default rather
// than crashing the goto-anything dialog.
it('should default to workflow mode when no args are passed', async () => {
Object.defineProperty(window, 'location', {
writable: true,
value: { pathname: '/apps' },
})
await executeCommand('create.open')
expect(mockOpenGenerator).toHaveBeenCalledWith({
mode: 'workflow',
currentAppId: null,
currentAppMode: null,
})
})
// Studio sub-routes (/app/<id>/workflow/edit, etc.) still need to be
// treated as Studio context — the regex must anchor only at the start.
it('should still match Studio context for nested workflow sub-paths', async () => {
Object.defineProperty(window, 'location', {
writable: true,
value: { pathname: '/app/xyz/workflow/run-history' },
})
await executeCommand('create.open', { mode: 'workflow' })
expect(mockOpenGenerator).toHaveBeenCalledWith({
mode: 'workflow',
currentAppId: 'xyz',
currentAppMode: 'workflow',
})
expect(mockOpenGenerator).toHaveBeenCalledWith({ mode: 'workflow' })
})
})

View File

@ -31,8 +31,16 @@ const OPTIONS: CreateOption[] = [
]
/**
* `/create` command — generate a new Workflow or Chatflow app from a natural-
* language description. See ``components/workflow/workflow-generator/``.
* `/create` command — generate a brand-new Workflow or Chatflow app from a
* natural-language description.
*
* This command is scoped to NEW-app creation only. Refining the current
* Studio draft is handled by the toolbar button in
* ``components/workflow-app/components/workflow-header/generate-trigger.tsx``,
* which opens the same modal with the app's real mode locked + currentAppId
* set. Keeping the two journeys separate avoids the mode-mismatch dead-end
* the URL-sniffing approach used to produce when /create was triggered from
* a Workflow Studio page with the "wrong" mode picked.
*/
export const createCommand: SlashCommandHandler = {
name: 'create',
@ -63,25 +71,8 @@ export const createCommand: SlashCommandHandler = {
registerCommands({
'create.open': async (args) => {
const mode: WorkflowGeneratorMode = (args?.mode ?? 'workflow') as WorkflowGeneratorMode
// Detect Studio context: /app/<appId>/workflow.
let currentAppId: string | null = null
let currentAppMode: WorkflowGeneratorMode | null = null
if (typeof window !== 'undefined') {
const match = window.location.pathname.match(/^\/app\/([^/]+)\/workflow/)
if (match) {
currentAppId = match[1] ?? null
// The /workflow path covers both Workflow and Advanced-Chat apps,
// so we don't know the mode for sure from the URL alone. We pass
// the requested mode as the "current" mode — the modal compares
// them and only enables "Apply to current draft" when they match.
currentAppMode = mode
}
}
useWorkflowGeneratorStore.getState().openGenerator({
mode,
currentAppId,
currentAppMode,
})
// No currentAppId / currentAppMode — /create is new-app only.
useWorkflowGeneratorStore.getState().openGenerator({ mode })
},
})
},