Migrate to XState v5 (#3735)
* migrate settingsMachine
* Guard events with properties instead
* migrate settingsMachine
* Migrate auth machine
* Migrate file machine
* Migrate depracated types
* Migrate home machine
* Migrate command bar machine
* Version fixes
* Migrate command bar machine
* Migrate modeling machine
* Migrate types, state.can, state.matches and state.nextEvents
* Fix syntax
* Pass in modelingState into editor manager instead of modeling event
* Fix issue with missing command bar provider
* Fix state transition
* Fix type issue in Home
* Make sure no guards rely on event type
* Fix up command bar submission logic
* Home machine tweaks to get things running
* Fix AST fillet function args
* Handle "Set selection" when it is called by actor onDone
* Remove unused imports
* A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest)
* Fix injectin project to the fileTree machine
* Revert "A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest)"
This reverts commit 4b43ff69d1.
* A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest)
* Re-run CI
* Restore success toasts on file/folder deletion
* Replace casting with guarding against event.type
* Remove console.log
Co-authored-by: Jonathan Tran <jonnytran@gmail.com>
* Replace all instances of event casting with guards against event.type
---------
Co-authored-by: Frank Noirot <frank@kittycad.io>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Kurt Hutten Irev-Dev <k.hutten@protonmail.ch>
Co-authored-by: Jonathan Tran <jonnytran@gmail.com>
Co-authored-by: Frank Noirot <frank@zoo.dev>
			
			
This commit is contained in:
		
				
					committed by
					
						
						GitHub
					
				
			
			
				
	
			
			
			
						parent
						
							7c2cfba0ac
						
					
				
				
					commit
					5f8d4f8294
				
			
										
											Binary file not shown.
										
									
								
							| 
		 Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB  | 
@ -34,7 +34,7 @@
 | 
			
		||||
    "@ts-stack/markdown": "^1.5.0",
 | 
			
		||||
    "@tweenjs/tween.js": "^23.1.1",
 | 
			
		||||
    "@xstate/inspect": "^0.8.0",
 | 
			
		||||
    "@xstate/react": "^3.2.2",
 | 
			
		||||
    "@xstate/react": "^4.1.1",
 | 
			
		||||
    "bonjour-service": "^1.2.1",
 | 
			
		||||
    "codemirror": "^6.0.1",
 | 
			
		||||
    "decamelize": "^6.0.0",
 | 
			
		||||
