Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
8a83a74
Add AI-first landing screen for new workflow creation (#4856)
lmac-1 Jun 22, 2026
b87f517
Merge branch '4848-ai-first-starting-ux-parent' into 4856-ai-first-la…
lmac-1 Jun 22, 2026
792ff5c
Improve LandingScreen accessibility
lmac-1 Jun 25, 2026
33c72cf
refactor(#4856): read isNewWorkflow from store in BreadcrumbContent
lmac-1 Jun 25, 2026
7dda795
feat(#4856): update landing screen to match revised design
lmac-1 Jun 25, 2026
602d39d
fix(#4856): update E2E selectors for revised landing screen design
lmac-1 Jun 25, 2026
631d62e
AI First Starting UX: Import YAML path (#4894)
lmac-1 Jun 25, 2026
517571b
refactor(YAMLImportModal): read import state via hooks instead of Sto…
lmac-1 Jun 25, 2026
87be108
test(YAMLImportModal): rewrite tests to follow canonical vi.fn() mock…
lmac-1 Jun 25, 2026
5a0b63b
fix(LandingScreen): lowercase "scratch" in card title
lmac-1 Jun 25, 2026
ec3267f
fix(YAMLImportModal): handle save failures and fix disabled button ho…
lmac-1 Jun 25, 2026
2c91c5e
fix(LandingScreen, YAMLImportModal): focus rings, error position, dis…
lmac-1 Jun 25, 2026
db8b47f
remove side button for old create workflow flow
lmac-1 Jun 25, 2026
7cf3862
test(YAMLImportModal): fix saveWorkflow mock to return truthy value o…
lmac-1 Jun 25, 2026
bcf5304
Merge branch '4848-ai-first-starting-ux-parent' into 4856-ai-first-la…
lmac-1 Jun 25, 2026
8b64e58
feat(LandingScreen): wire Build with AI path — dismiss screen, open A…
lmac-1 Jun 25, 2026
19ea9e9
feat(AIAssistantPanel): lock panel close while isNewWorkflow — hide c…
lmac-1 Jun 25, 2026
8c8bb4f
feat(useAIWorkflowApplications): route validation errors to chat on n…
lmac-1 Jun 25, 2026
2119a34
test(build-with-ai): add coverage for new-workflow AI path behaviours
lmac-1 Jun 25, 2026
839f098
fix(handleApplyWorkflow): skip fit-view and add Retry button when sav…
lmac-1 Jun 25, 2026
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
14 changes: 14 additions & 0 deletions assets/css/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,20 @@
--color-warning-800: var(--color-yellow-800);
--color-warning-900: var(--color-yellow-900);
--color-warning-950: var(--color-yellow-950);

/* NEW DESIGN SYSTEM - TODO-AI-FIRST extend? */
--gray-light-200: #eae9e8;
--gray-light-300: #d6d5d4;
--gray-light-400: #cccbca;

--color-border-strong: var(--gray-light-400);
--color-border-subtle: var(--gray-light-200);

/* Surface tokens */
--color-surface-subtle: #EBF5F2;

/* Semantic tokens */
--color-semantic-success: #006840;
}

@layer base {
Expand Down
57 changes: 46 additions & 11 deletions assets/js/collaborative-editor/CollaborativeEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,36 @@ import { useMemo, useRef } from 'react';

import { useURLState } from '#/react/lib/use-url-state';

import { PickerButton } from '../picker/PickerButton';
import { SocketProvider } from '../react/contexts/SocketProvider';
import type { WithActionProps } from '../react/lib/with-props';

import { AIAssistantPanelWrapper } from './components/AIAssistantPanelWrapper';
import { BreadcrumbLink, BreadcrumbText } from './components/Breadcrumbs';
import { PickerButton } from '../picker/PickerButton';
import type { MonacoHandle } from './components/CollaborativeMonaco';
import { Header } from './components/Header';
import { LandingScreen } from './components/LandingScreen';
import { LoadingBoundary } from './components/LoadingBoundary';
import { Toaster } from './components/ui/Toaster';
import { VersionDebugLogger } from './components/VersionDebugLogger';
import { VersionDropdown } from './components/VersionDropdown';
import { WorkflowEditor } from './components/WorkflowEditor';
import { YAMLImportModal } from './components/YAMLImportModal';
import { CredentialModalProvider } from './contexts/CredentialModalContext';
import { LiveViewActionsProvider } from './contexts/LiveViewActionsContext';
import { MonacoRefProvider } from './contexts/MonacoRefContext';
import { SessionProvider } from './contexts/SessionProvider';
import { StoreProvider } from './contexts/StoreProvider';
import {
useIsNewWorkflow,
useLatestSnapshotLockVersion,
useProject,
} from './hooks/useSessionContext';
import { useIsRunPanelOpen } from './hooks/useUI';
import {
useIsRunPanelOpen,
useShowLandingScreen,
useUICommands,
} from './hooks/useUI';
import { useVersionSelect } from './hooks/useVersionSelect';
import { useWorkflowState } from './hooks/useWorkflow';
import { KeyboardProvider } from './keyboard';
Expand Down Expand Up @@ -67,7 +74,6 @@ interface BreadcrumbContentProps {
projectIsSandboxFallback?: string;
projectColorFallback?: string | null;
projectEnvFallback?: string;
isNewWorkflow?: boolean;
aiAssistantEnabled: boolean;
}

Expand All @@ -80,18 +86,16 @@ function BreadcrumbContent({
projectIsSandboxFallback,
projectColorFallback,
projectEnvFallback,
isNewWorkflow = false,
aiAssistantEnabled,
}: BreadcrumbContentProps) {
const isNewWorkflow = useIsNewWorkflow();
const projectFromStore = useProject();

const workflowFromStore = useWorkflowState(state => state.workflow);
const latestSnapshotLockVersion = useLatestSnapshotLockVersion();

const isRunPanelOpen = useIsRunPanelOpen();

const { params } = useURLState();
const isIDEOpen = params['panel'] === 'editor';
const handleVersionSelect = useVersionSelect();

const projectId = projectFromStore?.id ?? projectIdFallback;
const projectName = projectFromStore?.name ?? projectNameFallback;
Expand All @@ -101,8 +105,6 @@ function BreadcrumbContent({
const isSandbox = projectIsSandboxFallback === 'true';
const currentWorkflowName = workflowFromStore?.name ?? workflowName;

const handleVersionSelect = useVersionSelect();

const breadcrumbElements = useMemo(() => {
return [
// Project name as picker trigger
Expand Down Expand Up @@ -152,12 +154,15 @@ function BreadcrumbContent({
projectColor,
projectEnv,
currentWorkflowName,
workflowId,
workflowFromStore?.lock_version,
latestSnapshotLockVersion,
handleVersionSelect,
isNewWorkflow,
]);

// Hide header until the first save clears isNewWorkflow in the store.
if (isNewWorkflow) return null;

return (
<Header
key="canvas-header"
Expand All @@ -172,6 +177,34 @@ function BreadcrumbContent({
);
}

function LandingScreenWrapper({
aiAssistantEnabled,
}: {
aiAssistantEnabled: boolean;
}) {
const showLandingScreen = useShowLandingScreen();
const { openYAMLImportModal, dismissLandingScreen, openAIAssistantPanel } =
useUICommands();

if (!showLandingScreen) return null;

return (
<>
<LandingScreen
aiAssistantEnabled={aiAssistantEnabled}
onBuildWithAI={(prompt: string) => {
dismissLandingScreen();
openAIAssistantPanel(prompt);
}}
onBuildFromScratch={() => {}}
onBrowseTemplates={() => {}}
onImportYAML={openYAMLImportModal}
/>
<YAMLImportModal />
</>
);
}

export const CollaborativeEditor: WithActionProps<
CollaborativeEditorDataProps
> = props => {
Expand Down Expand Up @@ -222,7 +255,6 @@ export const CollaborativeEditor: WithActionProps<
<BreadcrumbContent
workflowId={workflowId}
workflowName={workflowName}
isNewWorkflow={isNewWorkflow}
aiAssistantEnabled={aiAssistantEnabled}
{...(projectId !== undefined && {
projectIdFallback: projectId,
Expand Down Expand Up @@ -251,6 +283,9 @@ export const CollaborativeEditor: WithActionProps<
</div>
</LoadingBoundary>
</div>
<LandingScreenWrapper
aiAssistantEnabled={aiAssistantEnabled}
/>
</div>
<AIAssistantPanelWrapper
aiAssistantEnabled={aiAssistantEnabled}
Expand Down
46 changes: 25 additions & 21 deletions assets/js/collaborative-editor/components/AIAssistantPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useEffect, useState, useRef } from 'react';

import { cn } from '#/utils/cn';

import { Tooltip } from '../../components/Tooltip';
import {
useAIStorageKey,
useAISessionType,
Expand All @@ -15,11 +16,10 @@ import { useSelectedStepId, useSelectedRunId } from '../hooks/useHistory';
import { ChatInput } from './ChatInput';
import { DisclaimerScreen } from './DisclaimerScreen';
import { SessionList } from './SessionList';
import { Tooltip } from '../../components/Tooltip';

interface AIAssistantPanelProps {
isOpen: boolean;
onClose: () => void;
onClose?: () => void;
onNewConversation?: () => void;
onSessionSelect?: (sessionId: string) => void;
onShowSessions?: () => void;
Expand Down Expand Up @@ -233,7 +233,7 @@ export function AIAssistantPanel({
if (onShowSessions) {
onShowSessions();
}
} else {
} else if (onClose) {
onClose();
}
};
Expand Down Expand Up @@ -390,27 +390,31 @@ export function AIAssistantPanel({
</div>
)}
</div>
<Tooltip
content={sessionId ? 'Close current session' : 'Close assistant'}
>
<button
type="button"
onClick={handleClose}
className={cn(
'inline-flex items-center justify-center',
'h-8 w-8 rounded-md',
'text-gray-400 hover:text-gray-600 hover:bg-gray-100',
'focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500',
'transition-all duration-150',
'flex-shrink-0'
)}
aria-label={
{onClose && (
<Tooltip
content={
sessionId ? 'Close current session' : 'Close assistant'
}
>
<span className="hero-x-mark h-5 w-5" />
</button>
</Tooltip>
<button
type="button"
onClick={handleClose}
className={cn(
'inline-flex items-center justify-center',
'h-8 w-8 rounded-md',
'text-gray-400 hover:text-gray-600 hover:bg-gray-100',
'focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500',
'transition-all duration-150',
'shrink-0'
)}
aria-label={
sessionId ? 'Close current session' : 'Close assistant'
}
>
<span className="hero-x-mark h-5 w-5" />
</button>
</Tooltip>
)}
</div>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ import {
useWorkflowState,
} from '../hooks/useWorkflow';
import { useKeyboardShortcut } from '../keyboard';
import type { JobCodeContext } from '../types/ai-assistant';
import type { JobCodeContext, Message } from '../types/ai-assistant';
import { Z_INDEX } from '../utils/constants';
import {
prepareWorkflowForSerialization,
Expand Down Expand Up @@ -103,6 +103,9 @@ export function AIAssistantPanelWrapper({
const isPinnedVersion =
currentVersion !== undefined && currentVersion !== null;

const { isReadOnly } = useWorkflowReadOnly();
const isNewWorkflow = useIsNewWorkflow();

// Track IDE state changes to re-focus chat input when IDE closes
const isIDEOpen = params.panel === 'editor';
const [focusTrigger, setFocusTrigger] = useState(0);
Expand All @@ -128,7 +131,7 @@ export function AIAssistantPanelWrapper({
toggleAIAssistantPanel();
},
0,
{ enabled: !isPinnedVersion && aiAssistantEnabled }
{ enabled: !isPinnedVersion && aiAssistantEnabled && !isNewWorkflow }
);

const aiStore = useAIStore();
Expand Down Expand Up @@ -157,10 +160,7 @@ export function AIAssistantPanelWrapper({
const workflow = useWorkflowState(state => state.workflow);
const limits = useLimits();

// Check readonly state and new workflow status
// AI can apply changes if: not readonly OR is a new workflow (being created)
const { isReadOnly } = useWorkflowReadOnly();
const isNewWorkflow = useIsNewWorkflow();
const canApplyChanges = !isReadOnly || isNewWorkflow;
const isWriteDisabled = !canApplyChanges;

Expand Down Expand Up @@ -548,6 +548,7 @@ export function AIAssistantPanelWrapper({
startApplyingJobCode,
doneApplyingJobCode,
updateJob,
saveWorkflow,
} = useWorkflowActions();

// Get applying state from workflow store for disabling Apply button across all users
Expand All @@ -559,6 +560,20 @@ export function AIAssistantPanelWrapper({
state => state.applyingJobCodeMessageId
);

const onValidationError = useCallback(
(errorMessage: string) => {
const message: Message = {
id: crypto.randomUUID(),
role: 'assistant',
content: errorMessage,
status: 'error',
inserted_at: new Date().toISOString(),
};
aiStore._addMessage(message);
},
[aiStore]
);

// Hook to handle workflow/job code application logic
const { handleApplyWorkflow, handlePreviewJobCode, handleApplyJobCode } =
useAIWorkflowApplications({
Expand All @@ -573,13 +588,16 @@ export function AIAssistantPanelWrapper({
: null,
currentUserId: user?.id,
aiMode,
isNewWorkflow,
onValidationError,
workflowActions: {
importWorkflow,
startApplyingWorkflow,
doneApplyingWorkflow,
startApplyingJobCode,
doneApplyingJobCode,
updateJob,
saveWorkflow,
},
monacoRef,
jobs,
Expand Down Expand Up @@ -683,7 +701,7 @@ export function AIAssistantPanelWrapper({
<div className="flex-1 overflow-hidden">
<AIAssistantPanel
isOpen={isAIAssistantPanelOpen}
onClose={handleClosePanel}
onClose={isNewWorkflow ? undefined : handleClosePanel}
onNewConversation={handleNewConversation}
onSessionSelect={handleSessionSelect}
onShowSessions={handleShowSessions}
Expand Down
Loading