/ src / ProjectOnboarding.tsx
ProjectOnboarding.tsx
  1  import * as React from 'react'
  2  import { OrderedList } from '@inkjs/ui'
  3  import { Box, Text } from 'ink'
  4  import {
  5    getCurrentProjectConfig,
  6    getGlobalConfig,
  7    saveCurrentProjectConfig,
  8    saveGlobalConfig,
  9  } from './utils/config.js'
 10  import { existsSync } from 'fs'
 11  import { join } from 'path'
 12  import { homedir } from 'os'
 13  import terminalSetup from './commands/terminalSetup.js'
 14  import { getTheme } from './utils/theme.js'
 15  import { RELEASE_NOTES } from './constants/releaseNotes.js'
 16  import { gt } from 'semver'
 17  import { isDirEmpty } from './utils/file.js'
 18  
 19  // Function to mark onboarding as complete
 20  export function markProjectOnboardingComplete(): void {
 21    const projectConfig = getCurrentProjectConfig()
 22    if (!projectConfig.hasCompletedProjectOnboarding) {
 23      saveCurrentProjectConfig({
 24        ...projectConfig,
 25        hasCompletedProjectOnboarding: true,
 26      })
 27    }
 28  }
 29  
 30  function markReleaseNotesSeen(): void {
 31    const config = getGlobalConfig()
 32    saveGlobalConfig({
 33      ...config,
 34      lastReleaseNotesSeen: MACRO.VERSION,
 35    })
 36  }
 37  
 38  type Props = {
 39    workspaceDir: string
 40  }
 41  
 42  export default function ProjectOnboarding({
 43    workspaceDir,
 44  }: Props): React.ReactNode {
 45    // Check if project onboarding has already been completed
 46    const projectConfig = getCurrentProjectConfig()
 47    const showOnboarding = !projectConfig.hasCompletedProjectOnboarding
 48  
 49    // Get previous version from config
 50    const config = getGlobalConfig()
 51    const previousVersion = config.lastReleaseNotesSeen
 52  
 53    // Get release notes to show
 54    let releaseNotesToShow: string[] = []
 55    if (!previousVersion || gt(MACRO.VERSION, previousVersion)) {
 56      releaseNotesToShow = RELEASE_NOTES[MACRO.VERSION] || []
 57    }
 58    const hasReleaseNotes = releaseNotesToShow.length > 0
 59  
 60    // Mark release notes as seen when they're displayed without onboarding
 61    React.useEffect(() => {
 62      if (hasReleaseNotes && !showOnboarding) {
 63        markReleaseNotesSeen()
 64      }
 65    }, [hasReleaseNotes, showOnboarding])
 66  
 67    // We only want to show either onboarding OR release notes (with preference for onboarding)
 68    // If there's no onboarding to show and no release notes, return null
 69    if (!showOnboarding && !hasReleaseNotes) {
 70      return null
 71    }
 72  
 73    // Load what we need for onboarding
 74    // NOTE: This whole component is staticly rendered Once
 75    const hasClaudeMd = existsSync(join(workspaceDir, 'CLAUDE.md'))
 76    const isWorkspaceDirEmpty = isDirEmpty(workspaceDir)
 77    const needsClaudeMd = !hasClaudeMd && !isWorkspaceDirEmpty
 78    const showTerminalTip =
 79      terminalSetup.isEnabled && !getGlobalConfig().shiftEnterKeyBindingInstalled
 80  
 81    const theme = getTheme()
 82  
 83    return (
 84      <Box flexDirection="column" gap={1} padding={1} paddingBottom={0}>
 85        {showOnboarding && (
 86          <>
 87            <Text color={theme.secondaryText}>Tips for getting started:</Text>
 88            <OrderedList>
 89              {/* Collect all the items that should be displayed */}
 90              {(() => {
 91                const items = []
 92  
 93                if (isWorkspaceDirEmpty) {
 94                  items.push(
 95                    <OrderedList.Item key="workspace">
 96                      <Text color={theme.secondaryText}>
 97                        Ask Claude to create a new app or clone a repository.
 98                      </Text>
 99                    </OrderedList.Item>,
100                  )
101                }
102                if (needsClaudeMd) {
103                  items.push(
104                    <OrderedList.Item key="claudemd">
105                      <Text color={theme.secondaryText}>
106                        Run <Text color={theme.text}>/init</Text> to create a
107                        CLAUDE.md file with instructions for Claude.
108                      </Text>
109                    </OrderedList.Item>,
110                  )
111                }
112  
113                if (showTerminalTip) {
114                  items.push(
115                    <OrderedList.Item key="terminal">
116                      <Text color={theme.secondaryText}>
117                        Run <Text color={theme.text}>/terminal-setup</Text>
118                        <Text bold={false}> to set up terminal integration</Text>
119                      </Text>
120                    </OrderedList.Item>,
121                  )
122                }
123  
124                items.push(
125                  <OrderedList.Item key="questions">
126                    <Text color={theme.secondaryText}>
127                      Ask Claude questions about your codebase.
128                    </Text>
129                  </OrderedList.Item>,
130                )
131  
132                items.push(
133                  <OrderedList.Item key="changes">
134                    <Text color={theme.secondaryText}>
135                      Ask Claude to implement changes to your codebase.
136                    </Text>
137                  </OrderedList.Item>,
138                )
139  
140                return items
141              })()}
142            </OrderedList>
143          </>
144        )}
145  
146        {!showOnboarding && hasReleaseNotes && (
147          <Box
148            borderColor={getTheme().secondaryBorder}
149            flexDirection="column"
150            marginRight={1}
151          >
152            <Box flexDirection="column" gap={0}>
153              <Box marginBottom={1}>
154                <Text>🆕 What&apos;s new in v{MACRO.VERSION}:</Text>
155              </Box>
156              <Box flexDirection="column" marginLeft={1}>
157                {releaseNotesToShow.map((note, noteIndex) => (
158                  <Text key={noteIndex} color={getTheme().secondaryText}>
159                    • {note}
160                  </Text>
161                ))}
162              </Box>
163            </Box>
164          </Box>
165        )}
166  
167        {workspaceDir === homedir() && (
168          <Text color={getTheme().warning}>
169            Note: You have launched <Text bold>claude</Text> in your home
170            directory. For the best experience, launch it in a project directory
171            instead.
172          </Text>
173        )}
174      </Box>
175    )
176  }