@ -64,7 +64,7 @@
 | 
			
		||||
    "vscode-languageserver-protocol": "^3.17.5",
 | 
			
		||||
    "vscode-uri": "^3.0.8",
 | 
			
		||||
    "web-vitals": "^3.5.2",
 | 
			
		||||
    "xstate": "^4.38.2"
 | 
			
		||||
    "xstate": "^5.17.4"
 | 
			
		||||
  },
 | 
			
		||||
  "scripts": {
 | 
			
		||||
    "start": "vite",
 | 
			
		||||
 | 
			
		||||
@ -95,7 +95,7 @@ export function App() {
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    const newCmdId = uuidv4()
 | 
			
		||||
    if (state.matches('idle.showPlanes')) return
 | 
			
		||||
    if (state.matches({ idle: 'showPlanes' })) return
 | 
			
		||||
    if (context.store?.buttonDownInStream !== undefined) return
 | 
			
		||||
    debounceSocketSend({
 | 
			
		||||
      type: 'modeling_cmd_req',
 | 
			
		||||
 | 
			
		||||
@ -70,12 +70,12 @@ export function Toolbar({
 | 
			
		||||
   */
 | 
			
		||||
  const configCallbackProps: ToolbarItemCallbackProps = useMemo(
 | 
			
		||||
    () => ({
 | 
			
		||||
      modelingStateMatches: state.matches,
 | 
			
		||||
      modelingState: state,
 | 
			
		||||
      modelingSend: send,
 | 
			
		||||
      commandBarSend,
 | 
			
		||||
      sketchPathId,
 | 
			
		||||
    }),
 | 
			
		||||
    [state.matches, send, commandBarSend, sketchPathId]
 | 
			
		||||
    [state, send, commandBarSend, sketchPathId]
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
 | 
			
		||||
@ -124,9 +124,9 @@ export const ClientSideScene = ({
 | 
			
		||||
    } else if (context.mouseState.type === 'isDragging') {
 | 
			
		||||
      cursor = 'grabbing'
 | 
			
		||||
    } else if (
 | 
			
		||||
      state.matches('Sketch.Line tool') ||
 | 
			
		||||
      state.matches('Sketch.Tangential arc to') ||
 | 
			
		||||
      state.matches('Sketch.Rectangle tool')
 | 
			
		||||
      state.matches({ Sketch: 'Line tool' }) ||
 | 
			
		||||
      state.matches({ Sketch: 'Tangential arc to' }) ||
 | 
			
		||||
      state.matches({ Sketch: 'Rectangle tool' })
 | 
			
		||||
    ) {
 | 
			
		||||
      cursor = 'crosshair'
 | 
			
		||||
    } else {
 | 
			
		||||
@ -214,9 +214,9 @@ const Overlay = ({
 | 
			
		||||
    overlay.visible &&
 | 
			
		||||
    typeof context?.segmentHoverMap?.[pathToNodeString] === 'number' &&
 | 
			
		||||
    !(
 | 
			
		||||
      state.matches('Sketch.Line tool') ||
 | 
			
		||||
      state.matches('Sketch.Tangential arc to') ||
 | 
			
		||||
      state.matches('Sketch.Rectangle tool')
 | 
			
		||||
      state.matches({ Sketch: 'Line tool' }) ||
 | 
			
		||||
      state.matches({ Sketch: 'Tangential arc to' }) ||
 | 
			
		||||
      state.matches({ Sketch: 'Rectangle tool' })
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
 | 
			
		||||
@ -1,53 +1,43 @@
 | 
			
		||||
import { useMachine } from '@xstate/react'
 | 
			
		||||
import { createActorContext } from '@xstate/react'
 | 
			
		||||
import { editorManager } from 'lib/singletons'
 | 
			
		||||
import { commandBarMachine } from 'machines/commandBarMachine'
 | 
			
		||||
import { createContext, useEffect } from 'react'
 | 
			
		||||
import { EventFrom, StateFrom } from 'xstate'
 | 
			
		||||
import { useEffect } from 'react'
 | 
			
		||||
 | 
			
		||||
type CommandsContextType = {
 | 
			
		||||
  commandBarState: StateFrom<typeof commandBarMachine>
 | 
			
		||||
  commandBarSend: (event: EventFrom<typeof commandBarMachine>) => void
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const CommandsContext = createContext<CommandsContextType>({
 | 
			
		||||
  commandBarState: commandBarMachine.initialState,
 | 
			
		||||
  commandBarSend: () => {},
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export const CommandBarProvider = ({
 | 
			
		||||
  children,
 | 
			
		||||
}: {
 | 
			
		||||
  children: React.ReactNode
 | 
			
		||||
}) => {
 | 
			
		||||
  const [commandBarState, commandBarSend] = useMachine(commandBarMachine, {
 | 
			
		||||
    devTools: true,
 | 
			
		||||
export const CommandsContext = createActorContext(
 | 
			
		||||
  commandBarMachine.provide({
 | 
			
		||||
    guards: {
 | 
			
		||||
      'Command has no arguments': (context, _event) => {
 | 
			
		||||
      'Command has no arguments': ({ context }) => {
 | 
			
		||||
        return (
 | 
			
		||||
          !context.selectedCommand?.args ||
 | 
			
		||||
          Object.keys(context.selectedCommand?.args).length === 0
 | 
			
		||||
        )
 | 
			
		||||
      },
 | 
			
		||||
      'All arguments are skippable': (context, _event) => {
 | 
			
		||||
      'All arguments are skippable': ({ context }) => {
 | 
			
		||||
        return Object.values(context.selectedCommand!.args!).every(
 | 
			
		||||
          (argConfig) => argConfig.skip
 | 
			
		||||
        )
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  })
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    editorManager.setCommandBarSend(commandBarSend)
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
export const CommandBarProvider = ({
 | 
			
		||||
  children,
 | 
			
		||||
}: {
 | 
			
		||||
  children: React.ReactNode
 | 
			
		||||
}) => {
 | 
			
		||||
  return (
 | 
			
		||||
    <CommandsContext.Provider
 | 
			
		||||
      value={{
 | 
			
		||||
        commandBarState,
 | 
			
		||||
        commandBarSend,
 | 
			
		||||
      }}
 | 
			
		||||
    >
 | 
			
		||||
      {children}
 | 
			
		||||
    <CommandsContext.Provider>
 | 
			
		||||
      <CommandBarProviderInner>{children}</CommandBarProviderInner>
 | 
			
		||||
    </CommandsContext.Provider>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
function CommandBarProviderInner({ children }: { children: React.ReactNode }) {
 | 
			
		||||
  const commandBarActor = CommandsContext.useActorRef()
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    editorManager.setCommandBarSend(commandBarActor.send)
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  return children
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -52,7 +52,7 @@ function CommandBarReview({ stepBack }: { stepBack: () => void }) {
 | 
			
		||||
    e.preventDefault()
 | 
			
		||||
    commandBarSend({
 | 
			
		||||
      type: 'Submit command',
 | 
			
		||||
      data: argumentsToSubmit,
 | 
			
		||||
      output: argumentsToSubmit,
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -9,7 +9,7 @@ import {
 | 
			
		||||
  getSelectionTypeDisplayText,
 | 
			
		||||
} from 'lib/selections'
 | 
			
		||||
import { modelingMachine } from 'machines/modelingMachine'
 | 
			
		||||
import { useCallback, useEffect, useRef, useState } from 'react'
 | 
			
		||||
import { useEffect, useMemo, useRef, useState } from 'react'
 | 
			
		||||
import { StateFrom } from 'xstate'
 | 
			
		||||
 | 
			
		||||
const semanticEntityNames: { [key: string]: Array<Selection['type']> } = {
 | 
			
		||||
@ -48,15 +48,15 @@ function CommandBarSelectionInput({
 | 
			
		||||
  const { commandBarState, commandBarSend } = useCommandsContext()
 | 
			
		||||
  const [hasSubmitted, setHasSubmitted] = useState(false)
 | 
			
		||||
  const selection = useSelector(arg.machineActor, selectionSelector)
 | 
			
		||||
  const initSelectionsByType = useCallback(() => {
 | 
			
		||||
  const selectionsByType = useMemo(() => {
 | 
			
		||||
    const selectionRangeEnd = selection.codeBasedSelections[0]?.range[1]
 | 
			
		||||
    return !selectionRangeEnd || selectionRangeEnd === code.length
 | 
			
		||||
      ? 'none'
 | 
			
		||||
      : getSelectionType(selection)
 | 
			
		||||
  }, [selection, code])
 | 
			
		||||
  const selectionsByType = initSelectionsByType()
 | 
			
		||||
  const [canSubmitSelection, setCanSubmitSelection] = useState<boolean>(
 | 
			
		||||
    canSubmitSelectionArg(selectionsByType, arg)
 | 
			
		||||
  const canSubmitSelection = useMemo<boolean>(
 | 
			
		||||
    () => canSubmitSelectionArg(selectionsByType, arg),
 | 
			
		||||
    [selectionsByType]
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
@ -66,26 +66,18 @@ function CommandBarSelectionInput({
 | 
			
		||||
  // Fast-forward through this arg if it's marked as skippable
 | 
			
		||||
  // and we have a valid selection already
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    console.log('selection input effect', {
 | 
			
		||||
      selectionsByType,
 | 
			
		||||
      canSubmitSelection,
 | 
			
		||||
      arg,
 | 
			
		||||
    })
 | 
			
		||||
    setCanSubmitSelection(canSubmitSelectionArg(selectionsByType, arg))
 | 
			
		||||
    const argValue = commandBarState.context.argumentsToSubmit[arg.name]
 | 
			
		||||
    if (canSubmitSelection && arg.skip && argValue === undefined) {
 | 
			
		||||
      handleSubmit({
 | 
			
		||||
        preventDefault: () => {},
 | 
			
		||||
      } as React.FormEvent<HTMLFormElement>)
 | 
			
		||||
      handleSubmit()
 | 
			
		||||
    }
 | 
			
		||||
  }, [selectionsByType, arg])
 | 
			
		||||
  }, [canSubmitSelection])
 | 
			
		||||
 | 
			
		||||
  function handleChange() {
 | 
			
		||||
    inputRef.current?.focus()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
 | 
			
		||||
    e.preventDefault()
 | 
			
		||||
  function handleSubmit(e?: React.FormEvent<HTMLFormElement>) {
 | 
			
		||||
    e?.preventDefault()
 | 
			
		||||
 | 
			
		||||
    if (!canSubmitSelection) {
 | 
			
		||||
      setHasSubmitted(true)
 | 
			
		||||
 | 
			
		||||
@ -5,13 +5,12 @@ import { PATHS } from 'lib/paths'
 | 
			
		||||
import React, { createContext } from 'react'
 | 
			
		||||
import { toast } from 'react-hot-toast'
 | 
			
		||||
import {
 | 
			
		||||
  Actor,
 | 
			
		||||
  AnyStateMachine,
 | 
			
		||||
  ContextFrom,
 | 
			
		||||
  EventFrom,
 | 
			
		||||
  InterpreterFrom,
 | 
			
		||||
  Prop,
 | 
			
		||||
  StateFrom,
 | 
			
		||||
  assign,
 | 
			
		||||
  fromPromise,
 | 
			
		||||
} from 'xstate'
 | 
			
		||||
import { useCommandsContext } from 'hooks/useCommandsContext'
 | 
			
		||||
import { fileMachine } from 'machines/fileMachine'
 | 
			
		||||
@ -27,7 +26,7 @@ import { getNextDirName, getNextFileName } from 'lib/desktopFS'
 | 
			
		||||
type MachineContext<T extends AnyStateMachine> = {
 | 
			
		||||
  state: StateFrom<T>
 | 
			
		||||
  context: ContextFrom<T>
 | 
			
		||||
  send: Prop<InterpreterFrom<T>, 'send'>
 | 
			
		||||
  send: Prop<Actor<T>, 'send'>
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const FileContext = createContext(
 | 
			
		||||
@ -43,239 +42,234 @@ export const FileMachineProvider = ({
 | 
			
		||||
  const { commandBarSend } = useCommandsContext()
 | 
			
		||||
  const { project, file } = useRouteLoaderData(PATHS.FILE) as IndexLoaderData
 | 
			
		||||
 | 
			
		||||
  const [state, send] = useMachine(fileMachine, {
 | 
			
		||||
    context: {
 | 
			
		||||
      project,
 | 
			
		||||
      selectedDirectory: project,
 | 
			
		||||
    },
 | 
			
		||||
    actions: {
 | 
			
		||||
      navigateToFile: (context, event) => {
 | 
			
		||||
        if (event.data && 'name' in event.data) {
 | 
			
		||||
          commandBarSend({ type: 'Close' })
 | 
			
		||||
          navigate(
 | 
			
		||||
            `..${PATHS.FILE}/${encodeURIComponent(
 | 
			
		||||
              context.selectedDirectory +
 | 
			
		||||
                window.electron.path.sep +
 | 
			
		||||
                event.data.name
 | 
			
		||||
            )}`
 | 
			
		||||
  const [state, send] = useMachine(
 | 
			
		||||
    fileMachine.provide({
 | 
			
		||||
      actions: {
 | 
			
		||||
        renameToastSuccess: ({ event }) => {
 | 
			
		||||
          if (event.type !== 'xstate.done.actor.rename-file') return
 | 
			
		||||
          toast.success(event.output.message)
 | 
			
		||||
        },
 | 
			
		||||
        createToastSuccess: ({ event }) => {
 | 
			
		||||
          if (event.type !== 'xstate.done.actor.create-and-open-file') return
 | 
			
		||||
          toast.success(event.output.message)
 | 
			
		||||
        },
 | 
			
		||||
        toastSuccess: ({ event }) => {
 | 
			
		||||
          if (
 | 
			
		||||
            event.type !== 'xstate.done.actor.rename-file' &&
 | 
			
		||||
            event.type !== 'xstate.done.actor.delete-file'
 | 
			
		||||
          )
 | 
			
		||||
        } else if (
 | 
			
		||||
          event.data &&
 | 
			
		||||
          'path' in event.data &&
 | 
			
		||||
          event.data.path.endsWith(FILE_EXT)
 | 
			
		||||
        ) {
 | 
			
		||||
          // Don't navigate to newly created directories
 | 
			
		||||
          navigate(`..${PATHS.FILE}/${encodeURIComponent(event.data.path)}`)
 | 
			
		||||
        }
 | 
			
		||||
            return
 | 
			
		||||
          toast.success(event.output.message)
 | 
			
		||||
        },
 | 
			
		||||
        toastError: ({ event }) => {
 | 
			
		||||
          if (event.type !== 'xstate.done.actor.rename-file') return
 | 
			
		||||
          toast.error(event.output.message)
 | 
			
		||||
        },
 | 
			
		||||
        navigateToFile: ({ context, event }) => {
 | 
			
		||||
          if (event.type !== 'xstate.done.actor.create-and-open-file') return
 | 
			
		||||
          if (event.output && 'name' in event.output) {
 | 
			
		||||
            commandBarSend({ type: 'Close' })
 | 
			
		||||
            navigate(
 | 
			
		||||
              `..${PATHS.FILE}/${encodeURIComponent(
 | 
			
		||||
                context.selectedDirectory +
 | 
			
		||||
                  window.electron.path.sep +
 | 
			
		||||
                  event.output.name
 | 
			
		||||
              )}`
 | 
			
		||||
            )
 | 
			
		||||
          } else if (
 | 
			
		||||
            event.output &&
 | 
			
		||||
            'path' in event.output &&
 | 
			
		||||
            event.output.path.endsWith(FILE_EXT)
 | 
			
		||||
          ) {
 | 
			
		||||
            // Don't navigate to newly created directories
 | 
			
		||||
            navigate(`..${PATHS.FILE}/${encodeURIComponent(event.output.path)}`)
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      addFileToRenamingQueue: assign({
 | 
			
		||||
        itemsBeingRenamed: (context, event) => [
 | 
			
		||||
          ...context.itemsBeingRenamed,
 | 
			
		||||
          event.data.path,
 | 
			
		||||
        ],
 | 
			
		||||
      }),
 | 
			
		||||
      removeFileFromRenamingQueue: assign({
 | 
			
		||||
        itemsBeingRenamed: (
 | 
			
		||||
          context,
 | 
			
		||||
          event: EventFrom<typeof fileMachine, 'done.invoke.rename-file'>
 | 
			
		||||
        ) =>
 | 
			
		||||
          context.itemsBeingRenamed.filter(
 | 
			
		||||
            (path) => path !== event.data.oldPath
 | 
			
		||||
          ),
 | 
			
		||||
      }),
 | 
			
		||||
      renameToastSuccess: (_, event) => toast.success(event.data.message),
 | 
			
		||||
      createToastSuccess: (_, event) => toast.success(event.data.message),
 | 
			
		||||
      toastSuccess: (_, event) =>
 | 
			
		||||
        event.data && toast.success((event.data || '') + ''),
 | 
			
		||||
      toastError: (_, event) => toast.error((event.data || '') + ''),
 | 
			
		||||
    },
 | 
			
		||||
    services: {
 | 
			
		||||
      readFiles: async (context: ContextFrom<typeof fileMachine>) => {
 | 
			
		||||
        const newFiles = isDesktop()
 | 
			
		||||
          ? (await getProjectInfo(context.project.path)).children
 | 
			
		||||
          : []
 | 
			
		||||
        return {
 | 
			
		||||
          ...context.project,
 | 
			
		||||
          children: newFiles,
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      createAndOpenFile: async (context, event) => {
 | 
			
		||||
        let createdName = event.data.name.trim() || DEFAULT_FILE_NAME
 | 
			
		||||
        let createdPath: string
 | 
			
		||||
 | 
			
		||||
        if (event.data.makeDir) {
 | 
			
		||||
          let { name, path } = getNextDirName({
 | 
			
		||||
            entryName: createdName,
 | 
			
		||||
            baseDir: context.selectedDirectory.path,
 | 
			
		||||
          })
 | 
			
		||||
          createdName = name
 | 
			
		||||
          createdPath = path
 | 
			
		||||
          await window.electron.mkdir(createdPath)
 | 
			
		||||
        } else {
 | 
			
		||||
          const { name, path } = getNextFileName({
 | 
			
		||||
            entryName: createdName,
 | 
			
		||||
            baseDir: context.selectedDirectory.path,
 | 
			
		||||
          })
 | 
			
		||||
          createdName = name
 | 
			
		||||
          createdPath = path
 | 
			
		||||
          await window.electron.writeFile(createdPath, event.data.content ?? '')
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return {
 | 
			
		||||
          message: `Successfully created "${createdName}"`,
 | 
			
		||||
          path: createdPath,
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      createFile: async (context, event) => {
 | 
			
		||||
        let createdName = event.data.name.trim() || DEFAULT_FILE_NAME
 | 
			
		||||
        let createdPath: string
 | 
			
		||||
 | 
			
		||||
        if (event.data.makeDir) {
 | 
			
		||||
          let { name, path } = getNextDirName({
 | 
			
		||||
            entryName: createdName,
 | 
			
		||||
            baseDir: context.selectedDirectory.path,
 | 
			
		||||
          })
 | 
			
		||||
          createdName = name
 | 
			
		||||
          createdPath = path
 | 
			
		||||
          await window.electron.mkdir(createdPath)
 | 
			
		||||
        } else {
 | 
			
		||||
          const { name, path } = getNextFileName({
 | 
			
		||||
            entryName: createdName,
 | 
			
		||||
            baseDir: context.selectedDirectory.path,
 | 
			
		||||
          })
 | 
			
		||||
          createdName = name
 | 
			
		||||
          createdPath = path
 | 
			
		||||
          await window.electron.writeFile(createdPath, event.data.content ?? '')
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return {
 | 
			
		||||
          path: createdPath,
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      renameFile: async (
 | 
			
		||||
        context: ContextFrom<typeof fileMachine>,
 | 
			
		||||
        event: EventFrom<typeof fileMachine, 'Rename file'>
 | 
			
		||||
      ) => {
 | 
			
		||||
        const { oldName, newName, isDir } = event.data
 | 
			
		||||
        const name = newName
 | 
			
		||||
          ? newName.endsWith(FILE_EXT) || isDir
 | 
			
		||||
            ? newName
 | 
			
		||||
            : newName + FILE_EXT
 | 
			
		||||
          : DEFAULT_FILE_NAME
 | 
			
		||||
        const oldPath = window.electron.path.join(
 | 
			
		||||
          context.selectedDirectory.path,
 | 
			
		||||
          oldName
 | 
			
		||||
        )
 | 
			
		||||
        const newPath = window.electron.path.join(
 | 
			
		||||
          context.selectedDirectory.path,
 | 
			
		||||
          name
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        // no-op
 | 
			
		||||
        if (oldPath === newPath) {
 | 
			
		||||
      actors: {
 | 
			
		||||
        readFiles: fromPromise(async ({ input }) => {
 | 
			
		||||
          const newFiles =
 | 
			
		||||
            (isDesktop() ? (await getProjectInfo(input.path)).children : []) ??
 | 
			
		||||
            []
 | 
			
		||||
          return {
 | 
			
		||||
            message: `Old is the same as new.`,
 | 
			
		||||
            ...input,
 | 
			
		||||
            children: newFiles,
 | 
			
		||||
          }
 | 
			
		||||
        }),
 | 
			
		||||
        createAndOpenFile: fromPromise(async ({ input }) => {
 | 
			
		||||
          let createdName = input.name.trim() || DEFAULT_FILE_NAME
 | 
			
		||||
          let createdPath: string
 | 
			
		||||
 | 
			
		||||
          if (input.makeDir) {
 | 
			
		||||
            let { name, path } = getNextDirName({
 | 
			
		||||
              entryName: createdName,
 | 
			
		||||
              baseDir: input.selectedDirectory.path,
 | 
			
		||||
            })
 | 
			
		||||
            createdName = name
 | 
			
		||||
            createdPath = path
 | 
			
		||||
            await window.electron.mkdir(createdPath)
 | 
			
		||||
          } else {
 | 
			
		||||
            const { name, path } = getNextFileName({
 | 
			
		||||
              entryName: createdName,
 | 
			
		||||
              baseDir: input.selectedDirectory.path,
 | 
			
		||||
            })
 | 
			
		||||
            createdName = name
 | 
			
		||||
            createdPath = path
 | 
			
		||||
            await window.electron.writeFile(createdPath, input.content ?? '')
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          return {
 | 
			
		||||
            message: `Successfully created "${createdName}"`,
 | 
			
		||||
            path: createdPath,
 | 
			
		||||
          }
 | 
			
		||||
        }),
 | 
			
		||||
        createFile: fromPromise(async ({ input }) => {
 | 
			
		||||
          let createdName = input.name.trim() || DEFAULT_FILE_NAME
 | 
			
		||||
          let createdPath: string
 | 
			
		||||
 | 
			
		||||
          if (input.makeDir) {
 | 
			
		||||
            let { name, path } = getNextDirName({
 | 
			
		||||
              entryName: createdName,
 | 
			
		||||
              baseDir: input.selectedDirectory.path,
 | 
			
		||||
            })
 | 
			
		||||
            createdName = name
 | 
			
		||||
            createdPath = path
 | 
			
		||||
            await window.electron.mkdir(createdPath)
 | 
			
		||||
          } else {
 | 
			
		||||
            const { name, path } = getNextFileName({
 | 
			
		||||
              entryName: createdName,
 | 
			
		||||
              baseDir: input.selectedDirectory.path,
 | 
			
		||||
            })
 | 
			
		||||
            createdName = name
 | 
			
		||||
            createdPath = path
 | 
			
		||||
            await window.electron.writeFile(createdPath, input.content ?? '')
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          return {
 | 
			
		||||
            path: createdPath,
 | 
			
		||||
          }
 | 
			
		||||
        }),
 | 
			
		||||
        renameFile: fromPromise(async ({ input }) => {
 | 
			
		||||
          const { oldName, newName, isDir } = input
 | 
			
		||||
          const name = newName
 | 
			
		||||
            ? newName.endsWith(FILE_EXT) || isDir
 | 
			
		||||
              ? newName
 | 
			
		||||
              : newName + FILE_EXT
 | 
			
		||||
            : DEFAULT_FILE_NAME
 | 
			
		||||
          const oldPath = window.electron.path.join(
 | 
			
		||||
            input.selectedDirectory.path,
 | 
			
		||||
            oldName
 | 
			
		||||
          )
 | 
			
		||||
          const newPath = window.electron.path.join(
 | 
			
		||||
            input.selectedDirectory.path,
 | 
			
		||||
            name
 | 
			
		||||
          )
 | 
			
		||||
 | 
			
		||||
          // no-op
 | 
			
		||||
          if (oldPath === newPath) {
 | 
			
		||||
            return {
 | 
			
		||||
              message: `Old is the same as new.`,
 | 
			
		||||
              newPath,
 | 
			
		||||
              oldPath,
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          // if there are any siblings with the same name, report error.
 | 
			
		||||
          const entries = await window.electron.readdir(
 | 
			
		||||
            window.electron.path.dirname(newPath)
 | 
			
		||||
          )
 | 
			
		||||
          for (let entry of entries) {
 | 
			
		||||
            if (entry === newName) {
 | 
			
		||||
              return Promise.reject(new Error('Filename already exists.'))
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          window.electron.rename(oldPath, newPath)
 | 
			
		||||
 | 
			
		||||
          if (!file) {
 | 
			
		||||
            return Promise.reject(new Error('file is not defined'))
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          if (oldPath === file.path && project?.path) {
 | 
			
		||||
            // If we just renamed the current file, navigate to the new path
 | 
			
		||||
            navigate(`..${PATHS.FILE}/${encodeURIComponent(newPath)}`)
 | 
			
		||||
          } else if (file?.path.includes(oldPath)) {
 | 
			
		||||
            // If we just renamed a directory that the current file is in, navigate to the new path
 | 
			
		||||
            navigate(
 | 
			
		||||
              `..${PATHS.FILE}/${encodeURIComponent(
 | 
			
		||||
                file.path.replace(oldPath, newPath)
 | 
			
		||||
              )}`
 | 
			
		||||
            )
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          return {
 | 
			
		||||
            message: `Successfully renamed "${oldName}" to "${name}"`,
 | 
			
		||||
            newPath,
 | 
			
		||||
            oldPath,
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
        }),
 | 
			
		||||
        deleteFile: fromPromise(async ({ input }) => {
 | 
			
		||||
          const isDir = !!input.children
 | 
			
		||||
 | 
			
		||||
        // if there are any siblings with the same name, report error.
 | 
			
		||||
        const entries = await window.electron.readdir(
 | 
			
		||||
          window.electron.path.dirname(newPath)
 | 
			
		||||
        )
 | 
			
		||||
        for (let entry of entries) {
 | 
			
		||||
          if (entry === newName) {
 | 
			
		||||
            return Promise.reject(new Error('Filename already exists.'))
 | 
			
		||||
          if (isDir) {
 | 
			
		||||
            await window.electron
 | 
			
		||||
              .rm(input.path, {
 | 
			
		||||
                recursive: true,
 | 
			
		||||
              })
 | 
			
		||||
              .catch((e) => console.error('Error deleting directory', e))
 | 
			
		||||
          } else {
 | 
			
		||||
            await window.electron
 | 
			
		||||
              .rm(input.path)
 | 
			
		||||
              .catch((e) => console.error('Error deleting file', e))
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        window.electron.rename(oldPath, newPath)
 | 
			
		||||
          // If there are no more files at all in the project, create a main.kcl
 | 
			
		||||
          // for when we navigate to the root.
 | 
			
		||||
          if (!project?.path) {
 | 
			
		||||
            return Promise.reject(new Error('Project path not set.'))
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
        if (!file) {
 | 
			
		||||
          return Promise.reject(new Error('file is not defined'))
 | 
			
		||||
        }
 | 
			
		||||
          const entries = await window.electron.readdir(project.path)
 | 
			
		||||
          const hasKclEntries =
 | 
			
		||||
            entries.filter((e: string) => e.endsWith('.kcl')).length !== 0
 | 
			
		||||
          if (!hasKclEntries) {
 | 
			
		||||
            await window.electron.writeFile(
 | 
			
		||||
              window.electron.path.join(project.path, DEFAULT_PROJECT_KCL_FILE),
 | 
			
		||||
              ''
 | 
			
		||||
            )
 | 
			
		||||
            // Refresh the route selected above because it's possible we're on
 | 
			
		||||
            // the same path on the navigate, which doesn't cause anything to
 | 
			
		||||
            // refresh, leaving a stale execution state.
 | 
			
		||||
            navigate(0)
 | 
			
		||||
            return {
 | 
			
		||||
              message: 'No more files in project, created main.kcl',
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
        if (oldPath === file.path && project?.path) {
 | 
			
		||||
          // If we just renamed the current file, navigate to the new path
 | 
			
		||||
          navigate(`..${PATHS.FILE}/${encodeURIComponent(newPath)}`)
 | 
			
		||||
        } else if (file?.path.includes(oldPath)) {
 | 
			
		||||
          // If we just renamed a directory that the current file is in, navigate to the new path
 | 
			
		||||
          navigate(
 | 
			
		||||
            `..${PATHS.FILE}/${encodeURIComponent(
 | 
			
		||||
              file.path.replace(oldPath, newPath)
 | 
			
		||||
            )}`
 | 
			
		||||
          )
 | 
			
		||||
        }
 | 
			
		||||
          // If we just deleted the current file or one of its parent directories,
 | 
			
		||||
          // navigate to the project root
 | 
			
		||||
          if (
 | 
			
		||||
            (input.path === file?.path || file?.path.includes(input.path)) &&
 | 
			
		||||
            project?.path
 | 
			
		||||
          ) {
 | 
			
		||||
            navigate(`../${PATHS.FILE}/${encodeURIComponent(project.path)}`)
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
        return {
 | 
			
		||||
          message: `Successfully renamed "${oldName}" to "${name}"`,
 | 
			
		||||
          newPath,
 | 
			
		||||
          oldPath,
 | 
			
		||||
        }
 | 
			
		||||
          return {
 | 
			
		||||
            message: `Successfully deleted ${isDir ? 'folder' : 'file'} "${
 | 
			
		||||
              input.name
 | 
			
		||||
            }"`,
 | 
			
		||||
          }
 | 
			
		||||
        }),
 | 
			
		||||
      },
 | 
			
		||||
      deleteFile: async (
 | 
			
		||||
        context: ContextFrom<typeof fileMachine>,
 | 
			
		||||
        event: EventFrom<typeof fileMachine, 'Delete file'>
 | 
			
		||||
      ) => {
 | 
			
		||||
        const isDir = !!event.data.children
 | 
			
		||||
 | 
			
		||||
        if (isDir) {
 | 
			
		||||
          await window.electron
 | 
			
		||||
            .rm(event.data.path, {
 | 
			
		||||
              recursive: true,
 | 
			
		||||
            })
 | 
			
		||||
            .catch((e) => console.error('Error deleting directory', e))
 | 
			
		||||
        } else {
 | 
			
		||||
          await window.electron
 | 
			
		||||
            .rm(event.data.path)
 | 
			
		||||
            .catch((e) => console.error('Error deleting file', e))
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // If there are no more files at all in the project, create a main.kcl
 | 
			
		||||
        // for when we navigate to the root.
 | 
			
		||||
        if (!project?.path) {
 | 
			
		||||
          return Promise.reject(new Error('Project path not set.'))
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const entries = await window.electron.readdir(project.path)
 | 
			
		||||
        const hasKclEntries =
 | 
			
		||||
          entries.filter((e: string) => e.endsWith('.kcl')).length !== 0
 | 
			
		||||
        if (!hasKclEntries) {
 | 
			
		||||
          await window.electron.writeFile(
 | 
			
		||||
            window.electron.path.join(project.path, DEFAULT_PROJECT_KCL_FILE),
 | 
			
		||||
            ''
 | 
			
		||||
          )
 | 
			
		||||
          // Refresh the route selected above because it's possible we're on
 | 
			
		||||
          // the same path on the navigate, which doesn't cause anything to
 | 
			
		||||
          // refresh, leaving a stale execution state.
 | 
			
		||||
          navigate(0)
 | 
			
		||||
          return
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // If we just deleted the current file or one of its parent directories,
 | 
			
		||||
        // navigate to the project root
 | 
			
		||||
        if (
 | 
			
		||||
          (event.data.path === file?.path ||
 | 
			
		||||
            file?.path.includes(event.data.path)) &&
 | 
			
		||||
          project?.path
 | 
			
		||||
        ) {
 | 
			
		||||
          navigate(`../${PATHS.FILE}/${encodeURIComponent(project.path)}`)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return `Successfully deleted ${isDir ? 'folder' : 'file'} "${
 | 
			
		||||
          event.data.name
 | 
			
		||||
        }"`
 | 
			
		||||
    }),
 | 
			
		||||
    {
 | 
			
		||||
      input: {
 | 
			
		||||
        project,
 | 
			
		||||
        selectedDirectory: project,
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    guards: {
 | 
			
		||||
      'Has at least 1 file': (_, event: EventFrom<typeof fileMachine>) => {
 | 
			
		||||
        if (event.type !== 'done.invoke.read-files') return false
 | 
			
		||||
        return !!event?.data?.children && event.data.children.length > 0
 | 
			
		||||
      },
 | 
			
		||||
      'Is not silent': (_, event) => !event.data?.silent,
 | 
			
		||||
    },
 | 
			
		||||
  })
 | 
			
		||||
    }
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <FileContext.Provider
 | 
			
		||||
 | 
			
		||||
@ -243,13 +243,13 @@ const FileTreeItem = ({
 | 
			
		||||
                  onClickCapture={(e) =>
 | 
			
		||||
                    fileSend({
 | 
			
		||||
                      type: 'Set selected directory',
 | 
			
		||||
                      data: fileOrDir,
 | 
			
		||||
                      directory: fileOrDir,
 | 
			
		||||
                    })
 | 
			
		||||
                  }
 | 
			
		||||
                  onFocusCapture={(e) =>
 | 
			
		||||
                    fileSend({
 | 
			
		||||
                      type: 'Set selected directory',
 | 
			
		||||
                      data: fileOrDir,
 | 
			
		||||
                      directory: fileOrDir,
 | 
			
		||||
                    })
 | 
			
		||||
                  }
 | 
			
		||||
                  onKeyDown={(e) => e.key === 'Enter' && e.preventDefault()}
 | 
			
		||||
@ -296,13 +296,13 @@ const FileTreeItem = ({
 | 
			
		||||
                  onClickCapture={(e) => {
 | 
			
		||||
                    fileSend({
 | 
			
		||||
                      type: 'Set selected directory',
 | 
			
		||||
                      data: fileOrDir,
 | 
			
		||||
                      directory: fileOrDir,
 | 
			
		||||
                    })
 | 
			
		||||
                  }}
 | 
			
		||||
                  onFocusCapture={(e) =>
 | 
			
		||||
                    fileSend({
 | 
			
		||||
                      type: 'Set selected directory',
 | 
			
		||||
                      data: fileOrDir,
 | 
			
		||||
                      directory: fileOrDir,
 | 
			
		||||
                    })
 | 
			
		||||
                  }
 | 
			
		||||
                >
 | 
			
		||||
@ -482,7 +482,7 @@ export const FileTreeInner = ({
 | 
			
		||||
        onClickCapture={(e) => {
 | 
			
		||||
          fileSend({
 | 
			
		||||
            type: 'Set selected directory',
 | 
			
		||||
            data: fileContext.project,
 | 
			
		||||
            directory: fileContext.project,
 | 
			
		||||
          })
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@ -8,7 +8,7 @@ import {
 | 
			
		||||
} from 'lib/settings/settingsTypes'
 | 
			
		||||
import { getSettingInputType } from 'lib/settings/settingsUtils'
 | 
			
		||||
import { useMemo } from 'react'
 | 
			
		||||
import { Event } from 'xstate'
 | 
			
		||||
import { EventFrom } from 'xstate'
 | 
			
		||||
 | 
			
		||||
interface SettingsFieldInputProps {
 | 
			
		||||
  // We don't need the fancy types here,
 | 
			
		||||
@ -59,7 +59,7 @@ export function SettingsFieldInput({
 | 
			
		||||
                  level: settingsLevel,
 | 
			
		||||
                  value: newValue,
 | 
			
		||||
                },
 | 
			
		||||
              } as unknown as Event<WildcardSetEvent>)
 | 
			
		||||
              } as unknown as EventFrom<WildcardSetEvent>)
 | 
			
		||||
            }}
 | 
			
		||||
          />
 | 
			
		||||
        )
 | 
			
		||||
@ -103,7 +103,7 @@ export function SettingsFieldInput({
 | 
			
		||||
                level: settingsLevel,
 | 
			
		||||
                value: e.target.value,
 | 
			
		||||
              },
 | 
			
		||||
            } as unknown as Event<WildcardSetEvent>)
 | 
			
		||||
            } as unknown as EventFrom<WildcardSetEvent>)
 | 
			
		||||
          }
 | 
			
		||||
        >
 | 
			
		||||
          {options &&
 | 
			
		||||
@ -137,7 +137,7 @@ export function SettingsFieldInput({
 | 
			
		||||
                  level: settingsLevel,
 | 
			
		||||
                  value: e.target.value,
 | 
			
		||||
                },
 | 
			
		||||
              } as unknown as Event<WildcardSetEvent>)
 | 
			
		||||
              } as unknown as EventFrom<WildcardSetEvent>)
 | 
			
		||||
            }
 | 
			
		||||
          }}
 | 
			
		||||
        />
 | 
			
		||||
 | 
			
		||||
@ -14,13 +14,7 @@ import {
 | 
			
		||||
  Themes,
 | 
			
		||||
} from 'lib/theme'
 | 
			
		||||
import decamelize from 'decamelize'
 | 
			
		||||
import {
 | 
			
		||||
  AnyStateMachine,
 | 
			
		||||
  ContextFrom,
 | 
			
		||||
  InterpreterFrom,
 | 
			
		||||
  Prop,
 | 
			
		||||
  StateFrom,
 | 
			
		||||
} from 'xstate'
 | 
			
		||||
import { Actor, AnyStateMachine, ContextFrom, Prop, StateFrom } from 'xstate'
 | 
			
		||||
import { isDesktop } from 'lib/isDesktop'
 | 
			
		||||
import { authCommandBarConfig } from 'lib/commandBarConfigs/authCommandConfig'
 | 
			
		||||
import { kclManager, sceneInfra, engineCommandManager } from 'lib/singletons'
 | 
			
		||||
@ -39,7 +33,7 @@ import { saveSettings } from 'lib/settings/settingsUtils'
 | 
			
		||||
type MachineContext<T extends AnyStateMachine> = {
 | 
			
		||||
  state: StateFrom<T>
 | 
			
		||||
  context: ContextFrom<T>
 | 
			
		||||
  send: Prop<InterpreterFrom<T>, 'send'>
 | 
			
		||||
  send: Prop<Actor<T>, 'send'>
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type SettingsAuthContextType = {
 | 
			
		||||
@ -50,7 +44,7 @@ type SettingsAuthContextType = {
 | 
			
		||||
// a little hacky for sure, open to changing it
 | 
			
		||||
// this implies that we should only even have one instance of this provider mounted at any one time
 | 
			
		||||
// but I think that's a safe assumption
 | 
			
		||||
let settingsStateRef: (typeof settingsMachine)['context'] | undefined
 | 
			
		||||
let settingsStateRef: ContextFrom<typeof settingsMachine> | undefined
 | 
			
		||||
export const getSettingsState = () => settingsStateRef
 | 
			
		||||
 | 
			
		||||
export const SettingsAuthContext = createContext({} as SettingsAuthContextType)
 | 
			
		||||
@ -101,21 +95,19 @@ export const SettingsAuthProviderBase = ({
 | 
			
		||||
  const { commandBarSend } = useCommandsContext()
 | 
			
		||||
 | 
			
		||||
  const [settingsState, settingsSend, settingsActor] = useMachine(
 | 
			
		||||
    settingsMachine,
 | 
			
		||||
    {
 | 
			
		||||
      context: loadedSettings,
 | 
			
		||||
    settingsMachine.provide({
 | 
			
		||||
      actions: {
 | 
			
		||||
        //TODO: batch all these and if that's difficult to do from tsx,
 | 
			
		||||
        // make it easy to do
 | 
			
		||||
 | 
			
		||||
        setClientSideSceneUnits: (context, event) => {
 | 
			
		||||
        setClientSideSceneUnits: ({ context, event }) => {
 | 
			
		||||
          const newBaseUnit =
 | 
			
		||||
            event.type === 'set.modeling.defaultUnit'
 | 
			
		||||
              ? (event.data.value as BaseUnit)
 | 
			
		||||
              : context.modeling.defaultUnit.current
 | 
			
		||||
          sceneInfra.baseUnit = newBaseUnit
 | 
			
		||||
        },
 | 
			
		||||
        setEngineTheme: (context) => {
 | 
			
		||||
        setEngineTheme: ({ context }) => {
 | 
			
		||||
          engineCommandManager.sendSceneCommand({
 | 
			
		||||
            cmd_id: uuidv4(),
 | 
			
		||||
            type: 'modeling_cmd_req',
 | 
			
		||||
@ -135,16 +127,16 @@ export const SettingsAuthProviderBase = ({
 | 
			
		||||
            },
 | 
			
		||||
          })
 | 
			
		||||
        },
 | 
			
		||||
        setEngineScaleGridVisibility: (context) => {
 | 
			
		||||
        setEngineScaleGridVisibility: ({ context }) => {
 | 
			
		||||
          engineCommandManager.setScaleGridVisibility(
 | 
			
		||||
            context.modeling.showScaleGrid.current
 | 
			
		||||
          )
 | 
			
		||||
        },
 | 
			
		||||
        setClientTheme: (context) => {
 | 
			
		||||
        setClientTheme: ({ context }) => {
 | 
			
		||||
          const opposingTheme = getOppositeTheme(context.app.theme.current)
 | 
			
		||||
          sceneInfra.theme = opposingTheme
 | 
			
		||||
        },
 | 
			
		||||
        setEngineEdges: (context) => {
 | 
			
		||||
        setEngineEdges: ({ context }) => {
 | 
			
		||||
          engineCommandManager.sendSceneCommand({
 | 
			
		||||
            cmd_id: uuidv4(),
 | 
			
		||||
            type: 'modeling_cmd_req',
 | 
			
		||||
@ -154,7 +146,8 @@ export const SettingsAuthProviderBase = ({
 | 
			
		||||
            },
 | 
			
		||||
          })
 | 
			
		||||
        },
 | 
			
		||||
        toastSuccess: (_, event) => {
 | 
			
		||||
        toastSuccess: ({ event }) => {
 | 
			
		||||
          if (!('data' in event)) return
 | 
			
		||||
          const eventParts = event.type.replace(/^set./, '').split('.') as [
 | 
			
		||||
            keyof typeof settings,
 | 
			
		||||
            string
 | 
			
		||||
@ -176,7 +169,7 @@ export const SettingsAuthProviderBase = ({
 | 
			
		||||
            id: `${event.type}.success`,
 | 
			
		||||
          })
 | 
			
		||||
        },
 | 
			
		||||
        'Execute AST': (context, event) => {
 | 
			
		||||
        'Execute AST': ({ context, event }) => {
 | 
			
		||||
          try {
 | 
			
		||||
            const allSettingsIncludesUnitChange =
 | 
			
		||||
              event.type === 'Set all settings' &&
 | 
			
		||||
@ -204,12 +197,11 @@ export const SettingsAuthProviderBase = ({
 | 
			
		||||
            console.error('Error executing AST after settings change', e)
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      services: {
 | 
			
		||||
        'Persist settings': (context) =>
 | 
			
		||||
        persistSettings: ({ context }) =>
 | 
			
		||||
          saveSettings(context, loadedProject?.project?.path),
 | 
			
		||||
      },
 | 
			
		||||
    }
 | 
			
		||||
    }),
 | 
			
		||||
    { input: loadedSettings }
 | 
			
		||||
  )
 | 
			
		||||
  settingsStateRef = settingsState.context
 | 
			
		||||
 | 
			
		||||
@ -292,19 +284,21 @@ export const SettingsAuthProviderBase = ({
 | 
			
		||||
  }, [settingsState.context.textEditor.blinkingCursor.current])
 | 
			
		||||
 | 
			
		||||
  // Auth machine setup
 | 
			
		||||
  const [authState, authSend, authActor] = useMachine(authMachine, {
 | 
			
		||||
    actions: {
 | 
			
		||||
      goToSignInPage: () => {
 | 
			
		||||
        navigate(PATHS.SIGN_IN)
 | 
			
		||||
        logout()
 | 
			
		||||
  const [authState, authSend, authActor] = useMachine(
 | 
			
		||||
    authMachine.provide({
 | 
			
		||||
      actions: {
 | 
			
		||||
        goToSignInPage: () => {
 | 
			
		||||
          navigate(PATHS.SIGN_IN)
 | 
			
		||||
          logout()
 | 
			
		||||
        },
 | 
			
		||||
        goToIndexPage: () => {
 | 
			
		||||
          if (location.pathname.includes(PATHS.SIGN_IN)) {
 | 
			
		||||
            navigate(PATHS.INDEX)
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      goToIndexPage: () => {
 | 
			
		||||
        if (location.pathname.includes(PATHS.SIGN_IN)) {
 | 
			
		||||
          navigate(PATHS.INDEX)
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  })
 | 
			
		||||
    })
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  useStateMachineCommands({
 | 
			
		||||
    machineId: 'auth',
 | 
			
		||||
 | 
			
		||||
@ -287,7 +287,7 @@ export const Stream = () => {
 | 
			
		||||
      },
 | 
			
		||||
    })
 | 
			
		||||
    if (state.matches('Sketch')) return
 | 
			
		||||
    if (state.matches('idle.showPlanes')) return
 | 
			
		||||
    if (state.matches({ idle: 'showPlanes' })) return
 | 
			
		||||
 | 
			
		||||
    if (!context.store?.didDragInStream && btnName(e).left) {
 | 
			
		||||
      sendSelectEventToEngine(
 | 
			
		||||
 | 
			
		||||
@ -26,7 +26,7 @@ import { sendTelemetry } from 'lib/textToCad'
 | 
			
		||||
import { Themes } from 'lib/theme'
 | 
			
		||||
import { ActionButton } from './ActionButton'
 | 
			
		||||
import { commandBarMachine } from 'machines/commandBarMachine'
 | 
			
		||||
import { EventData, EventFrom } from 'xstate'
 | 
			
		||||
import { EventFrom } from 'xstate'
 | 
			
		||||
import { fileMachine } from 'machines/fileMachine'
 | 
			
		||||
 | 
			
		||||
const CANVAS_SIZE = 128
 | 
			
		||||
@ -45,7 +45,7 @@ export function ToastTextToCadError({
 | 
			
		||||
  prompt: string
 | 
			
		||||
  commandBarSend: (
 | 
			
		||||
    event: EventFrom<typeof commandBarMachine>,
 | 
			
		||||
    data?: EventData
 | 
			
		||||
    data?: unknown
 | 
			
		||||
  ) => void
 | 
			
		||||
}) {
 | 
			
		||||
  return (
 | 
			
		||||
@ -112,7 +112,7 @@ export function ToastTextToCadSuccess({
 | 
			
		||||
  token?: string
 | 
			
		||||
  fileMachineSend: (
 | 
			
		||||
    event: EventFrom<typeof fileMachine>,
 | 
			
		||||
    data?: EventData
 | 
			
		||||
    data?: unknown
 | 
			
		||||
  ) => void
 | 
			
		||||
  settings: {
 | 
			
		||||
    theme: Themes
 | 
			
		||||
 | 
			
		||||
@ -133,7 +133,7 @@ const UserSidebarMenu = ({ user }: { user?: User }) => {
 | 
			
		||||
          Element: 'button',
 | 
			
		||||
          'data-testid': 'user-sidebar-sign-out',
 | 
			
		||||
          children: 'Sign out',
 | 
			
		||||
          onClick: () => send('Log out'),
 | 
			
		||||
          onClick: () => send({ type: 'Log out' }),
 | 
			
		||||
          className: '', // Just making TS's filter type coercion happy 😠
 | 
			
		||||
        },
 | 
			
		||||
      ].filter(
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,7 @@
 | 
			
		||||
import { EditorView, ViewUpdate } from '@codemirror/view'
 | 
			
		||||
import { EditorSelection, Annotation, Transaction } from '@codemirror/state'
 | 
			
		||||
import { engineCommandManager } from 'lib/singletons'
 | 
			
		||||
import { ModelingMachineEvent } from 'machines/modelingMachine'
 | 
			
		||||
import { modelingMachine, ModelingMachineEvent } from 'machines/modelingMachine'
 | 
			
		||||
import { Selections, processCodeMirrorRanges, Selection } from 'lib/selections'
 | 
			
		||||
import { undo, redo } from '@codemirror/commands'
 | 
			
		||||
import { CommandBarMachineEvent } from 'machines/commandBarMachine'
 | 
			
		||||
@ -11,6 +11,7 @@ import {
 | 
			
		||||
  forEachDiagnostic,
 | 
			
		||||
  setDiagnosticsEffect,
 | 
			
		||||
} from '@codemirror/lint'
 | 
			
		||||
import { StateFrom } from 'xstate'
 | 
			
		||||
 | 
			
		||||
const updateOutsideEditorAnnotation = Annotation.define<boolean>()
 | 
			
		||||
export const updateOutsideEditorEvent = updateOutsideEditorAnnotation.of(true)
 | 
			
		||||
@ -38,7 +39,7 @@ export default class EditorManager {
 | 
			
		||||
  private _lastEvent: { event: string; time: number } | null = null
 | 
			
		||||
 | 
			
		||||
  private _modelingSend: (eventInfo: ModelingMachineEvent) => void = () => {}
 | 
			
		||||
  private _modelingEvent: ModelingMachineEvent | null = null
 | 
			
		||||
  private _modelingState: StateFrom<typeof modelingMachine> | null = null
 | 
			
		||||
 | 
			
		||||
  private _commandBarSend: (eventInfo: CommandBarMachineEvent) => void =
 | 
			
		||||
    () => {}
 | 
			
		||||
@ -80,8 +81,8 @@ export default class EditorManager {
 | 
			
		||||
    this._modelingSend = send
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  set modelingEvent(event: ModelingMachineEvent) {
 | 
			
		||||
    this._modelingEvent = event
 | 
			
		||||
  set modelingState(state: StateFrom<typeof modelingMachine>) {
 | 
			
		||||
    this._modelingState = state
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  setCommandBarSend(send: (eventInfo: CommandBarMachineEvent) => void) {
 | 
			
		||||
@ -248,13 +249,11 @@ export default class EditorManager {
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const ignoreEvents: ModelingMachineEvent['type'][] = ['change tool']
 | 
			
		||||
 | 
			
		||||
    if (!this._modelingEvent) {
 | 
			
		||||
    if (!this._modelingState) {
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (ignoreEvents.includes(this._modelingEvent.type)) {
 | 
			
		||||
    if (this._modelingState.matches({ Sketch: 'Change Tool' })) {
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,10 @@
 | 
			
		||||
import { CommandsContext } from 'components/CommandBar/CommandBarProvider'
 | 
			
		||||
import { useContext } from 'react'
 | 
			
		||||
 | 
			
		||||
export const useCommandsContext = () => {
 | 
			
		||||
  return useContext(CommandsContext)
 | 
			
		||||
  const commandBarActor = CommandsContext.useActorRef()
 | 
			
		||||
  const commandBarState = CommandsContext.useSelector((state) => state)
 | 
			
		||||
  return {
 | 
			
		||||
    commandBarSend: commandBarActor.send,
 | 
			
		||||
    commandBarState,
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -23,7 +23,8 @@ export function useRefreshSettings(routeId: string = PATHS.INDEX) {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    ctx.settings.send('Set all settings', {
 | 
			
		||||
    ctx.settings.send({
 | 
			
		||||
      type: 'Set all settings',
 | 
			
		||||
      settings: routeData,
 | 
			
		||||
    })
 | 
			
		||||
  }, [])
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,5 @@
 | 
			
		||||
import { useEffect } from 'react'
 | 
			
		||||
import { AnyStateMachine, InterpreterFrom, StateFrom } from 'xstate'
 | 
			
		||||
import { AnyStateMachine, Actor, StateFrom } from 'xstate'
 | 
			
		||||
import { createMachineCommand } from '../lib/createMachineCommand'
 | 
			
		||||
import { useCommandsContext } from './useCommandsContext'
 | 
			
		||||
import { modelingMachine } from 'machines/modelingMachine'
 | 
			
		||||
@ -15,6 +15,7 @@ import { useKclContext } from 'lang/KclProvider'
 | 
			
		||||
import { useNetworkContext } from 'hooks/useNetworkContext'
 | 
			
		||||
import { NetworkHealthState } from 'hooks/useNetworkStatus'
 | 
			
		||||
import { useAppState } from 'AppState'
 | 
			
		||||
import { getActorNextEvents } from 'lib/utils'
 | 
			
		||||
 | 
			
		||||
// This might not be necessary, AnyStateMachine from xstate is working
 | 
			
		||||
export type AllMachines =
 | 
			
		||||
@ -30,7 +31,7 @@ interface UseStateMachineCommandsArgs<
 | 
			
		||||
  machineId: T['id']
 | 
			
		||||
  state: StateFrom<T>
 | 
			
		||||
  send: Function
 | 
			
		||||
  actor: InterpreterFrom<T>
 | 
			
		||||
  actor: Actor<T>
 | 
			
		||||
  commandBarConfig?: StateMachineCommandSetConfig<T, S>
 | 
			
		||||
  allCommandsRequireNetwork?: boolean
 | 
			
		||||
  onCancel?: () => void
 | 
			
		||||
@ -59,7 +60,7 @@ export default function useStateMachineCommands<
 | 
			
		||||
        overallState !== NetworkHealthState.Weak) ||
 | 
			
		||||
      isExecuting ||
 | 
			
		||||
      !isStreamReady
 | 
			
		||||
    const newCommands = state.nextEvents
 | 
			
		||||
    const newCommands = getActorNextEvents(state)
 | 
			
		||||
      .filter((_) => !allCommandsRequireNetwork || !disableAllButtons)
 | 
			
		||||
      .filter((e) => !['done.', 'error.'].some((n) => e.includes(n)))
 | 
			
		||||
      .flatMap((type) =>
 | 
			
		||||
 | 
			
		||||
@ -10,7 +10,7 @@ import {
 | 
			
		||||
} from 'lib/settings/settingsTypes'
 | 
			
		||||
import { settingsMachine } from 'machines/settingsMachine'
 | 
			
		||||
import { PathValue } from 'lib/types'
 | 
			
		||||
import { AnyStateMachine, ContextFrom, InterpreterFrom } from 'xstate'
 | 
			
		||||
import { Actor, AnyStateMachine, ContextFrom } from 'xstate'
 | 
			
		||||
import { getPropertyByPath } from 'lib/objectPropertyByPath'
 | 
			
		||||
import { buildCommandArgument } from 'lib/createMachineCommand'
 | 
			
		||||
import decamelize from 'decamelize'
 | 
			
		||||
@ -28,7 +28,7 @@ export const settingsWithCommandConfigs = (
 | 
			
		||||
  ) as SettingsPaths[]
 | 
			
		||||
 | 
			
		||||
const levelArgConfig = <T extends AnyStateMachine = AnyStateMachine>(
 | 
			
		||||
  actor: InterpreterFrom<T>,
 | 
			
		||||
  actor: Actor<T>,
 | 
			
		||||
  isProjectAvailable: boolean,
 | 
			
		||||
  hideOnLevel?: SettingsLevel
 | 
			
		||||
): CommandArgument<SettingsLevel, T> => ({
 | 
			
		||||
@ -55,7 +55,7 @@ interface CreateSettingsArgs {
 | 
			
		||||
  type: SettingsPaths
 | 
			
		||||
  send: Function
 | 
			
		||||
  context: ContextFrom<typeof settingsMachine>
 | 
			
		||||
  actor: InterpreterFrom<typeof settingsMachine>
 | 
			
		||||
  actor: Actor<typeof settingsMachine>
 | 
			
		||||
  isProjectAvailable: boolean
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -132,7 +132,7 @@ export function createSettingsCommand({
 | 
			
		||||
      if (data !== undefined && data !== null) {
 | 
			
		||||
        send({ type: `set.${type}`, data })
 | 
			
		||||
      } else {
 | 
			
		||||
        send(type)
 | 
			
		||||
        send({ type })
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    args: {
 | 
			
		||||
 | 
			
		||||
@ -1,11 +1,6 @@
 | 
			
		||||
import { CustomIconName } from 'components/CustomIcon'
 | 
			
		||||
import { AllMachines } from 'hooks/useStateMachineCommands'
 | 
			
		||||
import {
 | 
			
		||||
  AnyStateMachine,
 | 
			
		||||
  ContextFrom,
 | 
			
		||||
  EventFrom,
 | 
			
		||||
  InterpreterFrom,
 | 
			
		||||
} from 'xstate'
 | 
			
		||||
import { Actor, AnyStateMachine, ContextFrom, EventFrom } from 'xstate'
 | 
			
		||||
import { Selection } from './selections'
 | 
			
		||||
import { Identifier, Expr, VariableDeclaration } from 'lang/wasm'
 | 
			
		||||
import { commandBarMachine } from 'machines/commandBarMachine'
 | 
			
		||||
@ -186,7 +181,7 @@ export type CommandArgument<
 | 
			
		||||
        machineContext?: ContextFrom<T>
 | 
			
		||||
      ) => boolean)
 | 
			
		||||
  skip?: boolean
 | 
			
		||||
  machineActor: InterpreterFrom<T>
 | 
			
		||||
  machineActor: Actor<T>
 | 
			
		||||
  /** For showing a summary display of the current value, such as in
 | 
			
		||||
   *  the command bar's header
 | 
			
		||||
   */
 | 
			
		||||
 | 
			
		||||
@ -2,7 +2,7 @@ import {
 | 
			
		||||
  AnyStateMachine,
 | 
			
		||||
  ContextFrom,
 | 
			
		||||
  EventFrom,
 | 
			
		||||
  InterpreterFrom,
 | 
			
		||||
  Actor,
 | 
			
		||||
  StateFrom,
 | 
			
		||||
} from 'xstate'
 | 
			
		||||
import { isDesktop } from './isDesktop'
 | 
			
		||||
@ -23,7 +23,7 @@ interface CreateMachineCommandProps<
 | 
			
		||||
  groupId: T['id']
 | 
			
		||||
  state: StateFrom<T>
 | 
			
		||||
  send: Function
 | 
			
		||||
  actor: InterpreterFrom<T>
 | 
			
		||||
  actor: Actor<T>
 | 
			
		||||
  commandBarConfig?: StateMachineCommandSetConfig<T, S>
 | 
			
		||||
  onCancel?: () => void
 | 
			
		||||
}
 | 
			
		||||
@ -90,9 +90,9 @@ export function createMachineCommand<
 | 
			
		||||
    needsReview: commandConfig.needsReview || false,
 | 
			
		||||
    onSubmit: (data?: S[typeof type]) => {
 | 
			
		||||
      if (data !== undefined && data !== null) {
 | 
			
		||||
        send(type, { data })
 | 
			
		||||
        send({ type, data })
 | 
			
		||||
      } else {
 | 
			
		||||
        send(type)
 | 
			
		||||
        send({ type })
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
  }
 | 
			
		||||
@ -124,7 +124,7 @@ function buildCommandArguments<
 | 
			
		||||
>(
 | 
			
		||||
  state: StateFrom<T>,
 | 
			
		||||
  args: CommandConfig<T, CommandName, S>['args'],
 | 
			
		||||
  machineActor: InterpreterFrom<T>
 | 
			
		||||
  machineActor: Actor<T>
 | 
			
		||||
): NonNullable<Command<T, CommandName, S>['args']> {
 | 
			
		||||
  const newArgs = {} as NonNullable<Command<T, CommandName, S>['args']>
 | 
			
		||||
 | 
			
		||||
@ -143,7 +143,7 @@ export function buildCommandArgument<
 | 
			
		||||
>(
 | 
			
		||||
  arg: CommandArgumentConfig<O, T>,
 | 
			
		||||
  context: ContextFrom<T>,
 | 
			
		||||
  machineActor: InterpreterFrom<T>
 | 
			
		||||
  machineActor: Actor<T>
 | 
			
		||||
): CommandArgument<O, T> & { inputType: typeof arg.inputType } {
 | 
			
		||||
  const baseCommandArgument = {
 | 
			
		||||
    description: arg.description,
 | 
			
		||||
 | 
			
		||||
@ -6,7 +6,7 @@ import {
 | 
			
		||||
import { VITE_KC_API_BASE_URL } from 'env'
 | 
			
		||||
import toast from 'react-hot-toast'
 | 
			
		||||
import { FILE_EXT } from './constants'
 | 
			
		||||
import { ContextFrom, EventData, EventFrom } from 'xstate'
 | 
			
		||||
import { ContextFrom, EventFrom } from 'xstate'
 | 
			
		||||
import { fileMachine } from 'machines/fileMachine'
 | 
			
		||||
import { NavigateFunction } from 'react-router-dom'
 | 
			
		||||
import crossPlatformFetch from './crossPlatformFetch'
 | 
			
		||||
@ -63,12 +63,12 @@ interface TextToKclProps {
 | 
			
		||||
  trimmedPrompt: string
 | 
			
		||||
  fileMachineSend: (
 | 
			
		||||
    type: EventFrom<typeof fileMachine>,
 | 
			
		||||
    data?: EventData
 | 
			
		||||
    data?: unknown
 | 
			
		||||
  ) => unknown
 | 
			
		||||
  navigate: NavigateFunction
 | 
			
		||||
  commandBarSend: (
 | 
			
		||||
    type: EventFrom<typeof commandBarMachine>,
 | 
			
		||||
    data?: EventData
 | 
			
		||||
    data?: unknown
 | 
			
		||||
  ) => unknown
 | 
			
		||||
  context: ContextFrom<typeof fileMachine>
 | 
			
		||||
  token?: string
 | 
			
		||||
 | 
			
		||||
@ -16,7 +16,7 @@ type ToolbarMode = {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface ToolbarItemCallbackProps {
 | 
			
		||||
  modelingStateMatches: StateFrom<typeof modelingMachine>['matches']
 | 
			
		||||
  modelingState: StateFrom<typeof modelingMachine>
 | 
			
		||||
  modelingSend: (event: EventFrom<typeof modelingMachine>) => void
 | 
			
		||||
  commandBarSend: (event: EventFrom<typeof commandBarMachine>) => void
 | 
			
		||||
  sketchPathId: string | false
 | 
			
		||||
@ -84,7 +84,7 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
 | 
			
		||||
            type: 'Find and select command',
 | 
			
		||||
            data: { name: 'Extrude', groupId: 'modeling' },
 | 
			
		||||
          }),
 | 
			
		||||
        disabled: (state) => !state.can('Extrude'),
 | 
			
		||||
        disabled: (state) => !state.can({ type: 'Extrude' }),
 | 
			
		||||
        icon: 'extrude',
 | 
			
		||||
        status: 'available',
 | 
			
		||||
        title: 'Extrude',
 | 
			
		||||
@ -155,7 +155,7 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
 | 
			
		||||
          }),
 | 
			
		||||
        icon: 'fillet3d',
 | 
			
		||||
        status: DEV ? 'available' : 'kcl-only',
 | 
			
		||||
        disabled: (state) => !state.can('Fillet'),
 | 
			
		||||
        disabled: (state) => !state.can({ type: 'Fillet' }),
 | 
			
		||||
        title: 'Fillet',
 | 
			
		||||
        hotkey: 'F',
 | 
			
		||||
        description: 'Round the edges of a 3D solid.',
 | 
			
		||||
@ -272,7 +272,7 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
 | 
			
		||||
          }),
 | 
			
		||||
        disableHotkey: (state) =>
 | 
			
		||||
          !(
 | 
			
		||||
            state.matches('Sketch.SketchIdle') ||
 | 
			
		||||
            state.matches({ Sketch: 'SketchIdle' }) ||
 | 
			
		||||
            state.matches('Sketch no face')
 | 
			
		||||
          ),
 | 
			
		||||
        icon: 'arrowLeft',
 | 
			
		||||
@ -286,33 +286,37 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
 | 
			
		||||
      'break',
 | 
			
		||||
      {
 | 
			
		||||
        id: 'line',
 | 
			
		||||
        onClick: ({ modelingStateMatches: matches, modelingSend }) =>
 | 
			
		||||
        onClick: ({ modelingState, modelingSend }) =>
 | 
			
		||||
          modelingSend({
 | 
			
		||||
            type: 'change tool',
 | 
			
		||||
            data: {
 | 
			
		||||
              tool: !matches('Sketch.Line tool') ? 'line' : 'none',
 | 
			
		||||
              tool: !modelingState.matches({ Sketch: 'Line tool' })
 | 
			
		||||
                ? 'line'
 | 
			
		||||
                : 'none',
 | 
			
		||||
            },
 | 
			
		||||
          }),
 | 
			
		||||
        icon: 'line',
 | 
			
		||||
        status: 'available',
 | 
			
		||||
        disabled: (state) =>
 | 
			
		||||
          state.matches('Sketch no face') ||
 | 
			
		||||
          state.matches('Sketch.Rectangle tool.Awaiting second corner'),
 | 
			
		||||
          state.matches({
 | 
			
		||||
            Sketch: { 'Rectangle tool': 'Awaiting second corner' },
 | 
			
		||||
          }),
 | 
			
		||||
        title: 'Line',
 | 
			
		||||
        hotkey: (state) =>
 | 
			
		||||
          state.matches('Sketch.Line tool') ? ['Esc', 'L'] : 'L',
 | 
			
		||||
          state.matches({ Sketch: 'Line tool' }) ? ['Esc', 'L'] : 'L',
 | 
			
		||||
        description: 'Start drawing straight lines',
 | 
			
		||||
        links: [],
 | 
			
		||||
        isActive: (state) => state.matches('Sketch.Line tool'),
 | 
			
		||||
        isActive: (state) => state.matches({ Sketch: 'Line tool' }),
 | 
			
		||||
      },
 | 
			
		||||
      [
 | 
			
		||||
        {
 | 
			
		||||
          id: 'tangential-arc',
 | 
			
		||||
          onClick: ({ modelingStateMatches, modelingSend }) =>
 | 
			
		||||
          onClick: ({ modelingState, modelingSend }) =>
 | 
			
		||||
            modelingSend({
 | 
			
		||||
              type: 'change tool',
 | 
			
		||||
              data: {
 | 
			
		||||
                tool: !modelingStateMatches('Sketch.Tangential arc to')
 | 
			
		||||
                tool: !modelingState.matches({ Sketch: 'Tangential arc to' })
 | 
			
		||||
                  ? 'tangentialArc'
 | 
			
		||||
                  : 'none',
 | 
			
		||||
              },
 | 
			
		||||
@ -321,13 +325,13 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
 | 
			
		||||
          status: 'available',
 | 
			
		||||
          disabled: (state) =>
 | 
			
		||||
            !isEditingExistingSketch(state.context) &&
 | 
			
		||||
            !state.matches('Sketch.Tangential arc to'),
 | 
			
		||||
            !state.matches({ Sketch: 'Tangential arc to' }),
 | 
			
		||||
          title: 'Tangential Arc',
 | 
			
		||||
          hotkey: (state) =>
 | 
			
		||||
            state.matches('Sketch.Tangential arc to') ? ['Esc', 'A'] : 'A',
 | 
			
		||||
            state.matches({ Sketch: 'Tangential arc to' }) ? ['Esc', 'A'] : 'A',
 | 
			
		||||
          description: 'Start drawing an arc tangent to the current segment',
 | 
			
		||||
          links: [],
 | 
			
		||||
          isActive: (state) => state.matches('Sketch.Tangential arc to'),
 | 
			
		||||
          isActive: (state) => state.matches({ Sketch: 'Tangential arc to' }),
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          id: 'three-point-arc',
 | 
			
		||||
@ -388,11 +392,11 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
 | 
			
		||||
      [
 | 
			
		||||
        {
 | 
			
		||||
          id: 'corner-rectangle',
 | 
			
		||||
          onClick: ({ modelingStateMatches, modelingSend }) =>
 | 
			
		||||
          onClick: ({ modelingState, modelingSend }) =>
 | 
			
		||||
            modelingSend({
 | 
			
		||||
              type: 'change tool',
 | 
			
		||||
              data: {
 | 
			
		||||
                tool: !modelingStateMatches('Sketch.Rectangle tool')
 | 
			
		||||
                tool: !modelingState.matches({ Sketch: 'Rectangle tool' })
 | 
			
		||||
                  ? 'rectangle'
 | 
			
		||||
                  : 'none',
 | 
			
		||||
              },
 | 
			
		||||
@ -401,13 +405,13 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
 | 
			
		||||
          status: 'available',
 | 
			
		||||
          disabled: (state) =>
 | 
			
		||||
            !canRectangleTool(state.context) &&
 | 
			
		||||
            !state.matches('Sketch.Rectangle tool'),
 | 
			
		||||
            !state.matches({ Sketch: 'Rectangle tool' }),
 | 
			
		||||
          title: 'Corner rectangle',
 | 
			
		||||
          hotkey: (state) =>
 | 
			
		||||
            state.matches('Sketch.Rectangle tool') ? ['Esc', 'R'] : 'R',
 | 
			
		||||
            state.matches({ Sketch: 'Rectangle tool' }) ? ['Esc', 'R'] : 'R',
 | 
			
		||||
          description: 'Start drawing a rectangle',
 | 
			
		||||
          links: [],
 | 
			
		||||
          isActive: (state) => state.matches('Sketch.Rectangle tool'),
 | 
			
		||||
          isActive: (state) => state.matches({ Sketch: 'Rectangle tool' }),
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          id: 'center-rectangle',
 | 
			
		||||
@ -456,9 +460,8 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
 | 
			
		||||
          id: 'constraint-length',
 | 
			
		||||
          disabled: (state) =>
 | 
			
		||||
            !(
 | 
			
		||||
              state.matches('Sketch.SketchIdle') &&
 | 
			
		||||
              state.nextEvents.includes('Constrain length') &&
 | 
			
		||||
              state.can('Constrain length')
 | 
			
		||||
              state.matches({ Sketch: 'SketchIdle' }) &&
 | 
			
		||||
              state.can({ type: 'Constrain length' })
 | 
			
		||||
            ),
 | 
			
		||||
          onClick: ({ modelingSend }) =>
 | 
			
		||||
            modelingSend({ type: 'Constrain length' }),
 | 
			
		||||
@ -473,9 +476,8 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
 | 
			
		||||
          id: 'constraint-angle',
 | 
			
		||||
          disabled: (state) =>
 | 
			
		||||
            !(
 | 
			
		||||
              state.matches('Sketch.SketchIdle') &&
 | 
			
		||||
              state.nextEvents.includes('Constrain angle') &&
 | 
			
		||||
              state.can('Constrain angle')
 | 
			
		||||
              state.matches({ Sketch: 'SketchIdle' }) &&
 | 
			
		||||
              state.can({ type: 'Constrain angle' })
 | 
			
		||||
            ),
 | 
			
		||||
          onClick: ({ modelingSend }) =>
 | 
			
		||||
            modelingSend({ type: 'Constrain angle' }),
 | 
			
		||||
@ -489,9 +491,8 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
 | 
			
		||||
          id: 'constraint-vertical',
 | 
			
		||||
          disabled: (state) =>
 | 
			
		||||
            !(
 | 
			
		||||
              state.matches('Sketch.SketchIdle') &&
 | 
			
		||||
              state.nextEvents.includes('Make segment vertical') &&
 | 
			
		||||
              state.can('Make segment vertical')
 | 
			
		||||
              state.matches({ Sketch: 'SketchIdle' }) &&
 | 
			
		||||
              state.can({ type: 'Make segment vertical' })
 | 
			
		||||
            ),
 | 
			
		||||
          onClick: ({ modelingSend }) =>
 | 
			
		||||
            modelingSend({ type: 'Make segment vertical' }),
 | 
			
		||||
@ -506,9 +507,8 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
 | 
			
		||||
          id: 'constraint-horizontal',
 | 
			
		||||
          disabled: (state) =>
 | 
			
		||||
            !(
 | 
			
		||||
              state.matches('Sketch.SketchIdle') &&
 | 
			
		||||
              state.nextEvents.includes('Make segment horizontal') &&
 | 
			
		||||
              state.can('Make segment horizontal')
 | 
			
		||||
              state.matches({ Sketch: 'SketchIdle' }) &&
 | 
			
		||||
              state.can({ type: 'Make segment horizontal' })
 | 
			
		||||
            ),
 | 
			
		||||
          onClick: ({ modelingSend }) =>
 | 
			
		||||
            modelingSend({ type: 'Make segment horizontal' }),
 | 
			
		||||
@ -523,9 +523,8 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
 | 
			
		||||
          id: 'constraint-parallel',
 | 
			
		||||
          disabled: (state) =>
 | 
			
		||||
            !(
 | 
			
		||||
              state.matches('Sketch.SketchIdle') &&
 | 
			
		||||
              state.nextEvents.includes('Constrain parallel') &&
 | 
			
		||||
              state.can('Constrain parallel')
 | 
			
		||||
              state.matches({ Sketch: 'SketchIdle' }) &&
 | 
			
		||||
              state.can({ type: 'Constrain parallel' })
 | 
			
		||||
            ),
 | 
			
		||||
          onClick: ({ modelingSend }) =>
 | 
			
		||||
            modelingSend({ type: 'Constrain parallel' }),
 | 
			
		||||
@ -539,9 +538,8 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
 | 
			
		||||
          id: 'constraint-equal-length',
 | 
			
		||||
          disabled: (state) =>
 | 
			
		||||
            !(
 | 
			
		||||
              state.matches('Sketch.SketchIdle') &&
 | 
			
		||||
              state.nextEvents.includes('Constrain equal length') &&
 | 
			
		||||
              state.can('Constrain equal length')
 | 
			
		||||
              state.matches({ Sketch: 'SketchIdle' }) &&
 | 
			
		||||
              state.can({ type: 'Constrain equal length' })
 | 
			
		||||
            ),
 | 
			
		||||
          onClick: ({ modelingSend }) =>
 | 
			
		||||
            modelingSend({ type: 'Constrain equal length' }),
 | 
			
		||||
@ -555,9 +553,8 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
 | 
			
		||||
          id: 'constraint-horizontal-distance',
 | 
			
		||||
          disabled: (state) =>
 | 
			
		||||
            !(
 | 
			
		||||
              state.matches('Sketch.SketchIdle') &&
 | 
			
		||||
              state.nextEvents.includes('Constrain horizontal distance') &&
 | 
			
		||||
              state.can('Constrain horizontal distance')
 | 
			
		||||
              state.matches({ Sketch: 'SketchIdle' }) &&
 | 
			
		||||
              state.can({ type: 'Constrain horizontal distance' })
 | 
			
		||||
            ),
 | 
			
		||||
          onClick: ({ modelingSend }) =>
 | 
			
		||||
            modelingSend({ type: 'Constrain horizontal distance' }),
 | 
			
		||||
@ -571,9 +568,8 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
 | 
			
		||||
          id: 'constraint-vertical-distance',
 | 
			
		||||
          disabled: (state) =>
 | 
			
		||||
            !(
 | 
			
		||||
              state.matches('Sketch.SketchIdle') &&
 | 
			
		||||
              state.nextEvents.includes('Constrain vertical distance') &&
 | 
			
		||||
              state.can('Constrain vertical distance')
 | 
			
		||||
              state.matches({ Sketch: 'SketchIdle' }) &&
 | 
			
		||||
              state.can({ type: 'Constrain vertical distance' })
 | 
			
		||||
            ),
 | 
			
		||||
          onClick: ({ modelingSend }) =>
 | 
			
		||||
            modelingSend({ type: 'Constrain vertical distance' }),
 | 
			
		||||
@ -587,9 +583,8 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
 | 
			
		||||
          id: 'constraint-absolute-x',
 | 
			
		||||
          disabled: (state) =>
 | 
			
		||||
            !(
 | 
			
		||||
              state.matches('Sketch.SketchIdle') &&
 | 
			
		||||
              state.nextEvents.includes('Constrain ABS X') &&
 | 
			
		||||
              state.can('Constrain ABS X')
 | 
			
		||||
              state.matches({ Sketch: 'SketchIdle' }) &&
 | 
			
		||||
              state.can({ type: 'Constrain ABS X' })
 | 
			
		||||
            ),
 | 
			
		||||
          onClick: ({ modelingSend }) =>
 | 
			
		||||
            modelingSend({ type: 'Constrain ABS X' }),
 | 
			
		||||
@ -603,9 +598,8 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
 | 
			
		||||
          id: 'constraint-absolute-y',
 | 
			
		||||
          disabled: (state) =>
 | 
			
		||||
            !(
 | 
			
		||||
              state.matches('Sketch.SketchIdle') &&
 | 
			
		||||
              state.nextEvents.includes('Constrain ABS Y') &&
 | 
			
		||||
              state.can('Constrain ABS Y')
 | 
			
		||||
              state.matches({ Sketch: 'SketchIdle' }) &&
 | 
			
		||||
              state.can({ type: 'Constrain ABS Y' })
 | 
			
		||||
            ),
 | 
			
		||||
          onClick: ({ modelingSend }) =>
 | 
			
		||||
            modelingSend({ type: 'Constrain ABS Y' }),
 | 
			
		||||
@ -619,9 +613,8 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
 | 
			
		||||
          id: 'constraint-perpendicular-distance',
 | 
			
		||||
          disabled: (state) =>
 | 
			
		||||
            !(
 | 
			
		||||
              state.matches('Sketch.SketchIdle') &&
 | 
			
		||||
              state.nextEvents.includes('Constrain perpendicular distance') &&
 | 
			
		||||
              state.can('Constrain perpendicular distance')
 | 
			
		||||
              state.matches({ Sketch: 'SketchIdle' }) &&
 | 
			
		||||
              state.can({ type: 'Constrain perpendicular distance' })
 | 
			
		||||
            ),
 | 
			
		||||
          onClick: ({ modelingSend }) =>
 | 
			
		||||
            modelingSend({ type: 'Constrain perpendicular distance' }),
 | 
			
		||||
@ -636,9 +629,8 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
 | 
			
		||||
          id: 'constraint-align-horizontal',
 | 
			
		||||
          disabled: (state) =>
 | 
			
		||||
            !(
 | 
			
		||||
              state.matches('Sketch.SketchIdle') &&
 | 
			
		||||
              state.nextEvents.includes('Constrain horizontally align') &&
 | 
			
		||||
              state.can('Constrain horizontally align')
 | 
			
		||||
              state.matches({ Sketch: 'SketchIdle' }) &&
 | 
			
		||||
              state.can({ type: 'Constrain horizontally align' })
 | 
			
		||||
            ),
 | 
			
		||||
          onClick: ({ modelingSend }) =>
 | 
			
		||||
            modelingSend({ type: 'Constrain horizontally align' }),
 | 
			
		||||
@ -652,9 +644,8 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
 | 
			
		||||
          id: 'constraint-align-vertical',
 | 
			
		||||
          disabled: (state) =>
 | 
			
		||||
            !(
 | 
			
		||||
              state.matches('Sketch.SketchIdle') &&
 | 
			
		||||
              state.nextEvents.includes('Constrain vertically align') &&
 | 
			
		||||
              state.can('Constrain vertically align')
 | 
			
		||||
              state.matches({ Sketch: 'SketchIdle' }) &&
 | 
			
		||||
              state.can({ type: 'Constrain vertically align' })
 | 
			
		||||
            ),
 | 
			
		||||
          onClick: ({ modelingSend }) =>
 | 
			
		||||
            modelingSend({ type: 'Constrain vertically align' }),
 | 
			
		||||
@ -668,9 +659,8 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
 | 
			
		||||
          id: 'snap-to-x',
 | 
			
		||||
          disabled: (state) =>
 | 
			
		||||
            !(
 | 
			
		||||
              state.matches('Sketch.SketchIdle') &&
 | 
			
		||||
              state.nextEvents.includes('Constrain snap to X') &&
 | 
			
		||||
              state.can('Constrain snap to X')
 | 
			
		||||
              state.matches({ Sketch: 'SketchIdle' }) &&
 | 
			
		||||
              state.can({ type: 'Constrain snap to X' })
 | 
			
		||||
            ),
 | 
			
		||||
          onClick: ({ modelingSend }) =>
 | 
			
		||||
            modelingSend({ type: 'Constrain snap to X' }),
 | 
			
		||||
@ -684,9 +674,8 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
 | 
			
		||||
          id: 'snap-to-y',
 | 
			
		||||
          disabled: (state) =>
 | 
			
		||||
            !(
 | 
			
		||||
              state.matches('Sketch.SketchIdle') &&
 | 
			
		||||
              state.nextEvents.includes('Constrain snap to Y') &&
 | 
			
		||||
              state.can('Constrain snap to Y')
 | 
			
		||||
              state.matches({ Sketch: 'SketchIdle' }) &&
 | 
			
		||||
              state.can({ type: 'Constrain snap to Y' })
 | 
			
		||||
            ),
 | 
			
		||||
          onClick: ({ modelingSend }) =>
 | 
			
		||||
            modelingSend({ type: 'Constrain snap to Y' }),
 | 
			
		||||
@ -700,9 +689,8 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
 | 
			
		||||
          id: 'constraint-remove',
 | 
			
		||||
          disabled: (state) =>
 | 
			
		||||
            !(
 | 
			
		||||
              state.matches('Sketch.SketchIdle') &&
 | 
			
		||||
              state.nextEvents.includes('Constrain remove constraints') &&
 | 
			
		||||
              state.can('Constrain remove constraints')
 | 
			
		||||
              state.matches({ Sketch: 'SketchIdle' }) &&
 | 
			
		||||
              state.can({ type: 'Constrain remove constraints' })
 | 
			
		||||
            ),
 | 
			
		||||
          onClick: ({ modelingSend }) =>
 | 
			
		||||
            modelingSend({ type: 'Constrain remove constraints' }),
 | 
			
		||||
 | 
			
		||||
@ -2,6 +2,7 @@ import { SourceRange } from '../lang/wasm'
 | 
			
		||||
 | 
			
		||||
import { v4 } from 'uuid'
 | 
			
		||||
import { isDesktop } from './isDesktop'
 | 
			
		||||
import { AnyMachineSnapshot } from 'xstate'
 | 
			
		||||
 | 
			
		||||
export const uuidv4 = v4
 | 
			
		||||
 | 
			
		||||
@ -208,3 +209,7 @@ export function isReducedMotion(): boolean {
 | 
			
		||||
export function XOR(bool1: boolean, bool2: boolean): boolean {
 | 
			
		||||
  return (bool1 || bool2) && !(bool1 && bool2)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getActorNextEvents(snapshot: AnyMachineSnapshot) {
 | 
			
		||||
  return [...new Set([...snapshot._nodes.flatMap((sn) => sn.ownEvents)])]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
import { createMachine, assign } from 'xstate'
 | 
			
		||||
import { assign, setup, fromPromise } from 'xstate'
 | 
			
		||||
import { Models } from '@kittycad/lib'
 | 
			
		||||
import withBaseURL from '../lib/withBaseURL'
 | 
			
		||||
import { isDesktop } from 'lib/isDesktop'
 | 
			
		||||
@ -55,79 +55,92 @@ const persistedToken =
 | 
			
		||||
  localStorage?.getItem(TOKEN_PERSIST_KEY) ||
 | 
			
		||||
  ''
 | 
			
		||||
 | 
			
		||||
export const authMachine = createMachine<UserContext, Events>(
 | 
			
		||||
  {
 | 
			
		||||
    /** @xstate-layout N4IgpgJg5mDOIC5QEECuAXAFgOgMabFwGsBJAMwBkB7KGCEgOwGIIqGxsBLBgNyqI75CRALQAbGnRHcA2gAYAuolAAHKrE7pObZSAAeiAIwBmAEzYA7ABYAbAFZTcgBzGbN44adWANCACeiKbGdthypk4AnBFyVs6uQXYAvom+aFh4BMTk1LSQjExgAE6FVIXYKmIAhuhkpQC2GcLikpDSDPJKSCBqGlo6XQYIrk7YETYWctYRxmMWFk6+AUPj2I5OdjZyrnZOFmbJqRg4Ern0zDkABFQYHbo9mtoMuoOGFhHYxlZOhvbOsUGGRaIL4WbBONzWQxWYwWOx2H4HEBpY4tCAAeQwTEuskUd3UD36oEGIlMNlCuzk8Js0TcVisgP8iG2lmcGysb0mW3ByRSIAYVAgcF0yLxvUez0QIms5ImVJpNjpDKWxmw9PGdLh4Te00+iORjSylFRjFFBKeA0QThGQWcexMwWhniBCGiqrepisUVMdlszgieqO2BOdBNXXufXNRKMHtGVuphlJkXs4Wdriso2CCasdgipOidID6WDkAx6FNEYlCAT5jmcjrckMdj2b3GzpsjbBMVMWezDbGPMSQA */
 | 
			
		||||
    id: 'Auth',
 | 
			
		||||
    initial: 'checkIfLoggedIn',
 | 
			
		||||
    states: {
 | 
			
		||||
      checkIfLoggedIn: {
 | 
			
		||||
        id: 'check-if-logged-in',
 | 
			
		||||
        invoke: {
 | 
			
		||||
          src: 'getUser',
 | 
			
		||||
          id: 'check-logged-in',
 | 
			
		||||
          onDone: [
 | 
			
		||||
            {
 | 
			
		||||
              target: 'loggedIn',
 | 
			
		||||
              actions: assign((context, event) => ({
 | 
			
		||||
                user: event.data.user,
 | 
			
		||||
                token: event.data.token || context.token,
 | 
			
		||||
              })),
 | 
			
		||||
            },
 | 
			
		||||
          ],
 | 
			
		||||
          onError: [
 | 
			
		||||
            {
 | 
			
		||||
              target: 'loggedOut',
 | 
			
		||||
              actions: assign({
 | 
			
		||||
                user: () => undefined,
 | 
			
		||||
              }),
 | 
			
		||||
            },
 | 
			
		||||
          ],
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      loggedIn: {
 | 
			
		||||
        entry: ['goToIndexPage'],
 | 
			
		||||
        on: {
 | 
			
		||||
          'Log out': {
 | 
			
		||||
            target: 'loggedOut',
 | 
			
		||||
            actions: () => {
 | 
			
		||||
              if (isDesktop()) writeTokenFile('')
 | 
			
		||||
            },
 | 
			
		||||
export const authMachine = setup({
 | 
			
		||||
  types: {} as {
 | 
			
		||||
    context: UserContext
 | 
			
		||||
    events:
 | 
			
		||||
      | Events
 | 
			
		||||
      | {
 | 
			
		||||
          type: 'xstate.done.actor.check-logged-in'
 | 
			
		||||
          output: {
 | 
			
		||||
            user: Models['User_type']
 | 
			
		||||
            token: string
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
  },
 | 
			
		||||
  actions: {
 | 
			
		||||
    goToIndexPage: () => {},
 | 
			
		||||
    goToSignInPage: () => {},
 | 
			
		||||
  },
 | 
			
		||||
  actors: {
 | 
			
		||||
    getUser: fromPromise(({ input }: { input: { token?: string } }) =>
 | 
			
		||||
      getUser(input)
 | 
			
		||||
    ),
 | 
			
		||||
  },
 | 
			
		||||
}).createMachine({
 | 
			
		||||
  /** @xstate-layout N4IgpgJg5mDOIC5QEECuAXAFgOgMabFwGsBJAMwBkB7KGCEgOwGIIqGxsBLBgNyqI75CRALQAbGnRHcA2gAYAuolAAHKrE7pObZSAAeiAIwBmAEzYA7ABYAbAFZTcgBzGbN44adWANCACeiKbGdthypk4AnBFyVs6uQXYAvom+aFh4BMTk1LSQjExgAE6FVIXYKmIAhuhkpQC2GcLikpDSDPJKSCBqGlo6XQYIrk7YETYWctYRxmMWFk6+AUPj2I5OdjZyrnZOFmbJqRg4Ern0zDkABFQYHbo9mtoMuoOGFhHYxlZOhvbOsUGGRaIL4WbBONzWQxWYwWOx2H4HEBpY4tCAAeQwTEuskUd3UD36oEGIlMNlCuzk8Js0TcVisgP8iG2lmcGysb0mW3ByRSIAYVAgcF0yLxvUez0QIms5ImVJpNjpDKWxmw9PGdLh4Te00+iORjSylFRjFFBKeA0QThGQWcexMwWhniBCGiqrepisUVMdlszgieqO2BOdBNXXufXNRKMHtGVuphlJkXs4Wdriso2CCasdgipOidID6WDkAx6FNEYlCAT5jmcjrckMdj2b3GzpsjbBMVMWezDbGPMSQA */
 | 
			
		||||
  id: 'Auth',
 | 
			
		||||
  initial: 'checkIfLoggedIn',
 | 
			
		||||
  context: {
 | 
			
		||||
    token: persistedToken,
 | 
			
		||||
  },
 | 
			
		||||
  states: {
 | 
			
		||||
    checkIfLoggedIn: {
 | 
			
		||||
      id: 'check-if-logged-in',
 | 
			
		||||
      invoke: {
 | 
			
		||||
        src: 'getUser',
 | 
			
		||||
        input: ({ context }) => ({ token: context.token }),
 | 
			
		||||
        id: 'check-logged-in',
 | 
			
		||||
        onDone: [
 | 
			
		||||
          {
 | 
			
		||||
            target: 'loggedIn',
 | 
			
		||||
            actions: assign(({ context, event }) => ({
 | 
			
		||||
              user: event.output.user,
 | 
			
		||||
              token: event.output.token || context.token,
 | 
			
		||||
            })),
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      loggedOut: {
 | 
			
		||||
        entry: ['goToSignInPage'],
 | 
			
		||||
        on: {
 | 
			
		||||
          'Log in': {
 | 
			
		||||
            target: 'checkIfLoggedIn',
 | 
			
		||||
        ],
 | 
			
		||||
        onError: [
 | 
			
		||||
          {
 | 
			
		||||
            target: 'loggedOut',
 | 
			
		||||
            actions: assign({
 | 
			
		||||
              token: (_, event) => {
 | 
			
		||||
                const token = event.token || ''
 | 
			
		||||
                return token
 | 
			
		||||
              },
 | 
			
		||||
              user: () => undefined,
 | 
			
		||||
            }),
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    loggedIn: {
 | 
			
		||||
      entry: ['goToIndexPage'],
 | 
			
		||||
      on: {
 | 
			
		||||
        'Log out': {
 | 
			
		||||
          target: 'loggedOut',
 | 
			
		||||
          actions: () => {
 | 
			
		||||
            if (isDesktop()) writeTokenFile('')
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    schema: { events: {} as { type: 'Log out' } | { type: 'Log in' } },
 | 
			
		||||
    predictableActionArguments: true,
 | 
			
		||||
    preserveActionOrder: true,
 | 
			
		||||
    context: {
 | 
			
		||||
      token: persistedToken,
 | 
			
		||||
    loggedOut: {
 | 
			
		||||
      entry: ['goToSignInPage'],
 | 
			
		||||
      on: {
 | 
			
		||||
        'Log in': {
 | 
			
		||||
          target: 'checkIfLoggedIn',
 | 
			
		||||
          actions: assign({
 | 
			
		||||
            token: ({ event }) => {
 | 
			
		||||
              const token = event.token || ''
 | 
			
		||||
              return token
 | 
			
		||||
            },
 | 
			
		||||
          }),
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    actions: {},
 | 
			
		||||
    services: { getUser },
 | 
			
		||||
    guards: {},
 | 
			
		||||
    delays: {},
 | 
			
		||||
  }
 | 
			
		||||
)
 | 
			
		||||
  schema: { events: {} as { type: 'Log out' } | { type: 'Log in' } },
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
async function getUser(context: UserContext) {
 | 
			
		||||
  const token = await getAndSyncStoredToken(context)
 | 
			
		||||
async function getUser(input: { token?: string }) {
 | 
			
		||||
  const token = await getAndSyncStoredToken(input)
 | 
			
		||||
  const url = withBaseURL('/user')
 | 
			
		||||
  const headers: { [key: string]: string } = {
 | 
			
		||||
    'Content-Type': 'application/json',
 | 
			
		||||
@ -156,7 +169,7 @@ async function getUser(context: UserContext) {
 | 
			
		||||
      })
 | 
			
		||||
        .then((res) => res.json())
 | 
			
		||||
        .catch((err) => console.error('error from Browser getUser', err))
 | 
			
		||||
    : getUserDesktop(context.token ?? '', VITE_KC_API_BASE_URL)
 | 
			
		||||
    : getUserDesktop(input.token ?? '', VITE_KC_API_BASE_URL)
 | 
			
		||||
 | 
			
		||||
  const user = await userPromise
 | 
			
		||||
 | 
			
		||||
@ -193,13 +206,15 @@ function getCookie(cname: string): string | null {
 | 
			
		||||
  return null
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function getAndSyncStoredToken(context: UserContext): Promise<string> {
 | 
			
		||||
async function getAndSyncStoredToken(input: {
 | 
			
		||||
  token?: string
 | 
			
		||||
}): Promise<string> {
 | 
			
		||||
  // dev mode
 | 
			
		||||
  if (VITE_KC_DEV_TOKEN) return VITE_KC_DEV_TOKEN
 | 
			
		||||
 | 
			
		||||
  const token =
 | 
			
		||||
    context.token && context.token !== ''
 | 
			
		||||
      ? context.token
 | 
			
		||||
    input.token && input.token !== ''
 | 
			
		||||
      ? input.token
 | 
			
		||||
      : getCookie(COOKIE_NAME) || localStorage?.getItem(TOKEN_PERSIST_KEY) || ''
 | 
			
		||||
  if (token) {
 | 
			
		||||
    // has just logged in, update storage
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
import { assign, createMachine } from 'xstate'
 | 
			
		||||
import { assign, fromPromise, setup } from 'xstate'
 | 
			
		||||
import {
 | 
			
		||||
  Command,
 | 
			
		||||
  CommandArgument,
 | 
			
		||||
@ -25,7 +25,7 @@ export type CommandBarMachineEvent =
 | 
			
		||||
      data: { command: Command; argDefaultValues?: { [x: string]: unknown } }
 | 
			
		||||
    }
 | 
			
		||||
  | { type: 'Deselect command' }
 | 
			
		||||
  | { type: 'Submit command'; data: { [x: string]: unknown } }
 | 
			
		||||
  | { type: 'Submit command'; output: { [x: string]: unknown } }
 | 
			
		||||
  | {
 | 
			
		||||
      type: 'Add argument'
 | 
			
		||||
      data: { argument: CommandArgumentWithName<unknown> }
 | 
			
		||||
@ -48,12 +48,16 @@ export type CommandBarMachineEvent =
 | 
			
		||||
    }
 | 
			
		||||
  | { type: 'Submit argument'; data: { [x: string]: unknown } }
 | 
			
		||||
  | {
 | 
			
		||||
      type: 'done.invoke.validateArguments'
 | 
			
		||||
      data: { [x: string]: unknown }
 | 
			
		||||
      type: 'xstate.done.actor.validateSingleArgument'
 | 
			
		||||
      output: { [x: string]: unknown }
 | 
			
		||||
    }
 | 
			
		||||
  | {
 | 
			
		||||
      type: 'error.platform.validateArguments'
 | 
			
		||||
      data: { message: string; arg: CommandArgumentWithName<unknown> }
 | 
			
		||||
      type: 'xstate.done.actor.validateArguments'
 | 
			
		||||
      output: { [x: string]: unknown }
 | 
			
		||||
    }
 | 
			
		||||
  | {
 | 
			
		||||
      type: 'xstate.error.actor.validateArguments'
 | 
			
		||||
      error: { message: string; arg: CommandArgumentWithName<unknown> }
 | 
			
		||||
    }
 | 
			
		||||
  | {
 | 
			
		||||
      type: 'Find and select command'
 | 
			
		||||
@ -68,382 +72,199 @@ export type CommandBarMachineEvent =
 | 
			
		||||
      data: { [x: string]: CommandArgumentWithName<unknown> }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
export const commandBarMachine = createMachine(
 | 
			
		||||
  {
 | 
			
		||||
    /** @xstate-layout N4IgpgJg5mDOIC5QGED2BbdBDAdhABAEJYBOAxMgDaqxgDaADALqKgAONAlgC6eo6sQAD0QBaAJwA6AGwAmAKwBmBoukAWafIAcDcSoA0IAJ6JZDaZIDs8hgzV6AjA61a1DWQF8PhtJlwFiciowUkYWJBAOWB4+AQiRBFF5CwdpcVkHS1lpVyU5QxNEh1lFGTUsrUUtOQd5SwZLLx8MbDwiUkkqGkgyAHk2MBwwwSiY-kEEswZJbPltM0U3eXLZAsRFeQcrerUHRbTFvfkmkF9WgI6u2ggyADFONv98WkowAGNufDeW-2GI0d443iiHESkktUUilkskqdiUWjWCAcDC0kjUqnElkc6lkoK0JzOT0CnWo1zIAEEIARvn48LA-uwuIC4qAEqIHJjJPJcYtxFoYdJFFjVsZEG5pqCquJxJoGvJxOUCT82sSrj0AEpgdCoABuYC+yog9OYIyZsQmYmhWzMmTqLnU0kyikRGVRbj5lg2SmUam5StpFxIkgAymBXh8HlADQGyKHw58aecGZEzUDWYgchY7CisWoSlpQQ5EVDLJJxKkdIpyypUop-ed2kHCW0Xu9uD1kwDzcCEJCtlUNgWhZZ1OlnaKEBpZJItHVzDZ5xlpPWiZdDc8w22O7JwozosyLUj3KiZZY85YtMUNgx5Ii81JuS5xBtPVCCyuVR0AOJYbgACzAEhI3wUgoAAV3QQZuFgCg-1wGAvjAkgSCgkCSHAyCcG4TtUxZYRED2TQZGSOw80qaQ7ARCc9mmZYSjPPFpDSQUP0DSQf3-QDgNAiCoJggAROBNw+aMkxNf5cMPHJSjPWRdhvZ9LGcF0b0kVQ8ycDRNkvNRWMbdjfwAoCcCjHjMOgyRyQAdywGITPwB42DA7hYzAgAjdAeDQjCoJw-du3TJE6mnWwoT0NIUTUAs71sMtdGsRYCyUtI9OJDijO49DeKw2BJAANSwShOAgX9IzICB+DASQHh1VAAGsqp1Qrit-MBySy8y-LGPCElSeoy2lZJcwcNRfXHQpBWmWQsRsPYdDPZJUu-QyuPssy+Py5qSt4EyyEAkhUCDNhKF-AAzQ70EkJqiu2tqOt88S926w9UhhLlykWXFHXcTJixsd60hHGxNj2Jag01HVODAKzXI8rzE1+R6U38tN8IQJSuR0OZ0gvHQXHGxBPWmUdppUWwFV0MHJAhqGYcpAh1qwrqDx7ZFajUmVpulEbqlqREsfBTQ0jcPM5P5KmaehshNW1PVvOy7Cka7VHeoYDkyjSPQtAvPMqkRQU1BnX1+UydFkQvCWwEhqWAFEIC8xnFd3ZHntZuTDZG4ob1nGxPTUfXrBnS8nDmEpT2ObxTnXVUALeOrgPanycvKyrqpwWqGqurbWsThXjWd5WesQZYtmBkplCceplIncK0SrXYqnccxxcj5s2OQWP4-s3PzJgiqcCqmr6sa7P2x7vi6AcAvJJ7XEy0dUFMWsFxlnKfn+S5CL3E9Jiuapjv3i7qNx+T-bDskY6zourObpz+6cuZgK0Y5Ua0TqEvXDkJR-YnR0s1xjkIMnDln3uuVsHwOxT1NCjIuR5nCSBUExJi1huRqyooUTYpYnzeyUFkKsy4Tg4FQBAOAgg26Nmga7QKohshSBtNYXGDonQumtPYaQfsSjLBHDefepJICUJZtQ4oMx8wXj6jebGt4JwbC2OiVwig9hh2hLpVu0cOhxjbMBBGeABFPwSA3ERRMlhKWCn9WRVQzZQirMo0BAYNzxn4RJGBh5MRbGXsHawGQFBmLrpUasOQorOAjs0OxaUVrGVMvfaCuiVYEV2KWTYg1yxyA0i6ZYaIPSL3lOkS8wSo6hOWpxCJ8te6WRsnZKMjlnIxNgSNIiiTF6vVSdI9JM1taXlxLzTwqiClBnSqtSJScLIFVvjtKANSXquENqoJwtgyZzRFIUQ4MhqgNACdNTQCpLbWyshMnsjpSwjXRJYHecgcmImsLIz0odNAaGqPiHpDYY6HwTlE+ATiqHPwohYewWQshmGhHrCcJz5Bck5toZwGhMheC8EAA */
 | 
			
		||||
    predictableActionArguments: true,
 | 
			
		||||
    tsTypes: {} as import('./commandBarMachine.typegen').Typegen0,
 | 
			
		||||
    context: {
 | 
			
		||||
      commands: [],
 | 
			
		||||
      selectedCommand: undefined,
 | 
			
		||||
      currentArgument: undefined,
 | 
			
		||||
      selectionRanges: {
 | 
			
		||||
        otherSelections: [],
 | 
			
		||||
        codeBasedSelections: [],
 | 
			
		||||
      },
 | 
			
		||||
      argumentsToSubmit: {},
 | 
			
		||||
    } as CommandBarContext,
 | 
			
		||||
    id: 'Command Bar',
 | 
			
		||||
    initial: 'Closed',
 | 
			
		||||
    states: {
 | 
			
		||||
      Closed: {
 | 
			
		||||
        on: {
 | 
			
		||||
          Open: {
 | 
			
		||||
            target: 'Selecting command',
 | 
			
		||||
          },
 | 
			
		||||
 | 
			
		||||
          'Find and select command': {
 | 
			
		||||
            target: 'Command selected',
 | 
			
		||||
            actions: [
 | 
			
		||||
              'Find and select command',
 | 
			
		||||
              'Initialize arguments to submit',
 | 
			
		||||
            ],
 | 
			
		||||
          },
 | 
			
		||||
 | 
			
		||||
          'Add commands': {
 | 
			
		||||
            target: 'Closed',
 | 
			
		||||
 | 
			
		||||
            actions: [
 | 
			
		||||
              assign({
 | 
			
		||||
                commands: (context, event) =>
 | 
			
		||||
                  [...context.commands, ...event.data.commands].sort(
 | 
			
		||||
                    sortCommands
 | 
			
		||||
                  ),
 | 
			
		||||
              }),
 | 
			
		||||
            ],
 | 
			
		||||
 | 
			
		||||
            internal: true,
 | 
			
		||||
          },
 | 
			
		||||
 | 
			
		||||
          'Remove commands': {
 | 
			
		||||
            target: 'Closed',
 | 
			
		||||
 | 
			
		||||
            actions: [
 | 
			
		||||
              assign({
 | 
			
		||||
                commands: (context, event) =>
 | 
			
		||||
                  context.commands.filter(
 | 
			
		||||
                    (c) =>
 | 
			
		||||
                      !event.data.commands.some(
 | 
			
		||||
                        (c2) => c2.name === c.name && c2.groupId === c.groupId
 | 
			
		||||
                      )
 | 
			
		||||
                  ),
 | 
			
		||||
              }),
 | 
			
		||||
            ],
 | 
			
		||||
 | 
			
		||||
            internal: true,
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
 | 
			
		||||
      'Selecting command': {
 | 
			
		||||
        on: {
 | 
			
		||||
          'Select command': {
 | 
			
		||||
            target: 'Command selected',
 | 
			
		||||
            actions: ['Set selected command', 'Initialize arguments to submit'],
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
 | 
			
		||||
      'Command selected': {
 | 
			
		||||
        always: [
 | 
			
		||||
          {
 | 
			
		||||
            target: 'Closed',
 | 
			
		||||
            cond: 'Command has no arguments',
 | 
			
		||||
            actions: ['Execute command'],
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            target: 'Checking Arguments',
 | 
			
		||||
            cond: 'All arguments are skippable',
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            target: 'Gathering arguments',
 | 
			
		||||
            actions: ['Set current argument to first non-skippable'],
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
 | 
			
		||||
      'Gathering arguments': {
 | 
			
		||||
        states: {
 | 
			
		||||
          'Awaiting input': {
 | 
			
		||||
            on: {
 | 
			
		||||
              'Submit argument': {
 | 
			
		||||
                target: 'Validating',
 | 
			
		||||
              },
 | 
			
		||||
            },
 | 
			
		||||
          },
 | 
			
		||||
 | 
			
		||||
          Validating: {
 | 
			
		||||
            invoke: {
 | 
			
		||||
              src: 'Validate argument',
 | 
			
		||||
              id: 'validateArgument',
 | 
			
		||||
              onDone: {
 | 
			
		||||
                target: '#Command Bar.Checking Arguments',
 | 
			
		||||
                actions: [
 | 
			
		||||
                  assign({
 | 
			
		||||
                    argumentsToSubmit: (context, event) => {
 | 
			
		||||
                      const [argName, argData] = Object.entries(event.data)[0]
 | 
			
		||||
                      const { currentArgument } = context
 | 
			
		||||
                      if (!currentArgument) return {}
 | 
			
		||||
                      return {
 | 
			
		||||
                        ...context.argumentsToSubmit,
 | 
			
		||||
                        [argName]: argData,
 | 
			
		||||
                      }
 | 
			
		||||
                    },
 | 
			
		||||
                  }),
 | 
			
		||||
                ],
 | 
			
		||||
              },
 | 
			
		||||
              onError: [
 | 
			
		||||
                {
 | 
			
		||||
                  target: 'Awaiting input',
 | 
			
		||||
                },
 | 
			
		||||
              ],
 | 
			
		||||
            },
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        initial: 'Awaiting input',
 | 
			
		||||
 | 
			
		||||
        on: {
 | 
			
		||||
          'Change current argument': {
 | 
			
		||||
            target: 'Gathering arguments',
 | 
			
		||||
            internal: true,
 | 
			
		||||
            actions: ['Set current argument'],
 | 
			
		||||
          },
 | 
			
		||||
 | 
			
		||||
          'Deselect command': {
 | 
			
		||||
            target: 'Selecting command',
 | 
			
		||||
            actions: [
 | 
			
		||||
              assign({
 | 
			
		||||
                selectedCommand: (_c, _e) => undefined,
 | 
			
		||||
              }),
 | 
			
		||||
            ],
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
 | 
			
		||||
      Review: {
 | 
			
		||||
        entry: ['Clear current argument'],
 | 
			
		||||
        on: {
 | 
			
		||||
          'Submit command': {
 | 
			
		||||
            target: 'Closed',
 | 
			
		||||
            actions: ['Execute command'],
 | 
			
		||||
          },
 | 
			
		||||
 | 
			
		||||
          'Add argument': {
 | 
			
		||||
            target: 'Gathering arguments',
 | 
			
		||||
            actions: ['Set current argument'],
 | 
			
		||||
          },
 | 
			
		||||
 | 
			
		||||
          'Remove argument': {
 | 
			
		||||
            target: 'Review',
 | 
			
		||||
            actions: ['Remove argument'],
 | 
			
		||||
          },
 | 
			
		||||
 | 
			
		||||
          'Edit argument': {
 | 
			
		||||
            target: 'Gathering arguments',
 | 
			
		||||
            actions: ['Set current argument'],
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
 | 
			
		||||
      'Checking Arguments': {
 | 
			
		||||
        invoke: {
 | 
			
		||||
          src: 'Validate all arguments',
 | 
			
		||||
          id: 'validateArguments',
 | 
			
		||||
          onDone: [
 | 
			
		||||
            {
 | 
			
		||||
              target: 'Review',
 | 
			
		||||
              cond: 'Command needs review',
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
              target: 'Closed',
 | 
			
		||||
              actions: 'Execute command',
 | 
			
		||||
            },
 | 
			
		||||
          ],
 | 
			
		||||
          onError: [
 | 
			
		||||
            {
 | 
			
		||||
              target: 'Gathering arguments',
 | 
			
		||||
              actions: ['Set current argument to first non-skippable'],
 | 
			
		||||
            },
 | 
			
		||||
          ],
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    on: {
 | 
			
		||||
      Close: {
 | 
			
		||||
        target: '.Closed',
 | 
			
		||||
      },
 | 
			
		||||
 | 
			
		||||
      Clear: {
 | 
			
		||||
        target: '#Command Bar',
 | 
			
		||||
        internal: true,
 | 
			
		||||
        actions: ['Clear argument data'],
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    schema: {
 | 
			
		||||
      events: {} as CommandBarMachineEvent,
 | 
			
		||||
    },
 | 
			
		||||
    preserveActionOrder: true,
 | 
			
		||||
export const commandBarMachine = setup({
 | 
			
		||||
  types: {
 | 
			
		||||
    context: {} as CommandBarContext,
 | 
			
		||||
    events: {} as CommandBarMachineEvent,
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    actions: {
 | 
			
		||||
      'Execute command': (context, event) => {
 | 
			
		||||
        const { selectedCommand } = context
 | 
			
		||||
        if (!selectedCommand) return
 | 
			
		||||
        if (
 | 
			
		||||
          (selectedCommand?.args && event.type === 'Submit command') ||
 | 
			
		||||
          event.type === 'done.invoke.validateArguments'
 | 
			
		||||
        ) {
 | 
			
		||||
          const resolvedArgs = {} as { [x: string]: unknown }
 | 
			
		||||
          for (const [argName, argValue] of Object.entries(
 | 
			
		||||
            getCommandArgumentKclValuesOnly(event.data)
 | 
			
		||||
          )) {
 | 
			
		||||
            resolvedArgs[argName] =
 | 
			
		||||
              typeof argValue === 'function' ? argValue(context) : argValue
 | 
			
		||||
          }
 | 
			
		||||
          selectedCommand?.onSubmit(resolvedArgs)
 | 
			
		||||
        } else {
 | 
			
		||||
          selectedCommand?.onSubmit()
 | 
			
		||||
  actions: {
 | 
			
		||||
    enqueueValidArgsToSubmit: assign({
 | 
			
		||||
      argumentsToSubmit: ({ context, event }) => {
 | 
			
		||||
        if (event.type !== 'xstate.done.actor.validateSingleArgument') return {}
 | 
			
		||||
        const [argName, argData] = Object.entries(event.output)[0]
 | 
			
		||||
        const { currentArgument } = context
 | 
			
		||||
        if (!currentArgument) return {}
 | 
			
		||||
        return {
 | 
			
		||||
          ...context.argumentsToSubmit,
 | 
			
		||||
          [argName]: argData,
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      'Set current argument to first non-skippable': assign({
 | 
			
		||||
        currentArgument: (context, event) => {
 | 
			
		||||
          const { selectedCommand } = context
 | 
			
		||||
          if (!(selectedCommand && selectedCommand.args)) return undefined
 | 
			
		||||
          const rejectedArg = 'data' in event && event.data.arg
 | 
			
		||||
    }),
 | 
			
		||||
    'Execute command': ({ context, event }) => {
 | 
			
		||||
      const { selectedCommand } = context
 | 
			
		||||
      if (!selectedCommand) return
 | 
			
		||||
      if (
 | 
			
		||||
        (selectedCommand?.args && event.type === 'Submit command') ||
 | 
			
		||||
        event.type === 'xstate.done.actor.validateArguments'
 | 
			
		||||
      ) {
 | 
			
		||||
        const resolvedArgs = {} as { [x: string]: unknown }
 | 
			
		||||
        for (const [argName, argValue] of Object.entries(
 | 
			
		||||
          getCommandArgumentKclValuesOnly(event.output)
 | 
			
		||||
        )) {
 | 
			
		||||
          resolvedArgs[argName] =
 | 
			
		||||
            typeof argValue === 'function' ? argValue(context) : argValue
 | 
			
		||||
        }
 | 
			
		||||
        selectedCommand?.onSubmit(resolvedArgs)
 | 
			
		||||
      } else {
 | 
			
		||||
        selectedCommand?.onSubmit()
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    'Set current argument to first non-skippable': assign({
 | 
			
		||||
      currentArgument: ({ context, event }) => {
 | 
			
		||||
        const { selectedCommand } = context
 | 
			
		||||
        if (!(selectedCommand && selectedCommand.args)) return undefined
 | 
			
		||||
        const rejectedArg =
 | 
			
		||||
          'data' in event && 'arg' in event.data && event.data.arg
 | 
			
		||||
 | 
			
		||||
          // Find the first argument that is not to be skipped:
 | 
			
		||||
          // that is, the first argument that is not already in the argumentsToSubmit
 | 
			
		||||
          // or that is not undefined, or that is not marked as "skippable".
 | 
			
		||||
          // TODO validate the type of the existing arguments
 | 
			
		||||
          let argIndex = 0
 | 
			
		||||
        // Find the first argument that is not to be skipped:
 | 
			
		||||
        // that is, the first argument that is not already in the argumentsToSubmit
 | 
			
		||||
        // or that is not undefined, or that is not marked as "skippable".
 | 
			
		||||
        // TODO validate the type of the existing arguments
 | 
			
		||||
        let argIndex = 0
 | 
			
		||||
 | 
			
		||||
          while (argIndex < Object.keys(selectedCommand.args).length) {
 | 
			
		||||
            const [argName, argConfig] = Object.entries(selectedCommand.args)[
 | 
			
		||||
              argIndex
 | 
			
		||||
            ]
 | 
			
		||||
            const argIsRequired =
 | 
			
		||||
              typeof argConfig.required === 'function'
 | 
			
		||||
                ? argConfig.required(context)
 | 
			
		||||
                : argConfig.required
 | 
			
		||||
            const mustNotSkipArg =
 | 
			
		||||
              argIsRequired &&
 | 
			
		||||
              (!context.argumentsToSubmit.hasOwnProperty(argName) ||
 | 
			
		||||
                context.argumentsToSubmit[argName] === undefined ||
 | 
			
		||||
                (rejectedArg && rejectedArg.name === argName))
 | 
			
		||||
        while (argIndex < Object.keys(selectedCommand.args).length) {
 | 
			
		||||
          const [argName, argConfig] = Object.entries(selectedCommand.args)[
 | 
			
		||||
            argIndex
 | 
			
		||||
          ]
 | 
			
		||||
          const argIsRequired =
 | 
			
		||||
            typeof argConfig.required === 'function'
 | 
			
		||||
              ? argConfig.required(context)
 | 
			
		||||
              : argConfig.required
 | 
			
		||||
          const mustNotSkipArg =
 | 
			
		||||
            argIsRequired &&
 | 
			
		||||
            (!context.argumentsToSubmit.hasOwnProperty(argName) ||
 | 
			
		||||
              context.argumentsToSubmit[argName] === undefined ||
 | 
			
		||||
              (rejectedArg &&
 | 
			
		||||
                typeof rejectedArg === 'object' &&
 | 
			
		||||
                'name' in rejectedArg &&
 | 
			
		||||
                rejectedArg.name === argName))
 | 
			
		||||
 | 
			
		||||
            if (
 | 
			
		||||
              mustNotSkipArg === true ||
 | 
			
		||||
              argIndex + 1 === Object.keys(selectedCommand.args).length
 | 
			
		||||
            ) {
 | 
			
		||||
              // If we have reached the end of the arguments and none are skippable,
 | 
			
		||||
              // return the last argument.
 | 
			
		||||
              return {
 | 
			
		||||
                ...selectedCommand.args[argName],
 | 
			
		||||
                name: argName,
 | 
			
		||||
              }
 | 
			
		||||
          if (
 | 
			
		||||
            mustNotSkipArg === true ||
 | 
			
		||||
            argIndex + 1 === Object.keys(selectedCommand.args).length
 | 
			
		||||
          ) {
 | 
			
		||||
            // If we have reached the end of the arguments and none are skippable,
 | 
			
		||||
            // return the last argument.
 | 
			
		||||
            return {
 | 
			
		||||
              ...selectedCommand.args[argName],
 | 
			
		||||
              name: argName,
 | 
			
		||||
            }
 | 
			
		||||
            argIndex++
 | 
			
		||||
          }
 | 
			
		||||
          argIndex++
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
          // TODO: use an XState service to continue onto review step
 | 
			
		||||
          // if all arguments are skippable and contain values.
 | 
			
		||||
          return undefined
 | 
			
		||||
        },
 | 
			
		||||
      }),
 | 
			
		||||
      'Clear current argument': assign({
 | 
			
		||||
        currentArgument: undefined,
 | 
			
		||||
      }),
 | 
			
		||||
      'Remove argument': assign({
 | 
			
		||||
        argumentsToSubmit: (context, event) => {
 | 
			
		||||
          if (event.type !== 'Remove argument') return context.argumentsToSubmit
 | 
			
		||||
          const argToRemove = Object.values(event.data)[0]
 | 
			
		||||
          // Extract all but the argument to remove and return it
 | 
			
		||||
          const { [argToRemove.name]: _, ...rest } = context.argumentsToSubmit
 | 
			
		||||
          return rest
 | 
			
		||||
        },
 | 
			
		||||
      }),
 | 
			
		||||
      'Set current argument': assign({
 | 
			
		||||
        currentArgument: (context, event) => {
 | 
			
		||||
          switch (event.type) {
 | 
			
		||||
            case 'Edit argument':
 | 
			
		||||
              return event.data.arg
 | 
			
		||||
            case 'Change current argument':
 | 
			
		||||
              return Object.values(event.data)[0]
 | 
			
		||||
            default:
 | 
			
		||||
              return context.currentArgument
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
      }),
 | 
			
		||||
      'Clear argument data': assign({
 | 
			
		||||
        selectedCommand: undefined,
 | 
			
		||||
        currentArgument: undefined,
 | 
			
		||||
        argumentsToSubmit: {},
 | 
			
		||||
      }),
 | 
			
		||||
      'Set selected command': assign({
 | 
			
		||||
        selectedCommand: (c, e) =>
 | 
			
		||||
          e.type === 'Select command' ? e.data.command : c.selectedCommand,
 | 
			
		||||
      }),
 | 
			
		||||
      'Find and select command': assign({
 | 
			
		||||
        selectedCommand: (c, e) => {
 | 
			
		||||
          if (e.type !== 'Find and select command') return c.selectedCommand
 | 
			
		||||
          const found = c.commands.find(
 | 
			
		||||
            (cmd) => cmd.name === e.data.name && cmd.groupId === e.data.groupId
 | 
			
		||||
          )
 | 
			
		||||
 | 
			
		||||
          return !!found ? found : c.selectedCommand
 | 
			
		||||
        },
 | 
			
		||||
      }),
 | 
			
		||||
      'Initialize arguments to submit': assign({
 | 
			
		||||
        argumentsToSubmit: (c, e) => {
 | 
			
		||||
          const command =
 | 
			
		||||
            'command' in e.data ? e.data.command : c.selectedCommand
 | 
			
		||||
          if (!command?.args) return {}
 | 
			
		||||
          const args: { [x: string]: unknown } = {}
 | 
			
		||||
          for (const [argName, arg] of Object.entries(command.args)) {
 | 
			
		||||
            args[argName] =
 | 
			
		||||
              e.data.argDefaultValues && argName in e.data.argDefaultValues
 | 
			
		||||
                ? e.data.argDefaultValues[argName]
 | 
			
		||||
                : arg.skip && 'defaultValue' in arg
 | 
			
		||||
                ? arg.defaultValue
 | 
			
		||||
                : undefined
 | 
			
		||||
          }
 | 
			
		||||
          return args
 | 
			
		||||
        },
 | 
			
		||||
      }),
 | 
			
		||||
    },
 | 
			
		||||
    guards: {
 | 
			
		||||
      'Command needs review': (context, _) =>
 | 
			
		||||
        context.selectedCommand?.needsReview || false,
 | 
			
		||||
    },
 | 
			
		||||
    services: {
 | 
			
		||||
      'Validate argument': (context, event) => {
 | 
			
		||||
        if (event.type !== 'Submit argument') return Promise.reject()
 | 
			
		||||
        return new Promise((resolve, reject) => {
 | 
			
		||||
          // TODO: figure out if we should validate argument data here or in the form itself,
 | 
			
		||||
          // and if we should support people configuring a argument's validation function
 | 
			
		||||
 | 
			
		||||
          resolve(event.data)
 | 
			
		||||
        })
 | 
			
		||||
        // TODO: use an XState service to continue onto review step
 | 
			
		||||
        // if all arguments are skippable and contain values.
 | 
			
		||||
        return undefined
 | 
			
		||||
      },
 | 
			
		||||
      'Validate all arguments': (context, _) => {
 | 
			
		||||
    }),
 | 
			
		||||
    'Clear current argument': assign({
 | 
			
		||||
      currentArgument: undefined,
 | 
			
		||||
    }),
 | 
			
		||||
    'Remove argument': assign({
 | 
			
		||||
      argumentsToSubmit: ({ context, event }) => {
 | 
			
		||||
        if (event.type !== 'Remove argument') return context.argumentsToSubmit
 | 
			
		||||
        const argToRemove = Object.values(event.data)[0]
 | 
			
		||||
        // Extract all but the argument to remove and return it
 | 
			
		||||
        const { [argToRemove.name]: _, ...rest } = context.argumentsToSubmit
 | 
			
		||||
        return rest
 | 
			
		||||
      },
 | 
			
		||||
    }),
 | 
			
		||||
    'Set current argument': assign({
 | 
			
		||||
      currentArgument: ({ context, event }) => {
 | 
			
		||||
        switch (event.type) {
 | 
			
		||||
          case 'Edit argument':
 | 
			
		||||
            return event.data.arg
 | 
			
		||||
          case 'Change current argument':
 | 
			
		||||
            return Object.values(event.data)[0]
 | 
			
		||||
          default:
 | 
			
		||||
            return context.currentArgument
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
    }),
 | 
			
		||||
    'Clear argument data': assign({
 | 
			
		||||
      selectedCommand: undefined,
 | 
			
		||||
      currentArgument: undefined,
 | 
			
		||||
      argumentsToSubmit: {},
 | 
			
		||||
    }),
 | 
			
		||||
    'Set selected command': assign({
 | 
			
		||||
      selectedCommand: ({ context, event }) =>
 | 
			
		||||
        event.type === 'Select command'
 | 
			
		||||
          ? event.data.command
 | 
			
		||||
          : context.selectedCommand,
 | 
			
		||||
    }),
 | 
			
		||||
    'Find and select command': assign({
 | 
			
		||||
      selectedCommand: ({ context, event }) => {
 | 
			
		||||
        if (event.type !== 'Find and select command')
 | 
			
		||||
          return context.selectedCommand
 | 
			
		||||
        const found = context.commands.find(
 | 
			
		||||
          (cmd) =>
 | 
			
		||||
            cmd.name === event.data.name && cmd.groupId === event.data.groupId
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        return !!found ? found : context.selectedCommand
 | 
			
		||||
      },
 | 
			
		||||
    }),
 | 
			
		||||
    'Initialize arguments to submit': assign({
 | 
			
		||||
      argumentsToSubmit: ({ context, event }) => {
 | 
			
		||||
        if (
 | 
			
		||||
          event.type !== 'Select command' &&
 | 
			
		||||
          event.type !== 'Find and select command'
 | 
			
		||||
        )
 | 
			
		||||
          return {}
 | 
			
		||||
        const command =
 | 
			
		||||
          'data' in event && 'command' in event.data
 | 
			
		||||
            ? event.data.command
 | 
			
		||||
            : context.selectedCommand
 | 
			
		||||
        if (!command?.args) return {}
 | 
			
		||||
        const args: { [x: string]: unknown } = {}
 | 
			
		||||
        for (const [argName, arg] of Object.entries(command.args)) {
 | 
			
		||||
          args[argName] =
 | 
			
		||||
            event.data.argDefaultValues &&
 | 
			
		||||
            argName in event.data.argDefaultValues
 | 
			
		||||
              ? event.data.argDefaultValues[argName]
 | 
			
		||||
              : arg.skip && 'defaultValue' in arg
 | 
			
		||||
              ? arg.defaultValue
 | 
			
		||||
              : undefined
 | 
			
		||||
        }
 | 
			
		||||
        return args
 | 
			
		||||
      },
 | 
			
		||||
    }),
 | 
			
		||||
  },
 | 
			
		||||
  guards: {
 | 
			
		||||
    'Command needs review': ({ context }) =>
 | 
			
		||||
      context.selectedCommand?.needsReview || false,
 | 
			
		||||
    'Command has no arguments': () => false,
 | 
			
		||||
    'All arguments are skippable': () => false,
 | 
			
		||||
  },
 | 
			
		||||
  actors: {
 | 
			
		||||
    'Validate argument': fromPromise(({ input }) => {
 | 
			
		||||
      return new Promise((resolve, reject) => {
 | 
			
		||||
        // TODO: figure out if we should validate argument data here or in the form itself,
 | 
			
		||||
        // and if we should support people configuring a argument's validation function
 | 
			
		||||
 | 
			
		||||
        resolve(input)
 | 
			
		||||
      })
 | 
			
		||||
    }),
 | 
			
		||||
    'Validate all arguments': fromPromise(
 | 
			
		||||
      ({ input }: { input: CommandBarContext }) => {
 | 
			
		||||
        return new Promise((resolve, reject) => {
 | 
			
		||||
          for (const [argName, argConfig] of Object.entries(
 | 
			
		||||
            context.selectedCommand!.args!
 | 
			
		||||
            input.selectedCommand!.args!
 | 
			
		||||
          )) {
 | 
			
		||||
            let arg = context.argumentsToSubmit[argName]
 | 
			
		||||
            let argValue = typeof arg === 'function' ? arg(context) : arg
 | 
			
		||||
            let arg = input.argumentsToSubmit[argName]
 | 
			
		||||
            let argValue = typeof arg === 'function' ? arg(input) : arg
 | 
			
		||||
 | 
			
		||||
            try {
 | 
			
		||||
              const isRequired =
 | 
			
		||||
                typeof argConfig.required === 'function'
 | 
			
		||||
                  ? argConfig.required(context)
 | 
			
		||||
                  ? argConfig.required(input)
 | 
			
		||||
                  : argConfig.required
 | 
			
		||||
 | 
			
		||||
              const resolvedDefaultValue =
 | 
			
		||||
                'defaultValue' in argConfig
 | 
			
		||||
                  ? typeof argConfig.defaultValue === 'function'
 | 
			
		||||
                    ? argConfig.defaultValue(context)
 | 
			
		||||
                    ? argConfig.defaultValue(input)
 | 
			
		||||
                    : argConfig.defaultValue
 | 
			
		||||
                  : undefined
 | 
			
		||||
 | 
			
		||||
@ -461,7 +282,7 @@ export const commandBarMachine = createMachine(
 | 
			
		||||
                !(
 | 
			
		||||
                  typeof argConfig.options === 'function'
 | 
			
		||||
                    ? argConfig.options(
 | 
			
		||||
                        context,
 | 
			
		||||
                        input,
 | 
			
		||||
                        argConfig.machineActor.getSnapshot().context
 | 
			
		||||
                      )
 | 
			
		||||
                    : argConfig.options
 | 
			
		||||
@ -502,13 +323,214 @@ export const commandBarMachine = createMachine(
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          return resolve(context.argumentsToSubmit)
 | 
			
		||||
          return resolve(input.argumentsToSubmit)
 | 
			
		||||
        })
 | 
			
		||||
      }
 | 
			
		||||
    ),
 | 
			
		||||
  },
 | 
			
		||||
}).createMachine({
 | 
			
		||||
  /** @xstate-layout N4IgpgJg5mDOIC5QGED2BbdBDAdhABAEJYBOAxMgDaqxgDaADALqKgAONAlgC6eo6sQAD0QBaAJwA6AGwAmAKwBmBoukAWafIAcDcSoA0IAJ6JZDaZIDs8hgzV6AjA61a1DWQF8PhtJlwFiciowUkYWJBAOWB4+AQiRBFF5CwdpcVkHS1lpVyU5QxNEh1lFGTUsrUUtOQd5SwZLLx8MbDwiUkkqGkgyAHk2MBwwwSiY-kEEswZJbPltM0U3eXLZAsRFeQcrerUHRbTFvfkmkF9WgI6u2ggyADFONv98WkowAGNufDeW-2GI0d443iiHESkktUUilkskqdiUWjWCAcDC0kjUqnElkc6lkoK0JzOT0CnWo1zIAEEIARvn48LA-uwuIC4qAEqIHJjJPJcYtxFoYdJFFjVsZEG5pqCquJxJoGvJxOUCT82sSrj0AEpgdCoABuYC+yog9OYIyZsQmYmhWzMmTqLnU0kyikRGVRbj5lg2SmUam5StpFxIkgAymBXh8HlADQGyKHw58aecGZEzUDWYgchY7CisWoSlpQQ5EVDLJJxKkdIpyypUop-ed2kHCW0Xu9uD1kwDzcCEJCtlUNgWhZZ1OlnaKEBpZJItHVzDZ5xlpPWiZdDc8w22O7JwozosyLUj3KiZZY85YtMUNgx5Ii81JuS5xBtPVCCyuVR0AOJYbgACzAEhI3wUgoAAV3QQZuFgCg-1wGAvjAkgSCgkCSHAyCcG4TtUxZYRED2TQZGSOw80qaQ7ARCc9mmZYSjPPFpDSQUP0DSQf3-QDgNAiCoJggAROBNw+aMkxNf5cMPHJSjPWRdhvZ9LGcF0b0kVQ8ycDRNkvNRWMbdjfwAoCcCjHjMOgyRyQAdywGITPwB42DA7hYzAgAjdAeDQjCoJw-du3TJE6mnWwoT0NIUTUAs71sMtdGsRYCyUtI9OJDijO49DeKw2BJAANSwShOAgX9IzICB+DASQHh1VAAGsqp1Qrit-MBySy8y-LGPCElSeoy2lZJcwcNRfXHQpBWmWQsRsPYdDPZJUu-QyuPssy+Py5qSt4EyyEAkhUCDNhKF-AAzQ70EkJqiu2tqOt88S926w9UhhLlykWXFHXcTJixsd60hHGxNj2Jag01HVODAKzXI8rzE1+R6U38tN8IQJSuR0OZ0gvHQXHGxBPWmUdppUWwFV0MHJAhqGYcpAh1qwrqDx7ZFajUmVpulEbqlqREsfBTQ0jcPM5P5KmaehshNW1PVvOy7Cka7VHeoYDkyjSPQtAvPMqkRQU1BnX1+UydFkQvCWwEhqWAFEIC8xnFd3ZHntZuTDZG4ob1nGxPTUfXrBnS8nDmEpT2ObxTnXVUALeOrgPanycvKyrqpwWqGqurbWsThXjWd5WesQZYtmBkplCceplIncK0SrXYqnccxxcj5s2OQWP4-s3PzJgiqcCqmr6sa7P2x7vi6AcAvJJ7XEy0dUFMWsFxlnKfn+S5CL3E9Jiuapjv3i7qNx+T-bDskY6zourObpz+6cuZgK0Y5Ua0TqEvXDkJR-YnR0s1xjkIMnDln3uuVsHwOxT1NCjIuR5nCSBUExJi1huRqyooUTYpYnzeyUFkKsy4Tg4FQBAOAgg26Nmga7QKohshSBtNYXGDonQumtPYaQfsSjLBHDefepJICUJZtQ4oMx8wXj6jebGt4JwbC2OiVwig9hh2hLpVu0cOhxjbMBBGeABFPwSA3ERRMlhKWCn9WRVQzZQirMo0BAYNzxn4RJGBh5MRbGXsHawGQFBmLrpUasOQorOAjs0OxaUVrGVMvfaCuiVYEV2KWTYg1yxyA0i6ZYaIPSL3lOkS8wSo6hOWpxCJ8te6WRsnZKMjlnIxNgSNIiiTF6vVSdI9JM1taXlxLzTwqiClBnSqtSJScLIFVvjtKANSXquENqoJwtgyZzRFIUQ4MhqgNACdNTQCpLbWyshMnsjpSwjXRJYHecgcmImsLIz0odNAaGqPiHpDYY6HwTlE+ATiqHPwohYewWQshmGhHrCcJz5Bck5toZwGhMheC8EAA */
 | 
			
		||||
  context: {
 | 
			
		||||
    commands: [],
 | 
			
		||||
    selectedCommand: undefined,
 | 
			
		||||
    currentArgument: undefined,
 | 
			
		||||
    selectionRanges: {
 | 
			
		||||
      otherSelections: [],
 | 
			
		||||
      codeBasedSelections: [],
 | 
			
		||||
    },
 | 
			
		||||
    argumentsToSubmit: {},
 | 
			
		||||
  },
 | 
			
		||||
  id: 'Command Bar',
 | 
			
		||||
  initial: 'Closed',
 | 
			
		||||
  states: {
 | 
			
		||||
    Closed: {
 | 
			
		||||
      on: {
 | 
			
		||||
        Open: {
 | 
			
		||||
          target: 'Selecting command',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        'Find and select command': {
 | 
			
		||||
          target: 'Command selected',
 | 
			
		||||
          actions: [
 | 
			
		||||
            'Find and select command',
 | 
			
		||||
            'Initialize arguments to submit',
 | 
			
		||||
          ],
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        'Add commands': {
 | 
			
		||||
          target: 'Closed',
 | 
			
		||||
 | 
			
		||||
          actions: [
 | 
			
		||||
            assign({
 | 
			
		||||
              commands: ({ context, event }) =>
 | 
			
		||||
                [...context.commands, ...event.data.commands].sort(
 | 
			
		||||
                  sortCommands
 | 
			
		||||
                ),
 | 
			
		||||
            }),
 | 
			
		||||
          ],
 | 
			
		||||
 | 
			
		||||
          reenter: false,
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        'Remove commands': {
 | 
			
		||||
          target: 'Closed',
 | 
			
		||||
 | 
			
		||||
          actions: [
 | 
			
		||||
            assign({
 | 
			
		||||
              commands: ({ context, event }) =>
 | 
			
		||||
                context.commands.filter(
 | 
			
		||||
                  (c) =>
 | 
			
		||||
                    !event.data.commands.some(
 | 
			
		||||
                      (c2) => c2.name === c.name && c2.groupId === c.groupId
 | 
			
		||||
                    )
 | 
			
		||||
                ),
 | 
			
		||||
            }),
 | 
			
		||||
          ],
 | 
			
		||||
 | 
			
		||||
          reenter: false,
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    delays: {},
 | 
			
		||||
  }
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
    'Selecting command': {
 | 
			
		||||
      on: {
 | 
			
		||||
        'Select command': {
 | 
			
		||||
          target: 'Command selected',
 | 
			
		||||
          actions: ['Set selected command', 'Initialize arguments to submit'],
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    'Command selected': {
 | 
			
		||||
      always: [
 | 
			
		||||
        {
 | 
			
		||||
          target: 'Closed',
 | 
			
		||||
          guard: 'Command has no arguments',
 | 
			
		||||
          actions: ['Execute command'],
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          target: 'Checking Arguments',
 | 
			
		||||
          guard: 'All arguments are skippable',
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          target: 'Gathering arguments',
 | 
			
		||||
          actions: ['Set current argument to first non-skippable'],
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    'Gathering arguments': {
 | 
			
		||||
      states: {
 | 
			
		||||
        'Awaiting input': {
 | 
			
		||||
          on: {
 | 
			
		||||
            'Submit argument': {
 | 
			
		||||
              target: 'Validating',
 | 
			
		||||
            },
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        Validating: {
 | 
			
		||||
          invoke: {
 | 
			
		||||
            src: 'Validate argument',
 | 
			
		||||
            id: 'validateSingleArgument',
 | 
			
		||||
            input: ({ event }) => {
 | 
			
		||||
              if (event.type !== 'Submit argument') return {}
 | 
			
		||||
              return event.data
 | 
			
		||||
            },
 | 
			
		||||
            onDone: {
 | 
			
		||||
              target: '#Command Bar.Checking Arguments',
 | 
			
		||||
              actions: ['enqueueValidArgsToSubmit'],
 | 
			
		||||
            },
 | 
			
		||||
            onError: [
 | 
			
		||||
              {
 | 
			
		||||
                target: 'Awaiting input',
 | 
			
		||||
              },
 | 
			
		||||
            ],
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
 | 
			
		||||
      initial: 'Awaiting input',
 | 
			
		||||
 | 
			
		||||
      on: {
 | 
			
		||||
        'Change current argument': {
 | 
			
		||||
          target: 'Gathering arguments',
 | 
			
		||||
          internal: true,
 | 
			
		||||
          actions: ['Set current argument'],
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        'Deselect command': {
 | 
			
		||||
          target: 'Selecting command',
 | 
			
		||||
          actions: [
 | 
			
		||||
            assign({
 | 
			
		||||
              selectedCommand: (_c, _e) => undefined,
 | 
			
		||||
            }),
 | 
			
		||||
          ],
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    Review: {
 | 
			
		||||
      entry: ['Clear current argument'],
 | 
			
		||||
      on: {
 | 
			
		||||
        'Submit command': {
 | 
			
		||||
          target: 'Closed',
 | 
			
		||||
          actions: ['Execute command'],
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        'Add argument': {
 | 
			
		||||
          target: 'Gathering arguments',
 | 
			
		||||
          actions: ['Set current argument'],
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        'Remove argument': {
 | 
			
		||||
          target: 'Review',
 | 
			
		||||
          actions: ['Remove argument'],
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        'Edit argument': {
 | 
			
		||||
          target: 'Gathering arguments',
 | 
			
		||||
          actions: ['Set current argument'],
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    'Checking Arguments': {
 | 
			
		||||
      invoke: {
 | 
			
		||||
        src: 'Validate all arguments',
 | 
			
		||||
        id: 'validateArguments',
 | 
			
		||||
        input: ({ context }) => context,
 | 
			
		||||
        onDone: [
 | 
			
		||||
          {
 | 
			
		||||
            target: 'Review',
 | 
			
		||||
            guard: 'Command needs review',
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            target: 'Closed',
 | 
			
		||||
            actions: 'Execute command',
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
        onError: [
 | 
			
		||||
          {
 | 
			
		||||
            target: 'Gathering arguments',
 | 
			
		||||
            actions: ['Set current argument to first non-skippable'],
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  on: {
 | 
			
		||||
    Close: {
 | 
			
		||||
      target: '.Closed',
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    Clear: {
 | 
			
		||||
      target: '#Command Bar',
 | 
			
		||||
      reenter: false,
 | 
			
		||||
      actions: ['Clear argument data'],
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
function sortCommands(a: Command, b: Command) {
 | 
			
		||||
  if (b.groupId === 'auth' && !(a.groupId === 'auth')) return -2
 | 
			
		||||
 | 
			
		||||
@ -1,233 +1,393 @@
 | 
			
		||||
import { assign, createMachine } from 'xstate'
 | 
			
		||||
import { assign, fromPromise, setup } from 'xstate'
 | 
			
		||||
import { Project, FileEntry } from 'lib/project'
 | 
			
		||||
 | 
			
		||||
export const fileMachine = createMachine(
 | 
			
		||||
  {
 | 
			
		||||
    /** @xstate-layout N4IgpgJg5mDOIC5QDECWAbMACAtgQwGMALVAOzAGI9ZZUpSBtABgF1FQAHAe1oBdUupdiAAeiACwAmADQgAnogAcANgCsAOnHiAjOICcAZh3K9TRQHYAvpdlpMuQiXIUASmABmAJzhFmbJCDcfAJCAWIIUrIKCHpq6nraipJJKorahqrWthjY+MRkYOoAEtRYpFxY7jmwFADC3ni82FWYfsJBqPyCwuG6hurKTAYG5mlSyeJRiHqS2prKg6Mj2pKz4lkgdrmOBcWlLXCuYKR4OM05bQEdXaGgvdpD6qPiioqqieJM2gaqUxELT3MQz0eleBlMhmUGy2Dny5D2sEq1TqDSaSNarHaPE6IR6iD6BgGQxGY1Wikm8gkQM0n2UknERm0qmUw2hOVhTkKJURBxq9TAjXOrW0-k42JueIQBKJw1GujJFOiqkkTE0X3M5mUuiYgxmbPseU5CPRhwAImBMGiDpcxcFumF8dptMp1GZRlqNYYdXo-ml1IlzMkHuZVAZJAY1PrtnCuftkQB5DjHE02wLi3EOqX6QmDWWkiZ-HSKf3gph6ZWqcwJIZRjm7bkmmoAZTAvCwsAtYAITQgWAgqG83a4njkqeuGbu+MkLPU7x+iiGszJfwj4ldTvB6UURh0klrht2-MaZCgWDwpF7XCTpBPJooEEEhTIADcuABrQoEVFgAC054gP5XscP7WpiVzpvak5SnO6hJD8RYfJ8ir4kw06zqoTDiMyTA4WGPz7js8JHvwpCnv+WBATepF3mAnieMO6gcOgjTuMOODqF+ApNH+F6AdeIEXGBto4pBoj4u8GjOiMqjiKMqhJFhfyVqqEbJIoCTkmk3wETG6huCcOC3gc96PuoL7voU3gGb+oGimmdq3GJCARuY8RMroEk6MMihKeWrpYepEZMKMSw6Ua+mnEZOQULR9GeIxzG8KxnjsVZpw2YJdnjqJ4QjK59KaoGLKhh6fyBpIsFgtqKjKuCYW7OalpRZgJnwuZH7qBAnbcbZWIOZKeXqAVyhFT8EbaOYK44f6kjlhYG6FVYNibOyB7wo1rbNZQsUMUxLFsZ13UZRiWUQY5uUakNskjdOY2lZSCAqhV24LlhHpMl89Xwm4eD9tRvKtU+pCvh1DQAbyY5nZKMwqZWwxqMorzltoZUrK6YbOlJoazIoX2FD9f2ngDD5tcDFnqGDAmYLADAin1InndMKrqD85jw8ySPvH8pgulqoYWEjc16HjekCoTjYxXRu2JclqVi1TcCQ-1mYwyzcMRhz6lcw9C56Cz4Yatd05ISLxFbYDZlkx1nGCgrSsM5KTJVgMMmjKkEYmAYfwrOkQ30i8WFSF8mTLTCa2FGb-3RTt8V7UlB02z1mX0xKmZMgu8R6C8YahqYwUow9TqBkNxXiLmUgGEyIsRYZUctSTQMg5ZxzpXbdPgcrUEuW57xYUyXkGD5D2Bhog9aKsLyzQywsbOUXXwAEYeEWAKcTk5P7KH8G+ujhuHDDJTJZ0t2QGsvxrlI2q85fiBhlgMZcQq8+iqDJ3OzAML2qCCqxDEkIsNryK+jMpSV1clIck3xB6ViLIWEwmhXiJF0EYIqptUS3nIpRLaQDHajAqvKCwqxEZTxkIXVChJNTqUDCkB4L9q4t1rkTHI2DMyRAeosIawxFxDESMoLCIsNokUYZgZhUF1IDGdK7LyWgX6TULqCIagYcKSHMAya6VdQ6rTPgTLaC9hKpygnSOY8FA7kj0J6WR0QISzn0J8IYN0tIi0TMcLBHcHZp1wf6cB5UiFZxIdEcEhJKyvQ9BqGSqCuIuL0WvXoHj8HeKSL472E0KrBRfrVRGL9cbWEsEAA */
 | 
			
		||||
    id: 'File machine',
 | 
			
		||||
type FileMachineContext = {
 | 
			
		||||
  project: Project
 | 
			
		||||
  selectedDirectory: FileEntry
 | 
			
		||||
  itemsBeingRenamed: (FileEntry | string)[]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
    initial: 'Reading files',
 | 
			
		||||
type FileMachineEvents =
 | 
			
		||||
  | { type: 'Open file'; data: { name: string } }
 | 
			
		||||
  | {
 | 
			
		||||
      type: 'Rename file'
 | 
			
		||||
      data: { oldName: string; newName: string; isDir: boolean }
 | 
			
		||||
    }
 | 
			
		||||
  | {
 | 
			
		||||
      type: 'Create file'
 | 
			
		||||
      data: {
 | 
			
		||||
        name: string
 | 
			
		||||
        makeDir: boolean
 | 
			
		||||
        content?: string
 | 
			
		||||
        silent?: boolean
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  | { type: 'Delete file'; data: FileEntry }
 | 
			
		||||
  | { type: 'Set selected directory'; directory: FileEntry }
 | 
			
		||||
  | { type: 'navigate'; data: { name: string } }
 | 
			
		||||
  | {
 | 
			
		||||
      type: 'xstate.done.actor.read-files'
 | 
			
		||||
      output: Project
 | 
			
		||||
    }
 | 
			
		||||
  | {
 | 
			
		||||
      type: 'xstate.done.actor.rename-file'
 | 
			
		||||
      output: {
 | 
			
		||||
        message: string
 | 
			
		||||
        oldPath: string
 | 
			
		||||
        newPath: string
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  | {
 | 
			
		||||
      type: 'xstate.done.actor.create-and-open-file'
 | 
			
		||||
      output: {
 | 
			
		||||
        message: string
 | 
			
		||||
        path: string
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  | {
 | 
			
		||||
      type: 'xstate.done.actor.create-file'
 | 
			
		||||
      output: {
 | 
			
		||||
        path: string
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  | {
 | 
			
		||||
      type: 'xstate.done.actor.delete-file'
 | 
			
		||||
      output: {
 | 
			
		||||
        message: string
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  | { type: 'assign'; data: { [key: string]: any } }
 | 
			
		||||
  | { type: 'Refresh' }
 | 
			
		||||
 | 
			
		||||
    context: {
 | 
			
		||||
      project: {} as Project,
 | 
			
		||||
      selectedDirectory: {} as FileEntry,
 | 
			
		||||
      itemsBeingRenamed: [] as string[],
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    on: {
 | 
			
		||||
      assign: {
 | 
			
		||||
        actions: assign((_, event) => ({
 | 
			
		||||
          ...event.data,
 | 
			
		||||
        })),
 | 
			
		||||
        target: '.Reading files',
 | 
			
		||||
      },
 | 
			
		||||
 | 
			
		||||
      Refresh: '.Reading files',
 | 
			
		||||
    },
 | 
			
		||||
    states: {
 | 
			
		||||
      'Has no files': {
 | 
			
		||||
        on: {
 | 
			
		||||
          'Create file': {
 | 
			
		||||
            target: 'Creating and opening file',
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
 | 
			
		||||
      'Has files': {
 | 
			
		||||
        on: {
 | 
			
		||||
          'Rename file': {
 | 
			
		||||
            target: 'Renaming file',
 | 
			
		||||
          },
 | 
			
		||||
 | 
			
		||||
          'Create file': [
 | 
			
		||||
            {
 | 
			
		||||
              target: 'Creating and opening file',
 | 
			
		||||
              cond: 'Is not silent',
 | 
			
		||||
            },
 | 
			
		||||
            'Creating file',
 | 
			
		||||
          ],
 | 
			
		||||
 | 
			
		||||
          'Delete file': {
 | 
			
		||||
            target: 'Deleting file',
 | 
			
		||||
          },
 | 
			
		||||
 | 
			
		||||
          'Open file': {
 | 
			
		||||
            target: 'Opening file',
 | 
			
		||||
          },
 | 
			
		||||
 | 
			
		||||
          'Set selected directory': {
 | 
			
		||||
            target: 'Has files',
 | 
			
		||||
            actions: ['setSelectedDirectory'],
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
 | 
			
		||||
      'Creating and opening file': {
 | 
			
		||||
        invoke: {
 | 
			
		||||
          id: 'create-and-open-file',
 | 
			
		||||
          src: 'createAndOpenFile',
 | 
			
		||||
          onDone: [
 | 
			
		||||
            {
 | 
			
		||||
              target: 'Reading files',
 | 
			
		||||
              actions: [
 | 
			
		||||
                'createToastSuccess',
 | 
			
		||||
                'addFileToRenamingQueue',
 | 
			
		||||
                'navigateToFile',
 | 
			
		||||
              ],
 | 
			
		||||
            },
 | 
			
		||||
          ],
 | 
			
		||||
          onError: [
 | 
			
		||||
            {
 | 
			
		||||
              target: 'Reading files',
 | 
			
		||||
              actions: ['toastError'],
 | 
			
		||||
            },
 | 
			
		||||
          ],
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
 | 
			
		||||
      'Renaming file': {
 | 
			
		||||
        invoke: {
 | 
			
		||||
          id: 'rename-file',
 | 
			
		||||
          src: 'renameFile',
 | 
			
		||||
          onDone: [
 | 
			
		||||
            {
 | 
			
		||||
              target: '#File machine.Reading files',
 | 
			
		||||
              actions: ['renameToastSuccess'],
 | 
			
		||||
              cond: 'Name has been changed',
 | 
			
		||||
            },
 | 
			
		||||
            'Reading files',
 | 
			
		||||
          ],
 | 
			
		||||
          onError: [
 | 
			
		||||
            {
 | 
			
		||||
              target: '#File machine.Reading files',
 | 
			
		||||
              actions: ['toastError'],
 | 
			
		||||
            },
 | 
			
		||||
          ],
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        exit: 'removeFileFromRenamingQueue',
 | 
			
		||||
      },
 | 
			
		||||
 | 
			
		||||
      'Deleting file': {
 | 
			
		||||
        invoke: {
 | 
			
		||||
          id: 'delete-file',
 | 
			
		||||
          src: 'deleteFile',
 | 
			
		||||
          onDone: [
 | 
			
		||||
            {
 | 
			
		||||
              actions: ['toastSuccess'],
 | 
			
		||||
              target: '#File machine.Reading files',
 | 
			
		||||
            },
 | 
			
		||||
          ],
 | 
			
		||||
          onError: {
 | 
			
		||||
            actions: ['toastError'],
 | 
			
		||||
            target: '#File machine.Has files',
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
 | 
			
		||||
      'Reading files': {
 | 
			
		||||
        invoke: {
 | 
			
		||||
          id: 'read-files',
 | 
			
		||||
          src: 'readFiles',
 | 
			
		||||
          onDone: [
 | 
			
		||||
            {
 | 
			
		||||
              cond: 'Has at least 1 file',
 | 
			
		||||
              target: 'Has files',
 | 
			
		||||
              actions: ['setFiles'],
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
              target: 'Has no files',
 | 
			
		||||
              actions: ['setFiles'],
 | 
			
		||||
            },
 | 
			
		||||
          ],
 | 
			
		||||
          onError: [
 | 
			
		||||
            {
 | 
			
		||||
              target: 'Has no files',
 | 
			
		||||
              actions: ['toastError'],
 | 
			
		||||
            },
 | 
			
		||||
          ],
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
 | 
			
		||||
      'Opening file': {
 | 
			
		||||
        entry: ['navigateToFile'],
 | 
			
		||||
      },
 | 
			
		||||
 | 
			
		||||
      'Creating file': {
 | 
			
		||||
        invoke: {
 | 
			
		||||
          src: 'createFile',
 | 
			
		||||
          id: 'create-file',
 | 
			
		||||
          onDone: 'Reading files',
 | 
			
		||||
          onError: 'Reading files',
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    schema: {
 | 
			
		||||
      events: {} as
 | 
			
		||||
        | { type: 'Open file'; data: { name: string } }
 | 
			
		||||
        | {
 | 
			
		||||
            type: 'Rename file'
 | 
			
		||||
            data: { oldName: string; newName: string; isDir: boolean }
 | 
			
		||||
          }
 | 
			
		||||
        | {
 | 
			
		||||
            type: 'Create file'
 | 
			
		||||
            data: {
 | 
			
		||||
              name: string
 | 
			
		||||
              makeDir: boolean
 | 
			
		||||
              content?: string
 | 
			
		||||
              silent?: boolean
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        | { type: 'Delete file'; data: FileEntry }
 | 
			
		||||
        | { type: 'Set selected directory'; data: FileEntry }
 | 
			
		||||
        | { type: 'navigate'; data: { name: string } }
 | 
			
		||||
        | {
 | 
			
		||||
            type: 'done.invoke.read-files'
 | 
			
		||||
            data: Project
 | 
			
		||||
          }
 | 
			
		||||
        | {
 | 
			
		||||
            type: 'done.invoke.rename-file'
 | 
			
		||||
            data: {
 | 
			
		||||
              message: string
 | 
			
		||||
              oldPath: string
 | 
			
		||||
              newPath: string
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        | {
 | 
			
		||||
            type: 'done.invoke.create-and-open-file'
 | 
			
		||||
            data: {
 | 
			
		||||
              message: string
 | 
			
		||||
              path: string
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        | {
 | 
			
		||||
            type: 'done.invoke.create-file'
 | 
			
		||||
            data: {
 | 
			
		||||
              path: string
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        | { type: 'assign'; data: { [key: string]: any } }
 | 
			
		||||
        | { type: 'Refresh' },
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    predictableActionArguments: true,
 | 
			
		||||
    preserveActionOrder: true,
 | 
			
		||||
    tsTypes: {} as import('./fileMachine.typegen').Typegen0,
 | 
			
		||||
export const fileMachine = setup({
 | 
			
		||||
  types: {} as {
 | 
			
		||||
    context: FileMachineContext
 | 
			
		||||
    events: FileMachineEvents
 | 
			
		||||
    input: Partial<Pick<FileMachineContext, 'project' | 'selectedDirectory'>>
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    actions: {
 | 
			
		||||
      setFiles: assign((_, event) => {
 | 
			
		||||
        return { project: event.data }
 | 
			
		||||
      }),
 | 
			
		||||
      setSelectedDirectory: assign((_, event) => {
 | 
			
		||||
        return { selectedDirectory: event.data }
 | 
			
		||||
      }),
 | 
			
		||||
  actions: {
 | 
			
		||||
    setFiles: assign(({ event }) => {
 | 
			
		||||
      if (event.type !== 'xstate.done.actor.read-files') return {}
 | 
			
		||||
      return { project: event.output }
 | 
			
		||||
    }),
 | 
			
		||||
    setSelectedDirectory: assign(({ event }) => {
 | 
			
		||||
      if (event.type !== 'Set selected directory') return {}
 | 
			
		||||
      return { selectedDirectory: event.directory }
 | 
			
		||||
    }),
 | 
			
		||||
    addFileToRenamingQueue: assign({
 | 
			
		||||
      itemsBeingRenamed: ({ context, event }) => {
 | 
			
		||||
        if (event.type !== 'xstate.done.actor.create-and-open-file')
 | 
			
		||||
          return context.itemsBeingRenamed
 | 
			
		||||
        return [...context.itemsBeingRenamed, event.output.path]
 | 
			
		||||
      },
 | 
			
		||||
    }),
 | 
			
		||||
    removeFileFromRenamingQueue: assign({
 | 
			
		||||
      itemsBeingRenamed: ({ context, event }) => {
 | 
			
		||||
        if (event.type !== 'xstate.done.actor.rename-file')
 | 
			
		||||
          return context.itemsBeingRenamed
 | 
			
		||||
        return context.itemsBeingRenamed.filter(
 | 
			
		||||
          (path) => path !== event.output.oldPath
 | 
			
		||||
        )
 | 
			
		||||
      },
 | 
			
		||||
    }),
 | 
			
		||||
    navigateToFile: () => {},
 | 
			
		||||
    renameToastSuccess: () => {},
 | 
			
		||||
    createToastSuccess: () => {},
 | 
			
		||||
    toastSuccess: () => {},
 | 
			
		||||
    toastError: () => {},
 | 
			
		||||
  },
 | 
			
		||||
  guards: {
 | 
			
		||||
    'Name has been changed': ({ event }) => {
 | 
			
		||||
      if (event.type !== 'xstate.done.actor.rename-file') return false
 | 
			
		||||
      return event.output.newPath !== event.output.oldPath
 | 
			
		||||
    },
 | 
			
		||||
    guards: {
 | 
			
		||||
      'Name has been changed': (_, event) => {
 | 
			
		||||
        return event.data.newPath !== event.data.oldPath
 | 
			
		||||
    'Has at least 1 file': ({ event }) => {
 | 
			
		||||
      if (event.type !== 'xstate.done.actor.read-files') return false
 | 
			
		||||
      return !!event?.output?.children && event.output.children.length > 0
 | 
			
		||||
    },
 | 
			
		||||
    'Is not silent': ({ event }) =>
 | 
			
		||||
      event.type === 'Create file' ? !event.data.silent : false,
 | 
			
		||||
  },
 | 
			
		||||
  actors: {
 | 
			
		||||
    readFiles: fromPromise(({ input }: { input: Project }) =>
 | 
			
		||||
      Promise.resolve(input)
 | 
			
		||||
    ),
 | 
			
		||||
    createAndOpenFile: fromPromise(
 | 
			
		||||
      (_: {
 | 
			
		||||
        input: {
 | 
			
		||||
          name: string
 | 
			
		||||
          makeDir: boolean
 | 
			
		||||
          selectedDirectory: FileEntry
 | 
			
		||||
          content: string
 | 
			
		||||
        }
 | 
			
		||||
      }) => Promise.resolve({ message: '', path: '' })
 | 
			
		||||
    ),
 | 
			
		||||
    renameFile: fromPromise(
 | 
			
		||||
      (_: {
 | 
			
		||||
        input: {
 | 
			
		||||
          oldName: string
 | 
			
		||||
          newName: string
 | 
			
		||||
          isDir: boolean
 | 
			
		||||
          selectedDirectory: FileEntry
 | 
			
		||||
        }
 | 
			
		||||
      }) => Promise.resolve({ message: '', newPath: '', oldPath: '' })
 | 
			
		||||
    ),
 | 
			
		||||
    deleteFile: fromPromise(
 | 
			
		||||
      (_: {
 | 
			
		||||
        input: { path: string; children: FileEntry[] | null; name: string }
 | 
			
		||||
      }) => Promise.resolve({ message: '' } as { message: string } | undefined)
 | 
			
		||||
    ),
 | 
			
		||||
    createFile: fromPromise(
 | 
			
		||||
      (_: {
 | 
			
		||||
        input: {
 | 
			
		||||
          name: string
 | 
			
		||||
          makeDir: boolean
 | 
			
		||||
          selectedDirectory: FileEntry
 | 
			
		||||
          content: string
 | 
			
		||||
        }
 | 
			
		||||
      }) => Promise.resolve({ path: '' })
 | 
			
		||||
    ),
 | 
			
		||||
  },
 | 
			
		||||
}).createMachine({
 | 
			
		||||
  /** @xstate-layout N4IgpgJg5mDOIC5QDECWAbMACAtgQwGMALVAOzAGI9ZZUpSBtABgF1FQAHAe1oBdUupdiAAeiACwAmADQgAnogAcANgCsAOnHiAjOICcAZh3K9TRQHYAvpdlpMuQiXIUASmABmAJzhFmbJCDcfAJCAWIIUrIKCHpq6nraipJJKorahqrWthjY+MRkYOoAEtRYpFxY7jmwFADC3ni82FWYfsJBqPyCwuG6hurKTAYG5mlSyeJRiHqS2prKg6Mj2pKz4lkgdrmOBcWlLXCuYKR4OM05bQEdXaGgvdpD6qPiioqqieJM2gaqUxELT3MQz0eleBlMhmUGy2Dny5D2sEq1TqDSaSNarHaPE6IR6iD6BgGQxGY1Wikm8gkQM0n2UknERm0qmUw2hOVhTkKJURBxq9TAjXOrW0-k42JueIQBKJw1GujJFOiqkkTE0X3M5mUuiYgxmbPseU5CPRhwAImBMGiDpcxcFumF8dptMp1GZRlqNYYdXo-ml1IlzMkHuZVAZJAY1PrtnCuftkQB5DjHE02wLi3EOqX6QmDWWkiZ-HSKf3gph6ZWqcwJIZRjm7bkmmoAZTAvCwsAtYAITQgWAgqG83a4njkqeuGbu+MkLPU7x+iiGszJfwj4ldTvB6UURh0klrht2-MaZCgWDwpF7XCTpBPJooEEEhTIADcuABrQoEVFgAC054gP5XscP7WpiVzpvak5SnO6hJD8RYfJ8ir4kw06zqoTDiMyTA4WGPz7js8JHvwpCnv+WBATepF3mAnieMO6gcOgjTuMOODqF+ApNH+F6AdeIEXGBto4pBoj4u8GjOiMqjiKMqhJFhfyVqqEbJIoCTkmk3wETG6huCcOC3gc96PuoL7voU3gGb+oGimmdq3GJCARuY8RMroEk6MMihKeWrpYepEZMKMSw6Ua+mnEZOQULR9GeIxzG8KxnjsVZpw2YJdnjqJ4QjK59KaoGLKhh6fyBpIsFgtqKjKuCYW7OalpRZgJnwuZH7qBAnbcbZWIOZKeXqAVyhFT8EbaOYK44f6kjlhYG6FVYNibOyB7wo1rbNZQsUMUxLFsZ13UZRiWUQY5uUakNskjdOY2lZSCAqhV24LlhHpMl89Xwm4eD9tRvKtU+pCvh1DQAbyY5nZKMwqZWwxqMorzltoZUrK6YbOlJoazIoX2FD9f2ngDD5tcDFnqGDAmYLADAin1InndMKrqD85jw8ySPvH8pgulqoYWEjc16HjekCoTjYxXRu2JclqVi1TcCQ-1mYwyzcMRhz6lcw9C56Cz4Yatd05ISLxFbYDZlkx1nGCgrSsM5KTJVgMMmjKkEYmAYfwrOkQ30i8WFSF8mTLTCa2FGb-3RTt8V7UlB02z1mX0xKmZMgu8R6C8YahqYwUow9TqBkNxXiLmUgGEyIsRYZUctSTQMg5ZxzpXbdPgcrUEuW57xYUyXkGD5D2Bhog9aKsLyzQywsbOUXXwAEYeEWAKcTk5P7KH8G+ujhuHDDJTJZ0t2QGsvxrlI2q85fiBhlgMZcQq8+iqDJ3OzAML2qCCqxDEkIsNryK+jMpSV1clIck3xB6ViLIWEwmhXiJF0EYIqptUS3nIpRLaQDHajAqvKCwqxEZTxkIXVChJNTqUDCkB4L9q4t1rkTHI2DMyRAeosIawxFxDESMoLCIsNokUYZgZhUF1IDGdK7LyWgX6TULqCIagYcKSHMAya6VdQ6rTPgTLaC9hKpygnSOY8FA7kj0J6WR0QISzn0J8IYN0tIi0TMcLBHcHZp1wf6cB5UiFZxIdEcEhJKyvQ9BqGSqCuIuL0WvXoHj8HeKSL472E0KrBRfrVRGL9cbWEsEAA */
 | 
			
		||||
  id: 'File machine',
 | 
			
		||||
 | 
			
		||||
  initial: 'Reading files',
 | 
			
		||||
 | 
			
		||||
  context: ({ input }) => {
 | 
			
		||||
    return {
 | 
			
		||||
      project: input.project ?? ({} as Project), // TODO: Either make this a flexible type or type this property to allow empty object
 | 
			
		||||
      selectedDirectory: input.selectedDirectory ?? ({} as FileEntry), // TODO: Either make this a flexible type or type this property to allow empty object
 | 
			
		||||
      itemsBeingRenamed: [],
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  on: {
 | 
			
		||||
    assign: {
 | 
			
		||||
      actions: assign(({ event }) => ({
 | 
			
		||||
        ...event.data,
 | 
			
		||||
      })),
 | 
			
		||||
      target: '.Reading files',
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    Refresh: '.Reading files',
 | 
			
		||||
  },
 | 
			
		||||
  states: {
 | 
			
		||||
    'Has no files': {
 | 
			
		||||
      on: {
 | 
			
		||||
        'Create file': {
 | 
			
		||||
          target: 'Creating and opening file',
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  }
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
    'Has files': {
 | 
			
		||||
      on: {
 | 
			
		||||
        'Rename file': {
 | 
			
		||||
          target: 'Renaming file',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        'Create file': [
 | 
			
		||||
          {
 | 
			
		||||
            target: 'Creating and opening file',
 | 
			
		||||
            guard: 'Is not silent',
 | 
			
		||||
          },
 | 
			
		||||
          'Creating file',
 | 
			
		||||
        ],
 | 
			
		||||
 | 
			
		||||
        'Delete file': {
 | 
			
		||||
          target: 'Deleting file',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        'Open file': {
 | 
			
		||||
          target: 'Opening file',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        'Set selected directory': {
 | 
			
		||||
          target: 'Has files',
 | 
			
		||||
          actions: ['setSelectedDirectory'],
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    'Creating and opening file': {
 | 
			
		||||
      invoke: {
 | 
			
		||||
        id: 'create-and-open-file',
 | 
			
		||||
        src: 'createAndOpenFile',
 | 
			
		||||
        input: ({ event, context }) => {
 | 
			
		||||
          if (event.type !== 'Create file')
 | 
			
		||||
            // This is just to make TS happy
 | 
			
		||||
            return {
 | 
			
		||||
              name: '',
 | 
			
		||||
              makeDir: false,
 | 
			
		||||
              selectedDirectory: context.selectedDirectory,
 | 
			
		||||
              content: '',
 | 
			
		||||
            }
 | 
			
		||||
          return {
 | 
			
		||||
            name: event.data.name,
 | 
			
		||||
            makeDir: event.data.makeDir,
 | 
			
		||||
            selectedDirectory: context.selectedDirectory,
 | 
			
		||||
            content: event.data.content ?? '',
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        onDone: [
 | 
			
		||||
          {
 | 
			
		||||
            target: 'Reading files',
 | 
			
		||||
            actions: [
 | 
			
		||||
              {
 | 
			
		||||
                type: 'createToastSuccess',
 | 
			
		||||
                params: ({
 | 
			
		||||
                  event,
 | 
			
		||||
                }: {
 | 
			
		||||
                  // TODO: rely on type inference
 | 
			
		||||
                  event: Extract<
 | 
			
		||||
                    FileMachineEvents,
 | 
			
		||||
                    { type: 'xstate.done.actor.create-and-open-file' }
 | 
			
		||||
                  >
 | 
			
		||||
                }) => {
 | 
			
		||||
                  return { message: event.output.message }
 | 
			
		||||
                },
 | 
			
		||||
              },
 | 
			
		||||
              'addFileToRenamingQueue',
 | 
			
		||||
              'navigateToFile',
 | 
			
		||||
            ],
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
        onError: [
 | 
			
		||||
          {
 | 
			
		||||
            target: 'Reading files',
 | 
			
		||||
            actions: ['toastError'],
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    'Renaming file': {
 | 
			
		||||
      invoke: {
 | 
			
		||||
        id: 'rename-file',
 | 
			
		||||
        src: 'renameFile',
 | 
			
		||||
        input: ({ event, context }) => {
 | 
			
		||||
          if (event.type !== 'Rename file') {
 | 
			
		||||
            // This is just to make TS happy
 | 
			
		||||
            return {
 | 
			
		||||
              oldName: '',
 | 
			
		||||
              newName: '',
 | 
			
		||||
              isDir: false,
 | 
			
		||||
              selectedDirectory: {} as FileEntry,
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
          return {
 | 
			
		||||
            oldName: event.data.oldName,
 | 
			
		||||
            newName: event.data.newName,
 | 
			
		||||
            isDir: event.data.isDir,
 | 
			
		||||
            selectedDirectory: context.selectedDirectory,
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        onDone: [
 | 
			
		||||
          {
 | 
			
		||||
            target: '#File machine.Reading files',
 | 
			
		||||
            actions: ['renameToastSuccess'],
 | 
			
		||||
            guard: 'Name has been changed',
 | 
			
		||||
          },
 | 
			
		||||
          'Reading files',
 | 
			
		||||
        ],
 | 
			
		||||
        onError: [
 | 
			
		||||
          {
 | 
			
		||||
            target: '#File machine.Reading files',
 | 
			
		||||
            actions: ['toastError'],
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
 | 
			
		||||
      exit: 'removeFileFromRenamingQueue',
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    'Deleting file': {
 | 
			
		||||
      invoke: {
 | 
			
		||||
        id: 'delete-file',
 | 
			
		||||
        src: 'deleteFile',
 | 
			
		||||
        input: ({ event }) => {
 | 
			
		||||
          if (event.type !== 'Delete file') {
 | 
			
		||||
            // This is just to make TS happy
 | 
			
		||||
            return {
 | 
			
		||||
              path: '',
 | 
			
		||||
              children: [],
 | 
			
		||||
              name: '',
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
          return {
 | 
			
		||||
            path: event.data.path,
 | 
			
		||||
            children: event.data.children,
 | 
			
		||||
            name: event.data.name,
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        onDone: [
 | 
			
		||||
          {
 | 
			
		||||
            actions: ['toastSuccess'],
 | 
			
		||||
            target: '#File machine.Reading files',
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
        onError: {
 | 
			
		||||
          actions: ['toastError'],
 | 
			
		||||
          target: '#File machine.Has files',
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    'Reading files': {
 | 
			
		||||
      invoke: {
 | 
			
		||||
        id: 'read-files',
 | 
			
		||||
        src: 'readFiles',
 | 
			
		||||
        input: ({ context }) => context.project,
 | 
			
		||||
        onDone: [
 | 
			
		||||
          {
 | 
			
		||||
            guard: 'Has at least 1 file',
 | 
			
		||||
            target: 'Has files',
 | 
			
		||||
            actions: ['setFiles'],
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            target: 'Has no files',
 | 
			
		||||
            actions: ['setFiles'],
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
        onError: [
 | 
			
		||||
          {
 | 
			
		||||
            target: 'Has no files',
 | 
			
		||||
            actions: ['toastError'],
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    'Opening file': {
 | 
			
		||||
      entry: ['navigateToFile'],
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    'Creating file': {
 | 
			
		||||
      invoke: {
 | 
			
		||||
        src: 'createFile',
 | 
			
		||||
        id: 'create-file',
 | 
			
		||||
        input: ({ event, context }) => {
 | 
			
		||||
          if (event.type !== 'Create file') {
 | 
			
		||||
            // This is just to make TS happy
 | 
			
		||||
            return {
 | 
			
		||||
              name: '',
 | 
			
		||||
              makeDir: false,
 | 
			
		||||
              selectedDirectory: {} as FileEntry,
 | 
			
		||||
              content: '',
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
          return {
 | 
			
		||||
            name: event.data.name,
 | 
			
		||||
            makeDir: event.data.makeDir,
 | 
			
		||||
            selectedDirectory: context.selectedDirectory,
 | 
			
		||||
            content: event.data.content ?? '',
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        onDone: 'Reading files',
 | 
			
		||||
        onError: 'Reading files',
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
@ -1,164 +1,233 @@
 | 
			
		||||
import { assign, createMachine } from 'xstate'
 | 
			
		||||
import { assign, fromPromise, setup } from 'xstate'
 | 
			
		||||
import { HomeCommandSchema } from 'lib/commandBarConfigs/homeCommandConfig'
 | 
			
		||||
import { Project } from 'lib/project'
 | 
			
		||||
 | 
			
		||||
export const homeMachine = createMachine(
 | 
			
		||||
  {
 | 
			
		||||
    /** @xstate-layout N4IgpgJg5mDOIC5QAkD2BbMACdBDAxgBYCWAdmAHTK6xampYAOATqgFZj4AusAxAMLMwuLthbtOXANoAGALqJQjVLGJdiqUopAAPRAHYAbPooAWABwBGUwE5zAJgeGArM-MAaEAE9EN0wGYKGX97GX1nGVNDS0MbfwBfeM80TBwCEnIqGiZWDm4+ACUwUlxU8TzpeW1lVXVNbT0EcJNg02d-fzt7fU77Tx8EQ0iKCPtnfUsjGRtLGXtE5IxsPCIySmpacsk+QWFRHIluWQUkEBq1DS1TxqN7ChjzOxtXf0t7a37EcwsRibH-ZzRezA8wLEApZbpNZZTa5ba8AAiYAANmB9lsjlVTuc6ldQDdDOYKP5bm0os5TDJDJ8mlEzPpzIZHA4bO9umCIWlVpkNgcKnwAPKMYp8yTHaoqC71a6IEmBUz6BkWZzWDq2Uw0qzOIJAwz+PXWfSmeZJcFLLkZSi7ERkKCi7i8CCaShkABuqAA1pR8EIRGAALQYyonJSS3ENRDA2wUeyvd6dPVhGw0-RhGOp8IA8xGFkc80rS0Ua3qUh2oO8MDMVjMCiMZEiABmqGY6AoPr2AaD4uxYcuEYQoQpQWNNjsMnMgLGKbT3TC7TcOfsNjzqQL0KKJXQtvtXEdzoobs9lCEm87cMxIbOvel+MQqtMQRmS5ks31sZpAUsZkcIX+cQZJIrpC3KUBupTbuWlbVrW9ZcE2LYUCepRnocwYSrUfYyggbzvBQ+jMq49imLYwTUt4iCft+5i-u0-7UfoQEWtCSKoiWZbnruTqZIeXoUBAKJoihFTdqGGE3rod7UdqsQTI8hiGAqrIauRA7RvYeoqhO1jtAqjFrpkLFohBHEVlWzYwY2zatvxrFCWKWKiVKeISdh4yBJE-jGs4fhhA4zg0kRNgxhplhaW0nn4XpUKZEUuAQMZqF8FxLqkO6vG+hAgYcbAIlXmJzmNERdy0RYNiKgpthxDSEU6q8MSTJYjWGFFIEULF8WljuSX7jxx7CJlQY5ZYl44pht4IP61gyPc8njt0lIuH51UKrVVITEyMy2C1hbtQl-KmdBdaWQhGVZYluWjeJjSTf402shMEyuEyljPAFL0UNmMiuN86lWHMiSmvQ-HwKcnL6WA6FOf2k3mESMRDA4RpUm4U4qf6gSEt0QIvvqfjOCaiyrtF6zZPQXWQ+GWFlUEsbmNMf1TV9NLeXDcqRIySnNaaYPEzC5M9vl-b+IyFCjupryPF9jKWP5Kks-cbMWLERHRNt0LFntkgU2NLk4dqsz43YsTK++Kk2C+MbTOOcxzOMrhqzFxTgZ1Qba1dd6BUE1jGsLMxxK9KlDNqm3tMLUQvqYlgO5QhlsTubsFXesTTUuPTfHExshDS0RftRftGgEnTZtHbX9Zr+QJ-2S4Y3qnmTC+4tMyp1EfeOnmeQqdOhyXQrFOXXCV1hCkmLDOnBJYvRRDSsyRzGjiKj0lKdAkANAA */
 | 
			
		||||
    id: 'Home machine',
 | 
			
		||||
 | 
			
		||||
    initial: 'Reading projects',
 | 
			
		||||
 | 
			
		||||
    context: {
 | 
			
		||||
      projects: [] as Project[],
 | 
			
		||||
      defaultProjectName: '',
 | 
			
		||||
      defaultDirectory: '',
 | 
			
		||||
export const homeMachine = setup({
 | 
			
		||||
  types: {
 | 
			
		||||
    context: {} as {
 | 
			
		||||
      projects: Project[]
 | 
			
		||||
      defaultProjectName: string
 | 
			
		||||
      defaultDirectory: string
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    on: {
 | 
			
		||||
      assign: {
 | 
			
		||||
        actions: assign((_, event) => ({
 | 
			
		||||
          ...event.data,
 | 
			
		||||
        })),
 | 
			
		||||
        target: '.Reading projects',
 | 
			
		||||
      },
 | 
			
		||||
    events: {} as
 | 
			
		||||
      | { type: 'Open project'; data: HomeCommandSchema['Open project'] }
 | 
			
		||||
      | { type: 'Rename project'; data: HomeCommandSchema['Rename project'] }
 | 
			
		||||
      | { type: 'Create project'; data: HomeCommandSchema['Create project'] }
 | 
			
		||||
      | { type: 'Delete project'; data: HomeCommandSchema['Delete project'] }
 | 
			
		||||
      | { type: 'navigate'; data: { name: string } }
 | 
			
		||||
      | {
 | 
			
		||||
          type: 'xstate.done.actor.read-projects'
 | 
			
		||||
          output: Project[]
 | 
			
		||||
        }
 | 
			
		||||
      | { type: 'assign'; data: { [key: string]: any } },
 | 
			
		||||
    input: {} as {
 | 
			
		||||
      projects: Project[]
 | 
			
		||||
      defaultProjectName: string
 | 
			
		||||
      defaultDirectory: string
 | 
			
		||||
    },
 | 
			
		||||
    states: {
 | 
			
		||||
      'Has no projects': {
 | 
			
		||||
        on: {
 | 
			
		||||
          'Create project': {
 | 
			
		||||
            target: 'Creating project',
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
 | 
			
		||||
      'Has projects': {
 | 
			
		||||
        on: {
 | 
			
		||||
          'Rename project': {
 | 
			
		||||
            target: 'Renaming project',
 | 
			
		||||
          },
 | 
			
		||||
 | 
			
		||||
          'Create project': {
 | 
			
		||||
            target: 'Creating project',
 | 
			
		||||
          },
 | 
			
		||||
 | 
			
		||||
          'Delete project': {
 | 
			
		||||
            target: 'Deleting project',
 | 
			
		||||
          },
 | 
			
		||||
 | 
			
		||||
          'Open project': {
 | 
			
		||||
            target: 'Opening project',
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
 | 
			
		||||
      'Creating project': {
 | 
			
		||||
        invoke: {
 | 
			
		||||
          id: 'create-project',
 | 
			
		||||
          src: 'createProject',
 | 
			
		||||
          onDone: [
 | 
			
		||||
            {
 | 
			
		||||
              target: 'Reading projects',
 | 
			
		||||
              actions: ['toastSuccess'],
 | 
			
		||||
            },
 | 
			
		||||
          ],
 | 
			
		||||
          onError: [
 | 
			
		||||
            {
 | 
			
		||||
              target: 'Reading projects',
 | 
			
		||||
              actions: ['toastError'],
 | 
			
		||||
            },
 | 
			
		||||
          ],
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
 | 
			
		||||
      'Renaming project': {
 | 
			
		||||
        invoke: {
 | 
			
		||||
          id: 'rename-project',
 | 
			
		||||
          src: 'renameProject',
 | 
			
		||||
          onDone: [
 | 
			
		||||
            {
 | 
			
		||||
              target: '#Home machine.Reading projects',
 | 
			
		||||
              actions: ['toastSuccess'],
 | 
			
		||||
            },
 | 
			
		||||
          ],
 | 
			
		||||
          onError: [
 | 
			
		||||
            {
 | 
			
		||||
              target: '#Home machine.Reading projects',
 | 
			
		||||
              actions: ['toastError'],
 | 
			
		||||
            },
 | 
			
		||||
          ],
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
 | 
			
		||||
      'Deleting project': {
 | 
			
		||||
        invoke: {
 | 
			
		||||
          id: 'delete-project',
 | 
			
		||||
          src: 'deleteProject',
 | 
			
		||||
          onDone: [
 | 
			
		||||
            {
 | 
			
		||||
              actions: ['toastSuccess'],
 | 
			
		||||
              target: '#Home machine.Reading projects',
 | 
			
		||||
            },
 | 
			
		||||
          ],
 | 
			
		||||
          onError: {
 | 
			
		||||
            actions: ['toastError'],
 | 
			
		||||
            target: '#Home machine.Has projects',
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
 | 
			
		||||
      'Reading projects': {
 | 
			
		||||
        invoke: {
 | 
			
		||||
          id: 'read-projects',
 | 
			
		||||
          src: 'readProjects',
 | 
			
		||||
          onDone: [
 | 
			
		||||
            {
 | 
			
		||||
              cond: 'Has at least 1 project',
 | 
			
		||||
              target: 'Has projects',
 | 
			
		||||
              actions: ['setProjects'],
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
              target: 'Has no projects',
 | 
			
		||||
              actions: ['setProjects'],
 | 
			
		||||
            },
 | 
			
		||||
          ],
 | 
			
		||||
          onError: [
 | 
			
		||||
            {
 | 
			
		||||
              target: 'Has no projects',
 | 
			
		||||
              actions: ['toastError'],
 | 
			
		||||
            },
 | 
			
		||||
          ],
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
 | 
			
		||||
      'Opening project': {
 | 
			
		||||
        entry: ['navigateToProject'],
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    schema: {
 | 
			
		||||
      events: {} as
 | 
			
		||||
        | { type: 'Open project'; data: HomeCommandSchema['Open project'] }
 | 
			
		||||
        | { type: 'Rename project'; data: HomeCommandSchema['Rename project'] }
 | 
			
		||||
        | { type: 'Create project'; data: HomeCommandSchema['Create project'] }
 | 
			
		||||
        | { type: 'Delete project'; data: HomeCommandSchema['Delete project'] }
 | 
			
		||||
        | { type: 'navigate'; data: { name: string } }
 | 
			
		||||
        | {
 | 
			
		||||
            type: 'done.invoke.read-projects'
 | 
			
		||||
            data: Project[]
 | 
			
		||||
          }
 | 
			
		||||
        | { type: 'assign'; data: { [key: string]: any } },
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    predictableActionArguments: true,
 | 
			
		||||
    preserveActionOrder: true,
 | 
			
		||||
    tsTypes: {} as import('./homeMachine.typegen').Typegen0,
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    actions: {
 | 
			
		||||
      setProjects: assign((_, event) => {
 | 
			
		||||
        return { projects: event.data as Project[] }
 | 
			
		||||
      }),
 | 
			
		||||
  actions: {
 | 
			
		||||
    setProjects: assign({
 | 
			
		||||
      projects: ({ context, event }) =>
 | 
			
		||||
        'output' in event ? event.output : context.projects,
 | 
			
		||||
    }),
 | 
			
		||||
    toastSuccess: () => {},
 | 
			
		||||
    toastError: () => {},
 | 
			
		||||
    navigateToProject: () => {},
 | 
			
		||||
  },
 | 
			
		||||
  actors: {
 | 
			
		||||
    readProjects: fromPromise(() => Promise.resolve([] as Project[])),
 | 
			
		||||
    createProject: fromPromise((_: { input: { name: string } }) =>
 | 
			
		||||
      Promise.resolve('')
 | 
			
		||||
    ),
 | 
			
		||||
    renameProject: fromPromise(
 | 
			
		||||
      (_: {
 | 
			
		||||
        input: {
 | 
			
		||||
          oldName: string
 | 
			
		||||
          newName: string
 | 
			
		||||
          defaultProjectName: string
 | 
			
		||||
          defaultDirectory: string
 | 
			
		||||
        }
 | 
			
		||||
      }) => Promise.resolve('')
 | 
			
		||||
    ),
 | 
			
		||||
    deleteProject: fromPromise(
 | 
			
		||||
      (_: { input: { defaultDirectory: string; name: string } }) =>
 | 
			
		||||
        Promise.resolve('')
 | 
			
		||||
    ),
 | 
			
		||||
  },
 | 
			
		||||
  guards: {
 | 
			
		||||
    'Has at least 1 project': () => false,
 | 
			
		||||
  },
 | 
			
		||||
}).createMachine({
 | 
			
		||||
  /** @xstate-layout N4IgpgJg5mDOIC5QAkD2BbMACdBDAxgBYCWAdmAHTK6xampYAOATqgFZj4AusAxAMLMwuLthbtOXANoAGALqJQjVLGJdiqUopAAPRAHYAbPooAWABwBGUwE5zAJgeGArM-MAaEAE9EN0wGYKGX97GX1nGVNDS0MbfwBfeM80TBwCEnIqGiZWDm4+ACUwUlxU8TzpeW1lVXVNbT0EcJNg02d-fzt7fU77Tx8EQ0iKCPtnfUsjGRtLGXtE5IxsPCIySmpacsk+QWFRHIluWQUkEBq1DS1TxqN7ChjzOxtXf0t7a37EcwsRibH-ZzRezA8wLEApZbpNZZTa5ba8AAiYAANmB9lsjlVTuc6ldQDdDOYKP5bm0os5TDJDJ8mlEzPpzIZHA4bO9umCIWlVpkNgcKnwAPKMYp8yTHaoqC71a6IEmBUz6BkWZzWDq2Uw0qzOIJAwz+PXWfSmeZJcFLLkZSi7ERkKCi7i8CCaShkABuqAA1pR8EIRGAALQYyonJSS3ENRDA2wUeyvd6dPVhGw0-RhGOp8IA8xGFkc80rS0Ua3qUh2oO8MDMVjMCiMZEiABmqGY6AoPr2AaD4uxYcuEYQoQpQWNNjsMnMgLGKbT3TC7TcOfsNjzqQL0KKJXQtvtXEdzoobs9lCEm87cMxIbOvel+MQqtMQRmS5ks31sZpAUsZkcIX+cQZJIrpC3KUBupTbuWlbVrW9ZcE2LYUCepRnocwYSrUfYyggbzvBQ+jMq49imLYwTUt4iCft+5i-u0-7UfoQEWtCSKoiWZbnruTqZIeXoUBAKJoihFTdqGGE3rod7UdqsQTI8hiGAqrIauRA7RvYeoqhO1jtAqjFrpkLFohBHEVlWzYwY2zatvxrFCWKWKiVKeISdh4yBJE-jGs4fhhA4zg0kRNgxhplhaW0nn4XpUKZEUuAQMZqF8FxLqkO6vG+hAgYcbAIlXmJzmNERdy0RYNiKgpthxDSEU6q8MSTJYjWGFFIEULF8WljuSX7jxx7CJlQY5ZYl44pht4IP61gyPc8njt0lIuH51UKrVVITEyMy2C1hbtQl-KmdBdaWQhGVZYluWjeJjSTf402shMEyuEyljPAFL0UNmMiuN86lWHMiSmvQ-HwKcnL6WA6FOf2k3mESMRDA4RpUm4U4qf6gSEt0QIvvqfjOCaiyrtF6zZPQXWQ+GWFlUEsbmNMf1TV9NLeXDcqRIySnNaaYPEzC5M9vl-b+IyFCjupryPF9jKWP5Kks-cbMWLERHRNt0LFntkgU2NLk4dqsz43YsTK++Kk2C+MbTOOcxzOMrhqzFxTgZ1Qba1dd6BUE1jGsLMxxK9KlDNqm3tMLUQvqYlgO5QhlsTubsFXesTTUuPTfHExshDS0RftRftGgEnTZtHbX9Zr+QJ-2S4Y3qnmTC+4tMyp1EfeOnmeQqdOhyXQrFOXXCV1hCkmLDOnBJYvRRDSsyRzGjiKj0lKdAkANAA */
 | 
			
		||||
  id: 'Home machine',
 | 
			
		||||
 | 
			
		||||
  initial: 'Reading projects',
 | 
			
		||||
 | 
			
		||||
  context: {
 | 
			
		||||
    projects: [],
 | 
			
		||||
    defaultProjectName: '',
 | 
			
		||||
    defaultDirectory: '',
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  on: {
 | 
			
		||||
    assign: {
 | 
			
		||||
      actions: assign(({ event }) => ({
 | 
			
		||||
        ...event.data,
 | 
			
		||||
      })),
 | 
			
		||||
      target: '.Reading projects',
 | 
			
		||||
    },
 | 
			
		||||
  }
 | 
			
		||||
)
 | 
			
		||||
  },
 | 
			
		||||
  states: {
 | 
			
		||||
    'Has no projects': {
 | 
			
		||||
      on: {
 | 
			
		||||
        'Create project': {
 | 
			
		||||
          target: 'Creating project',
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    'Has projects': {
 | 
			
		||||
      on: {
 | 
			
		||||
        'Rename project': {
 | 
			
		||||
          target: 'Renaming project',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        'Create project': {
 | 
			
		||||
          target: 'Creating project',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        'Delete project': {
 | 
			
		||||
          target: 'Deleting project',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        'Open project': {
 | 
			
		||||
          target: 'Opening project',
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    'Creating project': {
 | 
			
		||||
      invoke: {
 | 
			
		||||
        id: 'create-project',
 | 
			
		||||
        src: 'createProject',
 | 
			
		||||
        input: ({ event }) => {
 | 
			
		||||
          if (event.type !== 'Create project') {
 | 
			
		||||
            return {
 | 
			
		||||
              name: '',
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
          return {
 | 
			
		||||
            name: event.data.name,
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        onDone: [
 | 
			
		||||
          {
 | 
			
		||||
            target: 'Reading projects',
 | 
			
		||||
            actions: ['toastSuccess'],
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
        onError: [
 | 
			
		||||
          {
 | 
			
		||||
            target: 'Reading projects',
 | 
			
		||||
            actions: ['toastError'],
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    'Renaming project': {
 | 
			
		||||
      invoke: {
 | 
			
		||||
        id: 'rename-project',
 | 
			
		||||
        src: 'renameProject',
 | 
			
		||||
        input: ({ event, context }) => {
 | 
			
		||||
          if (event.type !== 'Rename project') {
 | 
			
		||||
            // This is to make TS happy
 | 
			
		||||
            return {
 | 
			
		||||
              defaultProjectName: context.defaultProjectName,
 | 
			
		||||
              defaultDirectory: context.defaultDirectory,
 | 
			
		||||
              oldName: '',
 | 
			
		||||
              newName: '',
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
          return {
 | 
			
		||||
            defaultProjectName: context.defaultProjectName,
 | 
			
		||||
            defaultDirectory: context.defaultDirectory,
 | 
			
		||||
            oldName: event.data.oldName,
 | 
			
		||||
            newName: event.data.newName,
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        onDone: [
 | 
			
		||||
          {
 | 
			
		||||
            target: '#Home machine.Reading projects',
 | 
			
		||||
            actions: ['toastSuccess'],
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
        onError: [
 | 
			
		||||
          {
 | 
			
		||||
            target: '#Home machine.Reading projects',
 | 
			
		||||
            actions: ['toastError'],
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    'Deleting project': {
 | 
			
		||||
      invoke: {
 | 
			
		||||
        id: 'delete-project',
 | 
			
		||||
        src: 'deleteProject',
 | 
			
		||||
        input: ({ event, context }) => {
 | 
			
		||||
          if (event.type !== 'Delete project') {
 | 
			
		||||
            // This is to make TS happy
 | 
			
		||||
            return {
 | 
			
		||||
              defaultDirectory: context.defaultDirectory,
 | 
			
		||||
              name: '',
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
          return {
 | 
			
		||||
            defaultDirectory: context.defaultDirectory,
 | 
			
		||||
            name: event.data.name,
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        onDone: [
 | 
			
		||||
          {
 | 
			
		||||
            actions: ['toastSuccess'],
 | 
			
		||||
            target: '#Home machine.Reading projects',
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
        onError: {
 | 
			
		||||
          actions: ['toastError'],
 | 
			
		||||
          target: '#Home machine.Has projects',
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    'Reading projects': {
 | 
			
		||||
      invoke: {
 | 
			
		||||
        id: 'read-projects',
 | 
			
		||||
        src: 'readProjects',
 | 
			
		||||
        onDone: [
 | 
			
		||||
          {
 | 
			
		||||
            guard: 'Has at least 1 project',
 | 
			
		||||
            target: 'Has projects',
 | 
			
		||||
            actions: ['setProjects'],
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            target: 'Has no projects',
 | 
			
		||||
            actions: ['setProjects'],
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
        onError: [
 | 
			
		||||
          {
 | 
			
		||||
            target: 'Has no projects',
 | 
			
		||||
            actions: ['toastError'],
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    'Opening project': {
 | 
			
		||||
      entry: ['navigateToProject'],
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							@ -1,4 +1,4 @@
 | 
			
		||||
import { assign, createMachine } from 'xstate'
 | 
			
		||||
import { assign, setup } from 'xstate'
 | 
			
		||||
import { Themes, getSystemTheme, setThemeClass } from 'lib/theme'
 | 
			
		||||
import { createSettings, settings } from 'lib/settings/initialSettings'
 | 
			
		||||
import {
 | 
			
		||||
@ -9,177 +9,187 @@ import {
 | 
			
		||||
  WildcardSetEvent,
 | 
			
		||||
} from 'lib/settings/settingsTypes'
 | 
			
		||||
 | 
			
		||||
export const settingsMachine = createMachine(
 | 
			
		||||
  {
 | 
			
		||||
    /** @xstate-layout N4IgpgJg5mDOIC5QGUwBc0EsB2VYDpMIAbMAYlnXwEMAHW-Ae2wCNHqAnCHKZNatAFdYAbQAMAXUShajWJizNpIAB6IALAFYAnPgBMARgDsBsQDY969QGYjmzQBoQAT0SnrADnwePY61r0PAwNtMyMAX3CnVAweAiJSCio6BjQACzAAWzAAYUZiRg5xKSQQWXlFbGU1BD1PfFtfE3UzTUNNaydXBCD1b209PTEPTTMtdQNNSOj0LFx4knJKNHxMxggwYh58DYAzakFiNABVbAVi5XKFTCVSmusxPXx7bRt1DzMxI3UjD3UutwhAz4MyeHxiV5+AYRKIgGJzPCERZJFYpfDpLJgC6lK6VaqIExPMwWGwdGxBPRmAE9PSafCPMQ-EzWbQ6ELTOGzOJIxLLVbrTbbNKYKBpLaitAAUWgcGxMjk11uoBqVmBH0ZLKCrVs-xciCCwLCvhCjyMFhGHPh3IS5AASnB0AACZYI0SSS4KvF3AlafADRl1YZ2IxiRx6hBtIzPb7abQ+DxGaxmYKWrnzHnkGKO6jEYjOtN4OVlT03KrehAtOnm7Qaup6Ixm6mR6OaR4dAwjM1mVOxdM2lH8jZbXD4WBpRgAd2QAGMc2AAOIcIhF3Gl-EIRPA6yGcyh4whSnU0xGJ5GAat0OfFowma9xH9gBUK5LStUiECdMmfx+mg8hmNTY-PgMYQpoZoxh41g9q6+C0GAHDyLACL5nesBkBAzBgIQ2AAG6MAA1lhcEIZgSFWvMz4VGu5YALTbtYwEnj8HhxnooT1mG3QhmY-TmJ82gGCyjzaJEsLYAK8ClOReAelRr41HRJiMZYvysexdjUuohh+poBiGDuXzGKy0HWossmKmWyqIDR3zAZWLSahM2jWJ04YjDxHbDMmmhaYE3wmemxGIchLpxOZXpWQgNEjMB1h6WEYHqK8ZgJk2EL6N8wR1Cy-gJqJ4RAA */
 | 
			
		||||
    id: 'Settings',
 | 
			
		||||
    predictableActionArguments: true,
 | 
			
		||||
export const settingsMachine = setup({
 | 
			
		||||
  types: {
 | 
			
		||||
    context: {} as ReturnType<typeof createSettings>,
 | 
			
		||||
    initial: 'idle',
 | 
			
		||||
    states: {
 | 
			
		||||
      idle: {
 | 
			
		||||
        entry: ['setThemeClass', 'setClientSideSceneUnits'],
 | 
			
		||||
    input: {} as ReturnType<typeof createSettings>,
 | 
			
		||||
    events: {} as
 | 
			
		||||
      | WildcardSetEvent<SettingsPaths>
 | 
			
		||||
      | SetEventTypes
 | 
			
		||||
      | {
 | 
			
		||||
          type: 'set.app.theme'
 | 
			
		||||
          data: { level: SettingsLevel; value: Themes }
 | 
			
		||||
        }
 | 
			
		||||
      | {
 | 
			
		||||
          type: 'set.modeling.units'
 | 
			
		||||
          data: { level: SettingsLevel; value: BaseUnit }
 | 
			
		||||
        }
 | 
			
		||||
      | { type: 'Reset settings'; defaultDirectory: string }
 | 
			
		||||
      | { type: 'Set all settings'; settings: typeof settings },
 | 
			
		||||
  },
 | 
			
		||||
  actions: {
 | 
			
		||||
    setEngineTheme: () => {},
 | 
			
		||||
    setClientTheme: () => {},
 | 
			
		||||
    'Execute AST': () => {},
 | 
			
		||||
    toastSuccess: () => {},
 | 
			
		||||
    setEngineEdges: () => {},
 | 
			
		||||
    setEngineScaleGridVisibility: () => {},
 | 
			
		||||
    setClientSideSceneUnits: () => {},
 | 
			
		||||
    persistSettings: () => {},
 | 
			
		||||
    resetSettings: assign(({ context, event }) => {
 | 
			
		||||
      if (!('defaultDirectory' in event)) return {}
 | 
			
		||||
      // Reset everything except onboarding status,
 | 
			
		||||
      // which should be preserved
 | 
			
		||||
      const newSettings = createSettings()
 | 
			
		||||
      if (context.app.onboardingStatus.user) {
 | 
			
		||||
        newSettings.app.onboardingStatus.user =
 | 
			
		||||
          context.app.onboardingStatus.user
 | 
			
		||||
      }
 | 
			
		||||
      // We instead pass in the default directory since it's asynchronous
 | 
			
		||||
      // to re-initialize, and that can be done by the caller.
 | 
			
		||||
      newSettings.app.projectDirectory.default = event.defaultDirectory
 | 
			
		||||
 | 
			
		||||
        on: {
 | 
			
		||||
          '*': {
 | 
			
		||||
            target: 'persisting settings',
 | 
			
		||||
            actions: ['setSettingAtLevel', 'toastSuccess'],
 | 
			
		||||
          },
 | 
			
		||||
      return newSettings
 | 
			
		||||
    }),
 | 
			
		||||
    setAllSettings: assign(({ event }) => {
 | 
			
		||||
      if (!('settings' in event)) return {}
 | 
			
		||||
      return event.settings
 | 
			
		||||
    }),
 | 
			
		||||
    setSettingAtLevel: assign(({ context, event }) => {
 | 
			
		||||
      if (!('data' in event)) return {}
 | 
			
		||||
      const { level, value } = event.data
 | 
			
		||||
      const [category, setting] = event.type
 | 
			
		||||
        .replace(/^set./, '')
 | 
			
		||||
        .split('.') as [keyof typeof settings, string]
 | 
			
		||||
 | 
			
		||||
          'set.app.onboardingStatus': {
 | 
			
		||||
            target: 'persisting settings',
 | 
			
		||||
      // @ts-ignore
 | 
			
		||||
      context[category][setting][level] = value
 | 
			
		||||
 | 
			
		||||
            // No toast
 | 
			
		||||
            actions: ['setSettingAtLevel'],
 | 
			
		||||
          },
 | 
			
		||||
 | 
			
		||||
          'set.app.themeColor': {
 | 
			
		||||
            target: 'persisting settings',
 | 
			
		||||
 | 
			
		||||
            // No toast
 | 
			
		||||
            actions: ['setSettingAtLevel'],
 | 
			
		||||
          },
 | 
			
		||||
 | 
			
		||||
          'set.modeling.defaultUnit': {
 | 
			
		||||
            target: 'persisting settings',
 | 
			
		||||
 | 
			
		||||
            actions: [
 | 
			
		||||
              'setSettingAtLevel',
 | 
			
		||||
              'toastSuccess',
 | 
			
		||||
              'setClientSideSceneUnits',
 | 
			
		||||
              'Execute AST',
 | 
			
		||||
            ],
 | 
			
		||||
          },
 | 
			
		||||
 | 
			
		||||
          'set.app.theme': {
 | 
			
		||||
            target: 'persisting settings',
 | 
			
		||||
 | 
			
		||||
            actions: [
 | 
			
		||||
              'setSettingAtLevel',
 | 
			
		||||
              'toastSuccess',
 | 
			
		||||
              'setThemeClass',
 | 
			
		||||
              'setEngineTheme',
 | 
			
		||||
              'setClientTheme',
 | 
			
		||||
            ],
 | 
			
		||||
          },
 | 
			
		||||
 | 
			
		||||
          'set.app.streamIdleMode': {
 | 
			
		||||
            target: 'persisting settings',
 | 
			
		||||
 | 
			
		||||
            actions: ['setSettingAtLevel', 'toastSuccess'],
 | 
			
		||||
          },
 | 
			
		||||
 | 
			
		||||
          'set.modeling.highlightEdges': {
 | 
			
		||||
            target: 'persisting settings',
 | 
			
		||||
 | 
			
		||||
            actions: ['setSettingAtLevel', 'toastSuccess', 'setEngineEdges'],
 | 
			
		||||
          },
 | 
			
		||||
 | 
			
		||||
          'Reset settings': {
 | 
			
		||||
            target: 'persisting settings',
 | 
			
		||||
 | 
			
		||||
            actions: [
 | 
			
		||||
              'resetSettings',
 | 
			
		||||
              'setThemeClass',
 | 
			
		||||
              'setEngineTheme',
 | 
			
		||||
              'setClientSideSceneUnits',
 | 
			
		||||
              'Execute AST',
 | 
			
		||||
              'setClientTheme',
 | 
			
		||||
            ],
 | 
			
		||||
          },
 | 
			
		||||
 | 
			
		||||
          'Set all settings': {
 | 
			
		||||
            actions: [
 | 
			
		||||
              'setAllSettings',
 | 
			
		||||
              'setThemeClass',
 | 
			
		||||
              'setEngineTheme',
 | 
			
		||||
              'setClientSideSceneUnits',
 | 
			
		||||
              'Execute AST',
 | 
			
		||||
              'setClientTheme',
 | 
			
		||||
            ],
 | 
			
		||||
          },
 | 
			
		||||
 | 
			
		||||
          'set.modeling.showScaleGrid': {
 | 
			
		||||
            target: 'persisting settings',
 | 
			
		||||
            actions: [
 | 
			
		||||
              'setSettingAtLevel',
 | 
			
		||||
              'toastSuccess',
 | 
			
		||||
              'setEngineScaleGridVisibility',
 | 
			
		||||
            ],
 | 
			
		||||
          },
 | 
			
		||||
      const newContext = {
 | 
			
		||||
        ...context,
 | 
			
		||||
        [category]: {
 | 
			
		||||
          ...context[category],
 | 
			
		||||
          // @ts-ignore
 | 
			
		||||
          [setting]: context[category][setting],
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      'persisting settings': {
 | 
			
		||||
        invoke: {
 | 
			
		||||
          src: 'Persist settings',
 | 
			
		||||
          id: 'persistSettings',
 | 
			
		||||
          onDone: 'idle',
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    tsTypes: {} as import('./settingsMachine.typegen').Typegen0,
 | 
			
		||||
    schema: {
 | 
			
		||||
      events: {} as
 | 
			
		||||
        | WildcardSetEvent<SettingsPaths>
 | 
			
		||||
        | SetEventTypes
 | 
			
		||||
        | {
 | 
			
		||||
            type: 'set.app.theme'
 | 
			
		||||
            data: { level: SettingsLevel; value: Themes }
 | 
			
		||||
          }
 | 
			
		||||
        | {
 | 
			
		||||
            type: 'set.modeling.units'
 | 
			
		||||
            data: { level: SettingsLevel; value: BaseUnit }
 | 
			
		||||
          }
 | 
			
		||||
        | { type: 'Reset settings'; defaultDirectory: string }
 | 
			
		||||
        | { type: 'Set all settings'; settings: typeof settings },
 | 
			
		||||
      return newContext
 | 
			
		||||
    }),
 | 
			
		||||
    setThemeClass: ({ context }) => {
 | 
			
		||||
      const currentTheme = context.app.theme.current ?? Themes.System
 | 
			
		||||
      setThemeClass(
 | 
			
		||||
        currentTheme === Themes.System ? getSystemTheme() : currentTheme
 | 
			
		||||
      )
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    actions: {
 | 
			
		||||
      resetSettings: assign((context, { defaultDirectory }) => {
 | 
			
		||||
        // Reset everything except onboarding status,
 | 
			
		||||
        // which should be preserved
 | 
			
		||||
        const newSettings = createSettings()
 | 
			
		||||
        if (context.app.onboardingStatus.user) {
 | 
			
		||||
          newSettings.app.onboardingStatus.user =
 | 
			
		||||
            context.app.onboardingStatus.user
 | 
			
		||||
        }
 | 
			
		||||
        // We instead pass in the default directory since it's asynchronous
 | 
			
		||||
        // to re-initialize, and that can be done by the caller.
 | 
			
		||||
        newSettings.app.projectDirectory.default = defaultDirectory
 | 
			
		||||
}).createMachine({
 | 
			
		||||
  /** @xstate-layout N4IgpgJg5mDOIC5QGUwBc0EsB2VYDpMIAbMAYlnXwEMAHW-Ae2wCNHqAnCHKZNatAFdYAbQAMAXUShajWJizNpIAB6IALAFYAnPgBMARgDsBsQDY969QGYjmzQBoQAT0SnrADnwePY61r0PAwNtMyMAX3CnVAweAiJSCio6BjQACzAAWzAAYUZiRg5xKSQQWXlFbGU1BD1PfFtfE3UzTUNNaydXBCD1b209PTEPTTMtdQNNSOj0LFx4knJKNHxMxggwYh58DYAzakFiNABVbAVi5XKFTCVSmusxPXx7bRt1DzMxI3UjD3UutwhAz4MyeHxiV5+AYRKIgGJzPCERZJFYpfDpLJgC6lK6VaqIExPMwWGwdGxBPRmAE9PSafCPMQ-EzWbQ6ELTOGzOJIxLLVbrTbbNKYKBpLaitAAUWgcGxMjk11uoBqVmBH0ZLKCrVs-xciCCwLCvhCjyMFhGHPh3IS5AASnB0AACZYI0SSS4KvF3AlafADRl1YZ2IxiRx6hBtIzPb7abQ+DxGaxmYKWrnzHnkGKO6jEYjOtN4OVlT03KrehAtOnm7Qaup6Ixm6mR6OaR4dAwjM1mVOxdM2lH8jZbXD4WBpRgAd2QAGMc2AAOIcIhF3Gl-EIRPA6yGcyh4whSnU0xGJ5GAat0OfFowma9xH9gBUK5LStUiECdMmfx+mg8hmNTY-PgMYQpoZoxh41g9q6+C0GAHDyLACL5nesBkBAzBgIQ2AAG6MAA1lhcEIZgSFWvMz4VGu5YALTbtYwEnj8HhxnooT1mG3QhmY-TmJ82gGCyjzaJEsLYAK8ClOReAelRr41HRJiMZYvysexdjUuohh+poBiGDuXzGKy0HWossmKmWyqIDR3zAZWLSahM2jWJ04YjDxHbDMmmhaYE3wmemxGIchLpxOZXpWQgNEjMB1h6WEYHqK8ZgJk2EL6N8wR1Cy-gJqJ4RAA */
 | 
			
		||||
  id: 'Settings',
 | 
			
		||||
  initial: 'idle',
 | 
			
		||||
  context: ({ input }) => {
 | 
			
		||||
    return {
 | 
			
		||||
      ...createSettings(),
 | 
			
		||||
      ...input,
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  states: {
 | 
			
		||||
    idle: {
 | 
			
		||||
      entry: ['setThemeClass', 'setClientSideSceneUnits'],
 | 
			
		||||
 | 
			
		||||
        return newSettings
 | 
			
		||||
      }),
 | 
			
		||||
      setAllSettings: assign((_, event) => {
 | 
			
		||||
        return event.settings
 | 
			
		||||
      }),
 | 
			
		||||
      setSettingAtLevel: assign((context, event) => {
 | 
			
		||||
        const { level, value } = event.data
 | 
			
		||||
        const [category, setting] = event.type
 | 
			
		||||
          .replace(/^set./, '')
 | 
			
		||||
          .split('.') as [keyof typeof settings, string]
 | 
			
		||||
      on: {
 | 
			
		||||
        '*': {
 | 
			
		||||
          target: 'persisting settings',
 | 
			
		||||
          actions: ['setSettingAtLevel', 'toastSuccess'],
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        // @ts-ignore
 | 
			
		||||
        context[category][setting][level] = value
 | 
			
		||||
        'set.app.onboardingStatus': {
 | 
			
		||||
          target: 'persisting settings',
 | 
			
		||||
 | 
			
		||||
        const newContext = {
 | 
			
		||||
          ...context,
 | 
			
		||||
          [category]: {
 | 
			
		||||
            ...context[category],
 | 
			
		||||
            // @ts-ignore
 | 
			
		||||
            [setting]: context[category][setting],
 | 
			
		||||
          },
 | 
			
		||||
        }
 | 
			
		||||
          // No toast
 | 
			
		||||
          actions: ['setSettingAtLevel'],
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        return newContext
 | 
			
		||||
      }),
 | 
			
		||||
      setThemeClass: (context) => {
 | 
			
		||||
        const currentTheme = context.app.theme.current ?? Themes.System
 | 
			
		||||
        setThemeClass(
 | 
			
		||||
          currentTheme === Themes.System ? getSystemTheme() : currentTheme
 | 
			
		||||
        )
 | 
			
		||||
        'set.app.themeColor': {
 | 
			
		||||
          target: 'persisting settings',
 | 
			
		||||
 | 
			
		||||
          // No toast
 | 
			
		||||
          actions: ['setSettingAtLevel'],
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        'set.modeling.defaultUnit': {
 | 
			
		||||
          target: 'persisting settings',
 | 
			
		||||
 | 
			
		||||
          actions: [
 | 
			
		||||
            'setSettingAtLevel',
 | 
			
		||||
            'toastSuccess',
 | 
			
		||||
            'setClientSideSceneUnits',
 | 
			
		||||
            'Execute AST',
 | 
			
		||||
          ],
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        'set.app.theme': {
 | 
			
		||||
          target: 'persisting settings',
 | 
			
		||||
 | 
			
		||||
          actions: [
 | 
			
		||||
            'setSettingAtLevel',
 | 
			
		||||
            'toastSuccess',
 | 
			
		||||
            'setThemeClass',
 | 
			
		||||
            'setEngineTheme',
 | 
			
		||||
            'setClientTheme',
 | 
			
		||||
          ],
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        'set.app.streamIdleMode': {
 | 
			
		||||
          target: 'persisting settings',
 | 
			
		||||
 | 
			
		||||
          actions: ['setSettingAtLevel', 'toastSuccess'],
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        'set.modeling.highlightEdges': {
 | 
			
		||||
          target: 'persisting settings',
 | 
			
		||||
 | 
			
		||||
          actions: ['setSettingAtLevel', 'toastSuccess', 'setEngineEdges'],
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        'Reset settings': {
 | 
			
		||||
          target: 'persisting settings',
 | 
			
		||||
 | 
			
		||||
          actions: [
 | 
			
		||||
            'resetSettings',
 | 
			
		||||
            'setThemeClass',
 | 
			
		||||
            'setEngineTheme',
 | 
			
		||||
            'setClientSideSceneUnits',
 | 
			
		||||
            'Execute AST',
 | 
			
		||||
            'setClientTheme',
 | 
			
		||||
          ],
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        'Set all settings': {
 | 
			
		||||
          actions: [
 | 
			
		||||
            'setAllSettings',
 | 
			
		||||
            'setThemeClass',
 | 
			
		||||
            'setEngineTheme',
 | 
			
		||||
            'setClientSideSceneUnits',
 | 
			
		||||
            'Execute AST',
 | 
			
		||||
            'setClientTheme',
 | 
			
		||||
          ],
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        'set.modeling.showScaleGrid': {
 | 
			
		||||
          target: 'persisting settings',
 | 
			
		||||
          actions: [
 | 
			
		||||
            'setSettingAtLevel',
 | 
			
		||||
            'toastSuccess',
 | 
			
		||||
            'setEngineScaleGridVisibility',
 | 
			
		||||
          ],
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  }
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
    'persisting settings': {
 | 
			
		||||
      entry: ['persistSettings'],
 | 
			
		||||
      always: 'idle',
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
@ -14,7 +14,7 @@ import { type HomeLoaderData } from 'lib/types'
 | 
			
		||||
import Loading from 'components/Loading'
 | 
			
		||||
import { useMachine } from '@xstate/react'
 | 
			
		||||
import { homeMachine } from '../machines/homeMachine'
 | 
			
		||||
import { ContextFrom, EventFrom } from 'xstate'
 | 
			
		||||
import { fromPromise } from 'xstate'
 | 
			
		||||
import { PATHS } from 'lib/paths'
 | 
			
		||||
import {
 | 
			
		||||
  getNextSearchParams,
 | 
			
		||||
@ -68,95 +68,102 @@ const Home = () => {
 | 
			
		||||
  )
 | 
			
		||||
  const ref = useRef<HTMLDivElement>(null)
 | 
			
		||||
 | 
			
		||||
  const [state, send, actor] = useMachine(homeMachine, {
 | 
			
		||||
    context: {
 | 
			
		||||
      projects: loadedProjects,
 | 
			
		||||
      defaultProjectName: settings.projects.defaultProjectName.current,
 | 
			
		||||
      defaultDirectory: settings.app.projectDirectory.current,
 | 
			
		||||
    },
 | 
			
		||||
    actions: {
 | 
			
		||||
      navigateToProject: (
 | 
			
		||||
        context: ContextFrom<typeof homeMachine>,
 | 
			
		||||
        event: EventFrom<typeof homeMachine>
 | 
			
		||||
      ) => {
 | 
			
		||||
        if (event.data && 'name' in event.data) {
 | 
			
		||||
          let projectPath =
 | 
			
		||||
            context.defaultDirectory +
 | 
			
		||||
            window.electron.path.sep +
 | 
			
		||||
            event.data.name
 | 
			
		||||
          onProjectOpen(
 | 
			
		||||
            {
 | 
			
		||||
              name: event.data.name,
 | 
			
		||||
              path: projectPath,
 | 
			
		||||
            },
 | 
			
		||||
            null
 | 
			
		||||
          )
 | 
			
		||||
          commandBarSend({ type: 'Close' })
 | 
			
		||||
          navigate(`${PATHS.FILE}/${encodeURIComponent(projectPath)}`)
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      toastSuccess: (_, event) => toast.success((event.data || '') + ''),
 | 
			
		||||
      toastError: (_, event) => toast.error((event.data || '') + ''),
 | 
			
		||||
    },
 | 
			
		||||
    services: {
 | 
			
		||||
      readProjects: async (context: ContextFrom<typeof homeMachine>) =>
 | 
			
		||||
        listProjects(),
 | 
			
		||||
      createProject: async (
 | 
			
		||||
        context: ContextFrom<typeof homeMachine>,
 | 
			
		||||
        event: EventFrom<typeof homeMachine, 'Create project'>
 | 
			
		||||
      ) => {
 | 
			
		||||
        let name = (
 | 
			
		||||
          event.data && 'name' in event.data
 | 
			
		||||
            ? event.data.name
 | 
			
		||||
            : settings.projects.defaultProjectName.current
 | 
			
		||||
        ).trim()
 | 
			
		||||
 | 
			
		||||
        if (doesProjectNameNeedInterpolated(name)) {
 | 
			
		||||
          const nextIndex = getNextProjectIndex(name, projects)
 | 
			
		||||
          name = interpolateProjectNameWithIndex(name, nextIndex)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await createNewProjectDirectory(name)
 | 
			
		||||
 | 
			
		||||
        return `Successfully created "${name}"`
 | 
			
		||||
      },
 | 
			
		||||
      renameProject: async (
 | 
			
		||||
        context: ContextFrom<typeof homeMachine>,
 | 
			
		||||
        event: EventFrom<typeof homeMachine, 'Rename project'>
 | 
			
		||||
      ) => {
 | 
			
		||||
        const { oldName, newName } = event.data
 | 
			
		||||
        let name = newName ? newName : context.defaultProjectName
 | 
			
		||||
        if (doesProjectNameNeedInterpolated(name)) {
 | 
			
		||||
          const nextIndex = await getNextProjectIndex(name, projects)
 | 
			
		||||
          name = interpolateProjectNameWithIndex(name, nextIndex)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await renameProjectDirectory(
 | 
			
		||||
          window.electron.path.join(context.defaultDirectory, oldName),
 | 
			
		||||
          name
 | 
			
		||||
        )
 | 
			
		||||
        return `Successfully renamed "${oldName}" to "${name}"`
 | 
			
		||||
      },
 | 
			
		||||
      deleteProject: async (
 | 
			
		||||
        context: ContextFrom<typeof homeMachine>,
 | 
			
		||||
        event: EventFrom<typeof homeMachine, 'Delete project'>
 | 
			
		||||
      ) => {
 | 
			
		||||
        await window.electron.rm(
 | 
			
		||||
          window.electron.path.join(context.defaultDirectory, event.data.name),
 | 
			
		||||
          {
 | 
			
		||||
            recursive: true,
 | 
			
		||||
  const [state, send, actor] = useMachine(
 | 
			
		||||
    homeMachine.provide({
 | 
			
		||||
      actions: {
 | 
			
		||||
        navigateToProject: ({ context, event }) => {
 | 
			
		||||
          if ('data' in event && event.data && 'name' in event.data) {
 | 
			
		||||
            let projectPath =
 | 
			
		||||
              context.defaultDirectory +
 | 
			
		||||
              window.electron.path.sep +
 | 
			
		||||
              event.data.name
 | 
			
		||||
            onProjectOpen(
 | 
			
		||||
              {
 | 
			
		||||
                name: event.data.name,
 | 
			
		||||
                path: projectPath,
 | 
			
		||||
              },
 | 
			
		||||
              null
 | 
			
		||||
            )
 | 
			
		||||
            commandBarSend({ type: 'Close' })
 | 
			
		||||
            navigate(`${PATHS.FILE}/${encodeURIComponent(projectPath)}`)
 | 
			
		||||
          }
 | 
			
		||||
        )
 | 
			
		||||
        return `Successfully deleted "${event.data.name}"`
 | 
			
		||||
        },
 | 
			
		||||
        toastSuccess: ({ event }) =>
 | 
			
		||||
          toast.success(
 | 
			
		||||
            ('data' in event && typeof event.data === 'string' && event.data) ||
 | 
			
		||||
              ('output' in event &&
 | 
			
		||||
                typeof event.output === 'string' &&
 | 
			
		||||
                event.output) ||
 | 
			
		||||
              ''
 | 
			
		||||
          ),
 | 
			
		||||
        toastError: ({ event }) =>
 | 
			
		||||
          toast.error(
 | 
			
		||||
            ('data' in event && typeof event.data === 'string' && event.data) ||
 | 
			
		||||
              ('output' in event &&
 | 
			
		||||
                typeof event.output === 'string' &&
 | 
			
		||||
                event.output) ||
 | 
			
		||||
              ''
 | 
			
		||||
          ),
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    guards: {
 | 
			
		||||
      'Has at least 1 project': (_, event: EventFrom<typeof homeMachine>) => {
 | 
			
		||||
        if (event.type !== 'done.invoke.read-projects') return false
 | 
			
		||||
        return event?.data?.length ? event.data?.length >= 1 : false
 | 
			
		||||
      actors: {
 | 
			
		||||
        readProjects: fromPromise(() => listProjects()),
 | 
			
		||||
        createProject: fromPromise(async ({ input }) => {
 | 
			
		||||
          let name = (
 | 
			
		||||
            input && 'name' in input && input.name
 | 
			
		||||
              ? input.name
 | 
			
		||||
              : settings.projects.defaultProjectName.current
 | 
			
		||||
          ).trim()
 | 
			
		||||
 | 
			
		||||
          if (doesProjectNameNeedInterpolated(name)) {
 | 
			
		||||
            const nextIndex = getNextProjectIndex(name, projects)
 | 
			
		||||
            name = interpolateProjectNameWithIndex(name, nextIndex)
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          await createNewProjectDirectory(name)
 | 
			
		||||
 | 
			
		||||
          return `Successfully created "${name}"`
 | 
			
		||||
        }),
 | 
			
		||||
        renameProject: fromPromise(async ({ input }) => {
 | 
			
		||||
          const { oldName, newName, defaultProjectName, defaultDirectory } =
 | 
			
		||||
            input
 | 
			
		||||
          let name = newName ? newName : defaultProjectName
 | 
			
		||||
          if (doesProjectNameNeedInterpolated(name)) {
 | 
			
		||||
            const nextIndex = await getNextProjectIndex(name, projects)
 | 
			
		||||
            name = interpolateProjectNameWithIndex(name, nextIndex)
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          await renameProjectDirectory(
 | 
			
		||||
            window.electron.path.join(defaultDirectory, oldName),
 | 
			
		||||
            name
 | 
			
		||||
          )
 | 
			
		||||
          return `Successfully renamed "${oldName}" to "${name}"`
 | 
			
		||||
        }),
 | 
			
		||||
        deleteProject: fromPromise(async ({ input }) => {
 | 
			
		||||
          await window.electron.rm(
 | 
			
		||||
            window.electron.path.join(input.defaultDirectory, input.name),
 | 
			
		||||
            {
 | 
			
		||||
              recursive: true,
 | 
			
		||||
            }
 | 
			
		||||
          )
 | 
			
		||||
          return `Successfully deleted "${input.name}"`
 | 
			
		||||
        }),
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  })
 | 
			
		||||
      guards: {
 | 
			
		||||
        'Has at least 1 project': ({ event }) => {
 | 
			
		||||
          if (event.type !== 'xstate.done.actor.read-projects') return false
 | 
			
		||||
          console.log(`from has at least 1 project: ${event.output.length}`)
 | 
			
		||||
          return event.output.length ? event.output.length >= 1 : false
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    }),
 | 
			
		||||
    {
 | 
			
		||||
      input: {
 | 
			
		||||
        projects: loadedProjects,
 | 
			
		||||
        defaultProjectName: settings.projects.defaultProjectName.current,
 | 
			
		||||
        defaultDirectory: settings.app.projectDirectory.current,
 | 
			
		||||
      },
 | 
			
		||||
    }
 | 
			
		||||
  )
 | 
			
		||||
  const { projects } = state.context
 | 
			
		||||
  const [searchParams, setSearchParams] = useSearchParams()
 | 
			
		||||
  const { searchResults, query, setQuery } = useProjectSearch(projects)
 | 
			
		||||
@ -197,14 +204,18 @@ const Home = () => {
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    if (newProjectName !== project.name) {
 | 
			
		||||
      send('Rename project', {
 | 
			
		||||
        data: { oldName: project.name, newName: newProjectName },
 | 
			
		||||
      send({
 | 
			
		||||
        type: 'Rename project',
 | 
			
		||||
        data: { oldName: project.name, newName: newProjectName as string },
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async function handleDeleteProject(project: Project) {
 | 
			
		||||
    send('Delete project', { data: { name: project.name || '' } })
 | 
			
		||||
    send({
 | 
			
		||||
      type: 'Delete project',
 | 
			
		||||
      data: { name: project.name || '' },
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
@ -217,7 +228,9 @@ const Home = () => {
 | 
			
		||||
              <h1 className="text-3xl font-bold">Your Projects</h1>
 | 
			
		||||
              <ActionButton
 | 
			
		||||
                Element="button"
 | 
			
		||||
                onClick={() => send('Create project')}
 | 
			
		||||
                onClick={() =>
 | 
			
		||||
                  send({ type: 'Create project', data: { name: '' } })
 | 
			
		||||
                }
 | 
			
		||||
                className="group !bg-primary !text-chalkboard-10 !border-primary hover:shadow-inner hover:hue-rotate-15"
 | 
			
		||||
                iconStart={{
 | 
			
		||||
                  icon: 'plus',
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										50
									
								
								yarn.lock
									
									
									
									
									
								
							
							
						
						
									
										50
									
								
								yarn.lock
									
									
									
									
									
								
							@ -2952,13 +2952,13 @@
 | 
			
		||||
    "@babel/types" "^7.21.4"
 | 
			
		||||
    recast "^0.23.1"
 | 
			
		||||
 | 
			
		||||
"@xstate/react@^3.2.2":
 | 
			
		||||
  version "3.2.2"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@xstate/react/-/react-3.2.2.tgz#ddf0f9d75e2c19375b1e1b7335e72cb99762aed8"
 | 
			
		||||
  integrity sha512-feghXWLedyq8JeL13yda3XnHPZKwYDN5HPBLykpLeuNpr9178tQd2/3d0NrH6gSd0sG5mLuLeuD+ck830fgzLQ==
 | 
			
		||||
"@xstate/react@^4.1.1":
 | 
			
		||||
  version "4.1.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@xstate/react/-/react-4.1.1.tgz#2f580fc5f83d195f95b56df6cd8061c66660d9fa"
 | 
			
		||||
  integrity sha512-pFp/Y+bnczfaZ0V8B4LOhx3d6Gd71YKAPbzerGqydC2nsYN/mp7RZu3q/w6/kvI2hwR/jeDeetM7xc3JFZH2NA==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    use-isomorphic-layout-effect "^1.1.2"
 | 
			
		||||
    use-sync-external-store "^1.0.0"
 | 
			
		||||
    use-sync-external-store "^1.2.0"
 | 
			
		||||
 | 
			
		||||
"@xstate/tools-shared@^4.1.0":
 | 
			
		||||
  version "4.1.0"
 | 
			
		||||
@ -8757,16 +8757,7 @@ string-natural-compare@^3.0.1:
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4"
 | 
			
		||||
  integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw==
 | 
			
		||||
 | 
			
		||||
"string-width-cjs@npm:string-width@^4.2.0":
 | 
			
		||||
  version "4.2.3"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
 | 
			
		||||
  integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    emoji-regex "^8.0.0"
 | 
			
		||||
    is-fullwidth-code-point "^3.0.0"
 | 
			
		||||
    strip-ansi "^6.0.1"
 | 
			
		||||
 | 
			
		||||
"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
 | 
			
		||||
"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
 | 
			
		||||
  version "4.2.3"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
 | 
			
		||||
  integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
 | 
			
		||||
@ -8860,14 +8851,7 @@ string_decoder@~1.1.1:
 | 
			
		||||
  dependencies:
 | 
			
		||||
    safe-buffer "~5.1.0"
 | 
			
		||||
 | 
			
		||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
 | 
			
		||||
  version "6.0.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
 | 
			
		||||
  integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    ansi-regex "^5.0.1"
 | 
			
		||||
 | 
			
		||||
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
 | 
			
		||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
 | 
			
		||||
  version "6.0.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
 | 
			
		||||
  integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
 | 
			
		||||
@ -9412,7 +9396,7 @@ use-latest@^1.2.1:
 | 
			
		||||
  dependencies:
 | 
			
		||||
    use-isomorphic-layout-effect "^1.1.1"
 | 
			
		||||
 | 
			
		||||
use-sync-external-store@^1.0.0:
 | 
			
		||||
use-sync-external-store@^1.2.0:
 | 
			
		||||
  version "1.2.2"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz#c3b6390f3a30eba13200d2302dcdf1e7b57b2ef9"
 | 
			
		||||
  integrity sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==
 | 
			
		||||
@ -9741,16 +9725,7 @@ word-wrap@^1.2.3, word-wrap@^1.2.5:
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34"
 | 
			
		||||
  integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==
 | 
			
		||||
 | 
			
		||||
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
 | 
			
		||||
  version "7.0.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
 | 
			
		||||
  integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    ansi-styles "^4.0.0"
 | 
			
		||||
    string-width "^4.1.0"
 | 
			
		||||
    strip-ansi "^6.0.0"
 | 
			
		||||
 | 
			
		||||
wrap-ansi@^7.0.0:
 | 
			
		||||
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
 | 
			
		||||
  version "7.0.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
 | 
			
		||||
  integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
 | 
			
		||||
@ -9793,11 +9768,16 @@ xmlbuilder@>=11.0.1, xmlbuilder@^15.1.1:
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/xstate/-/xstate-5.0.0-beta.54.tgz#d80f1a9e43ad883a65fc9b399161bd39633bd9bf"
 | 
			
		||||
  integrity sha512-BTnCPBQ2iTKe4uCnHEe1hNx6VTbXU+5mQGybSQHOjTLiBi4Ryi+tL9T6N1tmqagvM8rfl4XRfvndogfWCWcdpw==
 | 
			
		||||
 | 
			
		||||
xstate@^4.33.4, xstate@^4.38.2:
 | 
			
		||||
xstate@^4.33.4:
 | 
			
		||||
  version "4.38.3"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/xstate/-/xstate-4.38.3.tgz#4e15e7ad3aa0ca1eea2010548a5379966d8f1075"
 | 
			
		||||
  integrity sha512-SH7nAaaPQx57dx6qvfcIgqKRXIh4L0A1iYEqim4s1u7c9VoCgzZc+63FY90AKU4ZzOC2cfJzTnpO4zK7fCUzzw==
 | 
			
		||||
 | 
			
		||||
xstate@^5.17.4:
 | 
			
		||||
  version "5.17.4"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/xstate/-/xstate-5.17.4.tgz#334ab2da123973634097f7ca48387ae1589c774e"
 | 
			
		||||
  integrity sha512-KM2FYVOUJ04HlOO4TY3wEXqoYPR/XsDu+ewm+IWw0vilXqND0jVfvv04tEFwp8Mkk7I/oHXM8t1Ex9xJyUS4ZA==
 | 
			
		||||
 | 
			
		||||
xterm-addon-fit@^0.5.0:
 | 
			
		||||
  version "0.5.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/xterm-addon-fit/-/xterm-addon-fit-0.5.0.tgz#2d51b983b786a97dcd6cde805e700c7f913bc596"
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user