Repository: BloopAI/bloop Branch: oss Commit: 431e9e82c5a2 Files: 624 Total size: 2.8 MB Directory structure: gitextract_xgg_tbnw/ ├── .dockerignore ├── .envrc ├── .eslintrc.json ├── .gitattributes ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ ├── feature_request.md │ │ └── request-for-comments.md │ └── workflows/ │ ├── build-on-pr-command.yml │ ├── build-on-pr.yml │ ├── build-on-release.yml │ ├── client-test.yml │ ├── dependencies.yml │ ├── dummy.yml │ ├── server-test.yml │ ├── tauri-release.yml │ └── tauri-test.yml ├── .gitignore ├── .gitpod.Dockerfile ├── .gitpod.yml ├── .taurignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Cargo.toml ├── Dockerfile ├── LICENSE ├── README.md ├── apps/ │ └── desktop/ │ ├── .gitignore │ ├── README.md │ ├── index.html │ ├── package.json │ ├── postcss.config.cjs │ ├── src/ │ │ ├── App.tsx │ │ ├── TextSearch.tsx │ │ ├── global.d.ts │ │ ├── main.tsx │ │ └── vite-env.d.ts │ ├── src-tauri/ │ │ ├── .gitignore │ │ ├── Cargo.toml │ │ ├── bin/ │ │ │ ├── qdrant-aarch64-apple-darwin │ │ │ ├── qdrant-x86_64-apple-darwin │ │ │ └── qdrant-x86_64-unknown-linux-gnu │ │ ├── build.rs │ │ ├── config/ │ │ │ └── config.json │ │ ├── dylibs/ │ │ │ └── .keep │ │ ├── frameworks/ │ │ │ └── .keep │ │ ├── icons/ │ │ │ └── icon.icns │ │ ├── installer.nsi │ │ ├── model/ │ │ │ ├── ggml/ │ │ │ │ └── tokenizer.json │ │ │ ├── model.onnx │ │ │ ├── special_tokens_map.json │ │ │ ├── tokenizer.json │ │ │ ├── tokenizer_config.json │ │ │ └── vocab.txt │ │ ├── src/ │ │ │ ├── QDRANT_CONFIG_TEMPLATE.yml │ │ │ ├── backend.rs │ │ │ ├── config.rs │ │ │ ├── main.rs │ │ │ └── qdrant.rs │ │ └── tauri.conf.json │ ├── tailwind.config.cjs │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts ├── client/ │ ├── .gitignore │ ├── .storybook/ │ │ ├── main.cjs │ │ ├── preview-head.html │ │ └── preview.cjs │ ├── README.md │ ├── index.html │ ├── jest.config.js │ ├── package.json │ ├── postcss.config.cjs │ ├── public/ │ │ ├── gray-to-blue.riv │ │ ├── gray-to-red.riv │ │ ├── like-blue.riv │ │ ├── like-red.riv │ │ └── like_button.riv │ ├── src/ │ │ ├── App.tsx │ │ ├── CloudApp.tsx │ │ ├── CommandBar/ │ │ │ ├── Body/ │ │ │ │ ├── Item.tsx │ │ │ │ ├── Section.tsx │ │ │ │ ├── SectionDivider.tsx │ │ │ │ └── index.tsx │ │ │ ├── Footer/ │ │ │ │ ├── HintButton.tsx │ │ │ │ └── index.tsx │ │ │ ├── Header/ │ │ │ │ ├── ChipItem.tsx │ │ │ │ └── index.tsx │ │ │ ├── Tutorial/ │ │ │ │ ├── TutorialBody.tsx │ │ │ │ └── TutorialTooltip.tsx │ │ │ ├── index.tsx │ │ │ └── steps/ │ │ │ ├── AddNewRepo.tsx │ │ │ ├── AddToStudio.tsx │ │ │ ├── CreateProject.tsx │ │ │ ├── Documentation/ │ │ │ │ ├── ActionsDropdown.tsx │ │ │ │ └── index.tsx │ │ │ ├── Initial.tsx │ │ │ ├── LocalRepos.tsx │ │ │ ├── ManageRepos/ │ │ │ │ ├── ActionsDropdown.tsx │ │ │ │ └── index.tsx │ │ │ ├── PrivateRepos/ │ │ │ │ ├── ActionsDropdown.tsx │ │ │ │ └── index.tsx │ │ │ ├── PublicRepos.tsx │ │ │ ├── SeachDocs.tsx │ │ │ ├── SeachFiles.tsx │ │ │ ├── ToggleTheme.tsx │ │ │ └── items/ │ │ │ ├── DocItem.tsx │ │ │ └── RepoItem.tsx │ │ ├── Project/ │ │ │ ├── CurrentTabContent/ │ │ │ │ ├── ChatTab/ │ │ │ │ │ ├── ActionsDropdown.tsx │ │ │ │ │ ├── ChatPersistentState.tsx │ │ │ │ │ ├── Conversation.tsx │ │ │ │ │ ├── DeprecatedClientModal.tsx │ │ │ │ │ ├── Input/ │ │ │ │ │ │ ├── ProseMirror/ │ │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ │ ├── mentionPlugin.ts │ │ │ │ │ │ │ ├── nodes.ts │ │ │ │ │ │ │ ├── placeholderPlugin.ts │ │ │ │ │ │ │ └── utils.ts │ │ │ │ │ │ ├── ReactMentions/ │ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── Message/ │ │ │ │ │ │ ├── LoadingStep.tsx │ │ │ │ │ │ ├── UserParsedQuery/ │ │ │ │ │ │ │ ├── LangChip.tsx │ │ │ │ │ │ │ ├── PathChip.tsx │ │ │ │ │ │ │ ├── RepoChip.tsx │ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── ScrollableContent.tsx │ │ │ │ │ ├── StarterMessage.tsx │ │ │ │ │ └── index.tsx │ │ │ │ ├── DocTab/ │ │ │ │ │ ├── ActionsDropdown.tsx │ │ │ │ │ ├── DocSection.tsx │ │ │ │ │ ├── RenderedSection.tsx │ │ │ │ │ └── index.tsx │ │ │ │ ├── DropTarget.tsx │ │ │ │ ├── EmptyTab.tsx │ │ │ │ ├── FileTab/ │ │ │ │ │ ├── ActionsDropdown.tsx │ │ │ │ │ └── index.tsx │ │ │ │ ├── Header/ │ │ │ │ │ ├── AddTabButton.tsx │ │ │ │ │ ├── AddTabDropdown.tsx │ │ │ │ │ ├── TabButton.tsx │ │ │ │ │ └── index.tsx │ │ │ │ ├── StudioTab/ │ │ │ │ │ ├── ActionsDropdown.tsx │ │ │ │ │ ├── Conversation/ │ │ │ │ │ │ ├── ContextError.tsx │ │ │ │ │ │ ├── GeneratedDiff.tsx │ │ │ │ │ │ ├── Input/ │ │ │ │ │ │ │ ├── TemplatesDropdown.tsx │ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ │ ├── NoFilesMessage.tsx │ │ │ │ │ │ ├── StarterMessage.tsx │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── DeprecatedClientModal.tsx │ │ │ │ │ ├── StudioPersistentState.tsx │ │ │ │ │ └── index.tsx │ │ │ │ └── index.tsx │ │ │ ├── EmptyProject.tsx │ │ │ ├── LeftSidebar/ │ │ │ │ ├── NavPanel/ │ │ │ │ │ ├── Conversations/ │ │ │ │ │ │ ├── ConversationsDropdown.tsx │ │ │ │ │ │ ├── CoversationEntry.tsx │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── Doc/ │ │ │ │ │ │ ├── DocDropdown.tsx │ │ │ │ │ │ ├── DocEntry.tsx │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── Repo/ │ │ │ │ │ │ ├── RepoDropdown.tsx │ │ │ │ │ │ ├── RepoEntry.tsx │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── Studios/ │ │ │ │ │ │ ├── AddContextFile.tsx │ │ │ │ │ │ ├── StudioEntry.tsx │ │ │ │ │ │ ├── StudioFile.tsx │ │ │ │ │ │ ├── StudioHistory.tsx │ │ │ │ │ │ ├── StudioSubItem.tsx │ │ │ │ │ │ ├── StudiosDropdown.tsx │ │ │ │ │ │ └── index.tsx │ │ │ │ │ └── index.tsx │ │ │ │ ├── RegexSearchPanel/ │ │ │ │ │ ├── AutocompleteMenu.tsx │ │ │ │ │ ├── AutocompleteMenuItem.tsx │ │ │ │ │ ├── Results/ │ │ │ │ │ │ ├── CodeLine.tsx │ │ │ │ │ │ ├── CodeResult.tsx │ │ │ │ │ │ ├── FileResult.tsx │ │ │ │ │ │ └── RepoResult.tsx │ │ │ │ │ └── index.tsx │ │ │ │ └── index.tsx │ │ │ ├── RightTab.tsx │ │ │ ├── TutorialCards.tsx │ │ │ └── index.tsx │ │ ├── ProjectSettings/ │ │ │ ├── General.tsx │ │ │ ├── Templates/ │ │ │ │ ├── ActionsDropdown.tsx │ │ │ │ ├── TemplateItem.tsx │ │ │ │ └── index.tsx │ │ │ └── index.tsx │ │ ├── Settings/ │ │ │ ├── General/ │ │ │ │ └── index.tsx │ │ │ ├── Preferences/ │ │ │ │ ├── ChatInputTypeDropdown.tsx │ │ │ │ ├── LanguageDropdown.tsx │ │ │ │ ├── ThemeDropdown.tsx │ │ │ │ └── index.tsx │ │ │ └── index.tsx │ │ ├── components/ │ │ │ ├── Badge/ │ │ │ │ └── index.tsx │ │ │ ├── Breadcrumbs/ │ │ │ │ ├── BreadcrumbSection.tsx │ │ │ │ ├── BreadcrumbsCollapsed.tsx │ │ │ │ ├── PathContainer.tsx │ │ │ │ └── index.tsx │ │ │ ├── Button/ │ │ │ │ ├── KeyHintButton.tsx │ │ │ │ └── index.tsx │ │ │ ├── Checkbox/ │ │ │ │ ├── Checkbox.stories.tsx │ │ │ │ └── index.tsx │ │ │ ├── Chips/ │ │ │ │ └── FileChip.tsx │ │ │ ├── Code/ │ │ │ │ ├── CodeBlockSearch/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── CodeDiff/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── CodeFragment/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── CodeFull/ │ │ │ │ │ ├── SelectionPopup.tsx │ │ │ │ │ ├── Token.tsx │ │ │ │ │ ├── VirtualizedCode.tsx │ │ │ │ │ └── index.tsx │ │ │ │ ├── CodeFullSelectable/ │ │ │ │ │ ├── CodeContainer.tsx │ │ │ │ │ ├── LazyLinesContainer.tsx │ │ │ │ │ ├── SelectionHandler.tsx │ │ │ │ │ ├── SelectionHint.tsx │ │ │ │ │ ├── SelectionRect.tsx │ │ │ │ │ └── index.tsx │ │ │ │ ├── CodeLine.tsx │ │ │ │ └── CodeToken.tsx │ │ │ ├── Dropdown/ │ │ │ │ ├── Section/ │ │ │ │ │ ├── SectionItem.tsx │ │ │ │ │ ├── SectionLabel.tsx │ │ │ │ │ └── index.tsx │ │ │ │ └── index.tsx │ │ │ ├── FileIcon/ │ │ │ │ └── index.tsx │ │ │ ├── Header/ │ │ │ │ ├── HeaderRightPart.tsx │ │ │ │ ├── ProjectsDropdown.tsx │ │ │ │ ├── UserDropdown.tsx │ │ │ │ └── index.tsx │ │ │ ├── IpynbRenderer/ │ │ │ │ ├── IpynbCell.tsx │ │ │ │ └── index.tsx │ │ │ ├── KeyboardHint/ │ │ │ │ ├── MultiKey.tsx │ │ │ │ └── index.tsx │ │ │ ├── Loaders/ │ │ │ │ ├── LiteLoader.tsx │ │ │ │ └── SpinnerLoader.tsx │ │ │ ├── MarkdownWithCode/ │ │ │ │ ├── CodeRenderer.tsx │ │ │ │ ├── CodeWithBreadcrumbs.tsx │ │ │ │ ├── CopyButton.tsx │ │ │ │ ├── DiffCode.tsx │ │ │ │ ├── FolderChip.tsx │ │ │ │ ├── LinkRenderer.tsx │ │ │ │ ├── NewCode.tsx │ │ │ │ └── index.tsx │ │ │ ├── Modal/ │ │ │ │ └── index.tsx │ │ │ ├── OverflowTracker/ │ │ │ │ └── index.tsx │ │ │ ├── RefsDefsPopup/ │ │ │ │ ├── Badge.tsx │ │ │ │ ├── RefDefFileItem.tsx │ │ │ │ ├── RefDefFileLine.tsx │ │ │ │ └── index.tsx │ │ │ ├── ScrollToBottom/ │ │ │ │ ├── Composer.tsx │ │ │ │ ├── EventSpy.ts │ │ │ │ ├── FunctionContext.ts │ │ │ │ ├── InternalContext.ts │ │ │ │ ├── Panel.tsx │ │ │ │ ├── SpineTo.tsx │ │ │ │ ├── debounce.ts │ │ │ │ └── index.tsx │ │ │ ├── SearchOnPage/ │ │ │ │ └── index.tsx │ │ │ ├── SectionsNav/ │ │ │ │ ├── SectionButton.tsx │ │ │ │ └── index.tsx │ │ │ ├── TextField/ │ │ │ │ └── index.tsx │ │ │ ├── TextInput/ │ │ │ │ ├── ClearButton/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── RegexButton/ │ │ │ │ │ └── index.tsx │ │ │ │ └── index.tsx │ │ │ ├── TokenUsage/ │ │ │ │ └── index.tsx │ │ │ └── Tooltip/ │ │ │ └── index.tsx │ │ ├── consts/ │ │ │ ├── animations.ts │ │ │ ├── code.ts │ │ │ ├── codeStudio.ts │ │ │ ├── commandBar.ts │ │ │ ├── general.ts │ │ │ ├── shortcuts.ts │ │ │ ├── tutorialSteps.ts │ │ │ └── validations.ts │ │ ├── context/ │ │ │ ├── arrowNavigationContext.ts │ │ │ ├── chatsContext.tsx │ │ │ ├── commandBarContext.ts │ │ │ ├── deviceContext.ts │ │ │ ├── fileHighlightsContext.ts │ │ │ ├── localeContext.ts │ │ │ ├── projectContext.ts │ │ │ ├── providers/ │ │ │ │ ├── ChatsContextProvider.tsx │ │ │ │ ├── CommandBarContextProvider.tsx │ │ │ │ ├── DeviceContextProvider.tsx │ │ │ │ ├── FileHighlightsContextProvider.tsx │ │ │ │ ├── ProjectContextProvider.tsx │ │ │ │ ├── RepositoriesContextProvider.tsx │ │ │ │ ├── StudiosContextProvider.tsx │ │ │ │ ├── TabsContextProvider.tsx │ │ │ │ └── UIContextProvider.tsx │ │ │ ├── repositoriesContext.ts │ │ │ ├── studiosContext.tsx │ │ │ ├── tabsContext.tsx │ │ │ └── uiContext.ts │ │ ├── file-icons.css │ │ ├── global.d.ts │ │ ├── hooks/ │ │ │ ├── useArrowNavigation.ts │ │ │ ├── useArrowNavigationItemProps.ts │ │ │ ├── useCodeSearch.ts │ │ │ ├── useComponentWillMount.ts │ │ │ ├── useDiffLines.ts │ │ │ ├── useEnterKey.ts │ │ │ ├── useGlobalShortcuts.ts │ │ │ ├── useKeyboardNavigation.ts │ │ │ ├── useNavPanel.ts │ │ │ ├── useOnClickOutsideHook.ts │ │ │ ├── usePersistentState.tsx │ │ │ ├── useResizeableWidth.ts │ │ │ ├── useScrollToBottom.ts │ │ │ ├── useShortcuts.ts │ │ │ └── useStateRef.ts │ │ ├── i18n.ts │ │ ├── icons/ │ │ │ ├── ArrowHistory.tsx │ │ │ ├── ArrowLeft.tsx │ │ │ ├── ArrowOut.tsx │ │ │ ├── ArrowTriangleBottom.tsx │ │ │ ├── BloopLogo.tsx │ │ │ ├── Branch.tsx │ │ │ ├── Broom.tsx │ │ │ ├── Bug.tsx │ │ │ ├── ChatBubbles.tsx │ │ │ ├── Check.tsx │ │ │ ├── CheckList.tsx │ │ │ ├── CheckmarkInSquare.tsx │ │ │ ├── ChevronDown.tsx │ │ │ ├── ChevronRight.tsx │ │ │ ├── ChevronUp.tsx │ │ │ ├── Clipboard.tsx │ │ │ ├── CloseSign.tsx │ │ │ ├── CloseSignInCircle.tsx │ │ │ ├── Code.tsx │ │ │ ├── CodeLineWithSparkle.tsx │ │ │ ├── CodeStudio.tsx │ │ │ ├── CodeSymbols/ │ │ │ │ ├── Class.tsx │ │ │ │ ├── Color.tsx │ │ │ │ ├── Constant.tsx │ │ │ │ ├── Enum.tsx │ │ │ │ ├── Event.tsx │ │ │ │ ├── Field.tsx │ │ │ │ ├── File.tsx │ │ │ │ ├── Folder.tsx │ │ │ │ ├── Interface.tsx │ │ │ │ ├── Keyword.tsx │ │ │ │ ├── Method.tsx │ │ │ │ ├── Module.tsx │ │ │ │ ├── Multiple.tsx │ │ │ │ ├── Operator.tsx │ │ │ │ ├── Property.tsx │ │ │ │ ├── Reference.tsx │ │ │ │ ├── Snippet.tsx │ │ │ │ ├── Struct.tsx │ │ │ │ ├── Symbol.tsx │ │ │ │ ├── Text.tsx │ │ │ │ ├── TypeParameter.tsx │ │ │ │ ├── Unit.tsx │ │ │ │ ├── Value.tsx │ │ │ │ └── Variable.tsx │ │ │ ├── Cog.tsx │ │ │ ├── ColorSwitch.tsx │ │ │ ├── Conversation.tsx │ │ │ ├── CopyText.tsx │ │ │ ├── DateTimeCalendar.tsx │ │ │ ├── Def.tsx │ │ │ ├── Documents.tsx │ │ │ ├── DoorOut.tsx │ │ │ ├── DoubleChevronIn.tsx │ │ │ ├── DoubleChevronOut.tsx │ │ │ ├── EyeCut.tsx │ │ │ ├── File.tsx │ │ │ ├── FileWithSparks.tsx │ │ │ ├── Filter.tsx │ │ │ ├── Folder.tsx │ │ │ ├── GitHubIcon.tsx │ │ │ ├── Globe.tsx │ │ │ ├── HardDrive.tsx │ │ │ ├── InfoBadge.tsx │ │ │ ├── KLetter.tsx │ │ │ ├── Like.tsx │ │ │ ├── LinkChain.tsx │ │ │ ├── LiteLoader.tsx │ │ │ ├── LogoFull.tsx │ │ │ ├── Macintosh.tsx │ │ │ ├── Magazine.tsx │ │ │ ├── MagnifyTool.tsx │ │ │ ├── MailIcon.tsx │ │ │ ├── MoreHorizontal.tsx │ │ │ ├── Pencil.tsx │ │ │ ├── Person.tsx │ │ │ ├── PlusSign.tsx │ │ │ ├── Prompt.tsx │ │ │ ├── Range.tsx │ │ │ ├── Ref.tsx │ │ │ ├── Refresh.tsx │ │ │ ├── Regex.tsx │ │ │ ├── RegexSearch.tsx │ │ │ ├── Repository.tsx │ │ │ ├── Run.tsx │ │ │ ├── Send.tsx │ │ │ ├── Shapes.tsx │ │ │ ├── SpinLoader.tsx │ │ │ ├── SplitView.tsx │ │ │ ├── StudioCloseSign.tsx │ │ │ ├── StudioPlusSign.tsx │ │ │ ├── Template.tsx │ │ │ ├── Templates.tsx │ │ │ ├── ThemeBlack.tsx │ │ │ ├── ThemeDark.tsx │ │ │ ├── ThemeLight.tsx │ │ │ ├── TooltipTailBottom.tsx │ │ │ ├── TooltipTailLeft.tsx │ │ │ ├── TooltipTailRight.tsx │ │ │ ├── TooltipTailTop.tsx │ │ │ ├── TrashCan.tsx │ │ │ ├── Unlike.tsx │ │ │ ├── Walk.tsx │ │ │ ├── Wallet.tsx │ │ │ ├── WarningSign.tsx │ │ │ ├── Wrapper.tsx │ │ │ └── index.ts │ │ ├── index.css │ │ ├── locales/ │ │ │ ├── en.json │ │ │ ├── es.json │ │ │ ├── it.json │ │ │ ├── ja.json │ │ │ ├── zh-CN.json │ │ │ └── zh-TW.json │ │ ├── main.tsx │ │ ├── mappers/ │ │ │ ├── conversation.ts │ │ │ └── results.ts │ │ ├── services/ │ │ │ ├── api.ts │ │ │ ├── cache.ts │ │ │ └── storage.ts │ │ ├── themes/ │ │ │ ├── default-dark.css │ │ │ └── default-light.css │ │ ├── types/ │ │ │ ├── api.ts │ │ │ ├── file-icons-js/ │ │ │ │ └── index.d.ts │ │ │ ├── general.ts │ │ │ ├── index.ts │ │ │ ├── prism.ts │ │ │ └── results.ts │ │ ├── utils/ │ │ │ ├── commandBarUtils.test.ts │ │ │ ├── commandBarUtils.ts │ │ │ ├── domUtils.ts │ │ │ ├── file.ts │ │ │ ├── index.test.ts │ │ │ ├── index.ts │ │ │ ├── keyboardUtils.ts │ │ │ ├── langs.json │ │ │ ├── mappers.ts │ │ │ ├── navigationUtils.ts │ │ │ ├── prism.ts │ │ │ ├── requestUtils.ts │ │ │ ├── scrollUtils.ts │ │ │ └── textSearch.ts │ │ └── vite-env.d.ts │ ├── tailwind.config.cjs │ ├── tests/ │ │ └── setupTests.ts │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts ├── docker-compose.yml ├── flake.nix ├── package.json ├── playwright.config.js ├── release_description_template.txt ├── server/ │ ├── README.md │ ├── bleep/ │ │ ├── Cargo.toml │ │ ├── build.rs │ │ ├── migrations/ │ │ │ ├── 20230424095042_conversations.sql │ │ │ ├── 20230613143506_file-cache.sql │ │ │ ├── 20230616140930_query-log.sql │ │ │ ├── 20230725183450_remove_conversation_llm_history_code_chunks_path.sql │ │ │ ├── 20230821131141_code_studio.sql │ │ │ ├── 20230831165918_templates.sql │ │ │ ├── 20230831170927_code_studio_uuid_blob.sql │ │ │ ├── 20230831184906_code_studio_history.sql │ │ │ ├── 20230901200307_code_studio_user_id.sql │ │ │ ├── 20230907154037_studio_int_id.sql │ │ │ ├── 20230911161509_template_user_id.sql │ │ │ ├── 20230912084309_nullable_studio_name.sql │ │ │ ├── 20230915091923_tutorial-questions.sql │ │ │ ├── 20230919100529_code_studio_docs.sql │ │ │ ├── 20231004101827_refactor_template.sql │ │ │ ├── 20231122012638_projects.sql │ │ │ └── 20231201200442_project_docs.sql │ │ ├── sqlx-data.json │ │ └── src/ │ │ ├── agent/ │ │ │ ├── exchange.rs │ │ │ ├── model.rs │ │ │ ├── prompts.rs │ │ │ ├── symbol.rs │ │ │ ├── tools/ │ │ │ │ ├── answer.rs │ │ │ │ ├── code.rs │ │ │ │ ├── path.rs │ │ │ │ └── proc.rs │ │ │ └── transcoder.rs │ │ ├── agent.rs │ │ ├── background/ │ │ │ ├── control.rs │ │ │ ├── notifyqueue.rs │ │ │ └── sync.rs │ │ ├── background.rs │ │ ├── bin/ │ │ │ └── bleep.rs │ │ ├── cache.rs │ │ ├── collector/ │ │ │ ├── bytes_filter.rs │ │ │ ├── frequency.rs │ │ │ └── group.rs │ │ ├── collector.rs │ │ ├── commits.rs │ │ ├── config.rs │ │ ├── db/ │ │ │ └── query_log.rs │ │ ├── db.rs │ │ ├── env.rs │ │ ├── indexes/ │ │ │ ├── analytics.rs │ │ │ ├── doc.rs │ │ │ ├── file.rs │ │ │ ├── reader.rs │ │ │ ├── repo.rs │ │ │ └── schema.rs │ │ ├── indexes.rs │ │ ├── intelligence/ │ │ │ ├── code_navigation.rs │ │ │ ├── language/ │ │ │ │ ├── c/ │ │ │ │ │ ├── mod.rs │ │ │ │ │ └── scopes.scm │ │ │ │ ├── c_sharp/ │ │ │ │ │ ├── mod.rs │ │ │ │ │ └── scopes.scm │ │ │ │ ├── cobol/ │ │ │ │ │ ├── mod.rs │ │ │ │ │ └── scopes.scm │ │ │ │ ├── cpp/ │ │ │ │ │ ├── mod.rs │ │ │ │ │ └── scopes.scm │ │ │ │ ├── go/ │ │ │ │ │ ├── mod.rs │ │ │ │ │ └── scopes.scm │ │ │ │ ├── java/ │ │ │ │ │ ├── mod.rs │ │ │ │ │ └── scopes.scm │ │ │ │ ├── javascript/ │ │ │ │ │ ├── mod.rs │ │ │ │ │ └── scopes.scm │ │ │ │ ├── php/ │ │ │ │ │ ├── mod.rs │ │ │ │ │ └── scopes.scm │ │ │ │ ├── python/ │ │ │ │ │ ├── mod.rs │ │ │ │ │ └── scopes.scm │ │ │ │ ├── r/ │ │ │ │ │ ├── mod.rs │ │ │ │ │ └── scopes.scm │ │ │ │ ├── ruby/ │ │ │ │ │ ├── mod.rs │ │ │ │ │ └── scopes.scm │ │ │ │ ├── rust/ │ │ │ │ │ ├── mod.rs │ │ │ │ │ └── scopes.scm │ │ │ │ ├── test_utils.rs │ │ │ │ └── typescript/ │ │ │ │ ├── mod.rs │ │ │ │ └── scopes.scm │ │ │ ├── language.rs │ │ │ ├── namespace.rs │ │ │ ├── scope_resolution/ │ │ │ │ ├── debug.rs │ │ │ │ ├── def.rs │ │ │ │ ├── import.rs │ │ │ │ ├── reference.rs │ │ │ │ └── scope.rs │ │ │ └── scope_resolution.rs │ │ ├── intelligence.rs │ │ ├── lib.rs │ │ ├── llm/ │ │ │ ├── call.rs │ │ │ └── client.rs │ │ ├── llm.rs │ │ ├── periodic/ │ │ │ ├── logrotate.rs │ │ │ └── remotes.rs │ │ ├── periodic.rs │ │ ├── query/ │ │ │ ├── compiler.rs │ │ │ ├── execute.rs │ │ │ ├── grammar.pest │ │ │ ├── languages.rs │ │ │ ├── parser.rs │ │ │ ├── planner/ │ │ │ │ └── optimize.rs │ │ │ ├── planner.rs │ │ │ ├── ranking.rs │ │ │ ├── stopwords.rs │ │ │ └── stopwords.txt │ │ ├── query.rs │ │ ├── remotes/ │ │ │ ├── github.rs │ │ │ └── poll.rs │ │ ├── remotes.rs │ │ ├── repo/ │ │ │ ├── iterator/ │ │ │ │ ├── filters.rs │ │ │ │ ├── fs.rs │ │ │ │ ├── git.rs │ │ │ │ └── language.rs │ │ │ └── iterator.rs │ │ ├── repo.rs │ │ ├── scraper/ │ │ │ ├── article.rs │ │ │ └── chunk.rs │ │ ├── scraper.rs │ │ ├── semantic/ │ │ │ ├── chunk.rs │ │ │ ├── embedder.rs │ │ │ ├── execute.rs │ │ │ └── schema.rs │ │ ├── semantic.rs │ │ ├── snippet.rs │ │ ├── state.rs │ │ ├── symbol.rs │ │ ├── text_range.rs │ │ ├── user.rs │ │ ├── webserver/ │ │ │ ├── answer.rs │ │ │ ├── autocomplete.rs │ │ │ ├── commits.rs │ │ │ ├── config.rs │ │ │ ├── conversation.rs │ │ │ ├── docs.rs │ │ │ ├── file.rs │ │ │ ├── hoverable.rs │ │ │ ├── index.rs │ │ │ ├── intelligence.rs │ │ │ ├── middleware.rs │ │ │ ├── project/ │ │ │ │ ├── doc.rs │ │ │ │ └── repo.rs │ │ │ ├── project.rs │ │ │ ├── query.rs │ │ │ ├── repos.rs │ │ │ ├── search.rs │ │ │ ├── studio/ │ │ │ │ └── diff.rs │ │ │ ├── studio.rs │ │ │ └── template.rs │ │ └── webserver.rs │ └── languages.yml └── tests/ ├── .example.env ├── .gitignore ├── all_onboarding.spec.js_ ├── github_onboarding.spec.js_ ├── local_onboarding.spec.js_ ├── onboarding.spec.ts ├── onboarding.ts ├── repository.spec.ts ├── search.spec.ts ├── settings.spec.ts └── tsconfig.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .dockerignore ================================================ /target /node_modules **/node_modules ================================================ FILE: .envrc ================================================ use flake . ================================================ FILE: .eslintrc.json ================================================ { "env": { "browser": true, "es2021": true, "mocha": true, "jest/globals": true, "node": true }, "extends": [ "plugin:react/recommended", "plugin:import/recommended", "prettier" ], "parser": "@typescript-eslint/parser", "parserOptions": { "ecmaFeatures": { "jsx": true }, "ecmaVersion": 12, "sourceType": "module" }, "plugins": [ "react", "jest", "prettier" ], "settings": { "react": { "version": "18" }, "import/resolver": { "typescript": {} } }, "rules": { "space-before-function-paren": ["error", {"anonymous": "always", "named": "never", "asyncArrow": "always"}], "curly": ["error", "multi-line"], "no-new": "off", "no-use-before-define": "off", "no-useless-constructor": "off", "react/prop-types": "off", "camelcase": "off", "prettier/prettier": ["error", { "singleQuote": true, "semi": true, "trailingComma": "all", "tabWidth": 2, "bracketSameLine": false }], "import/order": "error", "no-undef": "error", "import/no-unresolved": "off", "react/react-in-jsx-scope": "off" }, "globals": { "React": true, "JSX": true } } ================================================ FILE: .gitattributes ================================================ apps/desktop/src-tauri/model/vocab.txt filter=lfs diff=lfs merge=lfs -text apps/desktop/src-tauri/model/model.onnx filter=lfs diff=lfs merge=lfs -text apps/desktop/src-tauri/model/special_tokens_map.json filter=lfs diff=lfs merge=lfs -text apps/desktop/src-tauri/model/tokenizer.json filter=lfs diff=lfs merge=lfs -text apps/desktop/src-tauri/model/tokenizer_config.json filter=lfs diff=lfs merge=lfs -text apps/desktop/src-tauri/bin/qdrant-x86_64-apple-darwin filter=lfs diff=lfs merge=lfs -text apps/desktop/src-tauri/bin/qdrant-aarch64-apple-darwin filter=lfs diff=lfs merge=lfs -text apps/desktop/src-tauri/bin/qdrant-x86_64-unknown-linux-gnu filter=lfs diff=lfs merge=lfs -text apps/desktop/src-tauri/bin/qdrant-x86_64-pc-windows-msvc.exe filter=lfs diff=lfs merge=lfs -text apps/desktop/src-tauri/model/ggml/ggml-model-q4_0.bin filter=lfs diff=lfs merge=lfs -text apps/desktop/src-tauri/model/ggml/tokenizer.json filter=lfs diff=lfs merge=lfs -text ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Create a bug report for bloop. labels: bug --- **Describe the bug** A clear and concise description of what the bug is. If appropriate add a `frontend` or `backend` label. **Expected behavior** What did you expect to happen? **To Reproduce** How can we reproduce the bug? Helpful information could include: - Details about your system - The data that you had indexed - The query that you ran **Screenshots or output** If applicable, add screenshots to help illustrate the bug. **Additional context** Add any other context about the problem here. ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature request about: Suggest an idea for this project labels: feature --- **What's the problem?** A clear and concise description of the problem. For example, I find... frustrating **What's the solution?** A clear and concise description of what you'd like to happen. **Additional context** Add any other context or screenshots about the feature request here. ================================================ FILE: .github/ISSUE_TEMPLATE/request-for-comments.md ================================================ --- name: Request for comments about: An engineering/feature proposal with in-depth details labels: rfc --- **High level details** A short summary of the expected outcome and context. **Current situation** Some details about what we're currently doing. For example: * how it's insufficient * how it's flawed * what were the original motivations * why we need to change this **Proposal** Details about the proposal, and how to implement it. For example: * high-level behavior * what will/can break * what changes are necessary **Next steps** Are there any next steps after we implement these changes? ================================================ FILE: .github/workflows/build-on-pr-command.yml ================================================ name: Build Bloop container with latest PR commit tag on build command on: issue_comment: types: [created] jobs: debug: runs-on: ubuntu-latest steps: - name: $github run: echo "$GITHUB_CONTEXT" env: GITHUB_CONTEXT: ${{ toJson(github) }} permissions: runs-on: ubuntu-latest name: Validate user is the member of BloopAI organization if: github.event.issue.pull_request && contains(github.event.comment.body, '/build') outputs: is-member: ${{ steps.membership.outputs.is-member }} steps: - name: Validation id: membership env: ACTOR: ${{ github.triggering_actor }} run: | members=$(curl -L \ -H "Accept: application/vnd.github+json" \ -H "Authorization: Bearer ${{ secrets.BLOOP_DEVOPS_PAT}}"\ -H "X-GitHub-Api-Version: 2022-11-28" \ https://api.github.com/orgs/BloopAI/members | jq -r ".[] .login") for member in $(echo ${members}); do if [[ $member = $ACTOR ]]; then echo "is-member=true" >> $GITHUB_OUTPUT fi done build_tag: runs-on: ubuntu-latest name: Run container build on comment needs: [permissions] if: github.event.issue.pull_request && contains(github.event.comment.body, '/build') && needs.permissions.outputs.is-member == 'true' outputs: tag: build-${{ steps.comment-branch.outputs.head_sha }} ref: ${{ steps.comment-branch.outputs.head_ref }} steps: - name: Get PR branch uses: xt0rted/pull-request-comment-branch@v1 id: comment-branch - name: Checkout PR branch uses: actions/checkout@v4 with: ref: ${{ steps.comment-branch.outputs.head_ref }} build_and_push: uses: BloopAI/workflows/.github/workflows/build-container.yml@main if: needs.permissions.outputs.is-member == 'true' needs: [permissions, build_tag] with: repository: bloop tag: ${{ needs.build_tag.outputs.tag }} runner: ubuntu-latest secrets: awsRegion: ${{ secrets.AWS_REGION }} awsAccountID: ${{ secrets.AWS_ACCOUNT_ID }} slackBuildWebhook: ${{ secrets.SLACK_BUILD_WEBHOOK }} build-args: | SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_RELEASE_VERSION=${{ needs.build_tag.outputs.tag }} report_status: runs-on: ubuntu-latest name: Report status of the build needs: [permissions, build_tag, build_and_push] if: always() && needs.permissions.outputs.is-member == 'true' steps: - name: pr id: pr run: | PR_NUMBER=$(echo $GITHUB_REF | awk 'BEGIN { FS = "/" } ; { print $3 }') echo "number=${PR_NUMBER}" >> ${GITHUB_OUTPUT} - name: Comment failure build if: ${{ contains(needs.*.result, 'failure') }} uses: thollander/actions-comment-pull-request@v2 with: message: | :red_circle: Bloop container with `${{ needs.build_tag.outputs.tag }}` tag failed! :link: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} pr_number: ${{ steps.pr.outputs.number }} - name: Comment success build if: ${{ !contains(needs.*.result, 'failure') }} uses: thollander/actions-comment-pull-request@v2 with: message: | :green_circle: Bloop container with `${{ needs.build_tag.outputs.tag }}` tag is ready! :link: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} pr_number: ${{ steps.pr.outputs.number }} ================================================ FILE: .github/workflows/build-on-pr.yml ================================================ name: Build and push docker container on: workflow_dispatch jobs: build_and_push: uses: BloopAI/workflows/.github/workflows/build-container.yml@main with: repository: bloop tag: build-${{ github.sha }} runner: ubuntu-latest secrets: awsRegion: ${{ secrets.AWS_REGION }} awsAccountID: ${{ secrets.AWS_ACCOUNT_ID }} slackBuildWebhook: ${{ secrets.SLACK_BUILD_WEBHOOK }} build-args: | SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_RELEASE_VERSION=${{ github.sha }} validate_helm: uses: BloopAI/reusable-workflows/.github/workflows/validate-helm-chart.yml@main with: path: helm/bloop secrets: slackBuildWebhook: ${{ secrets.SLACK_BUILD_WEBHOOK }} ================================================ FILE: .github/workflows/build-on-release.yml ================================================ name: Build&Push bloop docker container image with release tag on: release: types: [published, prereleased] jobs: build_and_push: uses: BloopAI/workflows/.github/workflows/build-container.yml@main with: repository: bloop tag: ${{ github.event.release.tag_name }} runner: ubuntu-latest secrets: awsRegion: ${{ secrets.AWS_REGION }} awsAccountID: ${{ secrets.AWS_ACCOUNT_ID }} slackBuildWebhook: ${{ secrets.SLACK_BUILD_WEBHOOK }} build-args: | SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_RELEASE_VERSION=${{ github.event.release.tag_name }} ================================================ FILE: .github/workflows/client-test.yml ================================================ name: Client Tests on: pull_request: types: [opened, synchronize] branches: [main] paths: - "client/**" - ".github/workflows/client**" concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true jobs: test: runs-on: ubuntu-latest steps: - run: 'echo "No checks required" ' fmt-clippy-build: runs-on: ubuntu-latest steps: - run: 'echo "No checks required" ' build-client: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 - name: Use Node.js uses: actions/setup-node@v3 with: node-version: 16 cache: "npm" - name: Install dependencies run: npm ci - name: Run lint run: npm run lint - name: Run type-check run: npm run client-type-check - name: Run tests run: npm --prefix client run test ================================================ FILE: .github/workflows/dependencies.yml ================================================ name: Dependency matrix on: workflow_dispatch: pull_request: branches: [main] paths: - "flake.nix" - "flake.lock" - ".github/workflows/dependencies.yml" jobs: qdrant-rustup: strategy: fail-fast: false matrix: package: [ qdrant ] target: [ x86_64-unknown-linux-gnu, x86_64-apple-darwin, aarch64-apple-darwin, x86_64-pc-windows-msvc ] include: - target: x86_64-unknown-linux-gnu os: ubuntu-20.04 cross: false - target: x86_64-apple-darwin os: macos-11 cross: false - target: aarch64-apple-darwin os: macos-11 cross: true - target: x86_64-pc-windows-msvc os: windows-latest cross: false runs-on: ${{ matrix.os }} steps: - name: Install Rust stable uses: dtolnay/rust-toolchain@stable with: toolchain: 1.74.0 target: ${{ matrix.target }} - name: Install Protoc uses: arduino/setup-protoc@v2 with: repo-token: ${{ secrets.GITHUB_TOKEN }} - name: Build qdrant env: VERSION: 1.7.1 run: | cargo install --target ${{ matrix.target }} --git https://github.com/qdrant/qdrant --tag v${{ env.VERSION }} --root . qdrant - name: Upload binaries uses: actions/upload-artifact@v3 with: name: "${{ matrix.package }}_${{ matrix.target }}" path: bin ================================================ FILE: .github/workflows/dummy.yml ================================================ name: dummy on: pull_request: paths-ignore: - ".github/workflows/server**" - ".github/workflows/client**" - "apps/desktop/**" - "server/**" - "client/**" jobs: test: runs-on: ubuntu-latest steps: - run: 'echo "No checks required" ' fmt-clippy-build: runs-on: ubuntu-latest steps: - run: 'echo "No checks required" ' build-client: runs-on: ubuntu-latest steps: - run: 'echo "No checks required" ' ================================================ FILE: .github/workflows/server-test.yml ================================================ name: Server Unit Tests on: pull_request: branches: [main] paths: - "flake.*" - "server/**" - ".github/workflows/server**" concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true env: CARGO_TERM_COLOR: always jobs: clippy-test: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 with: token: ${{ secrets.GITHUB_TOKEN }} lfs: true - name: Install Rust stable uses: dtolnay/rust-toolchain@stable with: toolchain: 1.73.0 components: rustfmt, clippy # https://github.com/actions/cache/blob/main/examples.md#rust---cargo - name: Set up cargo cache uses: actions/cache@v3 continue-on-error: false with: path: | ~/.cargo/bin/ ~/.cargo/registry/index/ ~/.cargo/registry/cache/ ~/.cargo/git/db/ target/ key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} restore-keys: ${{ runner.os }}-cargo- - name: Rustfmt run: cargo --locked fmt -p bleep -- --check - name: Clippy run: cargo --locked clippy -p bleep - name: Tests run: cargo --locked test -p bleep --release ================================================ FILE: .github/workflows/tauri-release.yml ================================================ name: Tauri Release on: workflow_dispatch: pull_request: paths: - ".github/workflows/tauri-release.yml" release: types: [published] concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true jobs: build-and-sign-tauri: strategy: fail-fast: false matrix: target: [x86_64-unknown-linux-gnu, x86_64-apple-darwin, aarch64-apple-darwin] include: - target: x86_64-unknown-linux-gnu name: ubuntu-20.04 - target: x86_64-apple-darwin name: macos-11 - target: aarch64-apple-darwin name: macos-11 runs-on: ${{ matrix.name }} env: ORT_LIB_LOCATION: ${{ github.workspace }}/lib/${{ matrix.target }}/onnxruntime steps: - if: matrix.name == 'ubuntu-20.04' uses: pierotofy/set-swap-space@v1.0 with: swap-size-gb: 10 - name: Checkout code uses: actions/checkout@v4 with: token: ${{ secrets.GITHUB_TOKEN }} - name: Create LFS file list run: git lfs ls-files --long | cut -d ' ' -f1 | sort > .lfs-assets-id - name: LFS Cache uses: actions/cache@v3 with: path: .git/lfs/objects key: ${{ runner.os }}-lfs-${{ hashFiles('.lfs-assets-id') }} restore-keys: | ${{ runner.os }}-lfs- - name: Git LFS Pull run: git lfs install && git lfs pull - name: Install Protoc uses: arduino/setup-protoc@v2 with: repo-token: ${{ secrets.GITHUB_TOKEN }} - if: matrix.target == 'x86_64-unknown-linux-gnu' run: sudo apt-get update && sudo apt-get install -y libgtk-3-dev webkit2gtk-4.0 libappindicator3-dev librsvg2-dev patchelf - name: Use Node.js uses: actions/setup-node@v3 with: node-version: 16 cache: "npm" - name: Install app dependencies run: npm ci - name: Install Rust stable uses: dtolnay/rust-toolchain@stable with: toolchain: 1.73.0 target: ${{ matrix.target }} - name: Check if keys exist env: analytics_key: ${{ secrets.ANALYTICS_BE_WRITE_KEY_PROD }} analytics_fe_key: ${{ secrets.ANALYTICS_FE_WRITE_KEY_PROD }} sentry_dsn: ${{ secrets.SENTRY_DSN_BE }} sentry_dsn_fe: ${{ secrets.SENTRY_DSN_FE }} if: ${{ env.analytics_fe_key == '' || env.sentry_dsn_fe == '' || env.analytics_key == '' || env.sentry_dsn == ''}} run: exit 1 - name: Set environment run: echo "{\"analytics_key\":\"${{ secrets.ANALYTICS_BE_WRITE_KEY_PROD }}\",\"analytics_data_plane\":\"${{ secrets.ANALYTICS_DATA_PLANE_URL }}\",\"sentry_dsn_fe\":\"${{ secrets.SENTRY_DSN_FE }}\",\"sentry_dsn\":\"${{ secrets.SENTRY_DSN_BE }}\",\"analytics_key_fe\":\"${{ secrets.ANALYTICS_FE_WRITE_KEY_PROD }}\",\"answer_api_url\":\"${{ secrets.ANSWER_API_URL }}\",\"cognito_userpool_id\":\"${{ secrets.COGNITO_USERPOOL_ID }}\",\"cognito_client_id\":\"${{ secrets.COGNITO_CLIENT_ID }}\",\"cognito_auth_url\":\"${{ secrets.COGNITO_AUTH_URL }}\",\"cognito_mgmt_url\":\"${{ secrets.COGNITO_MGMT_URL }}\",\"cognito_config_url\":\"${{ secrets.COGNITO_CONFIG_URL }}\"}" > apps/desktop/src-tauri/config/config.json - name: Check environment is set run: du -h apps/desktop/src-tauri/config/config.json - name: Set providerShortName in tauri.conf.json uses: jossef/action-set-json-field@v2.1 with: file: apps/desktop/src-tauri/tauri.conf.json field: tauri.bundle.macOS.providerShortName value: ${{ secrets.MAC_PROVIDER_SHORT_NAME }} - name: Set signingIdentity in tauri.conf.json uses: jossef/action-set-json-field@v2.1 with: file: apps/desktop/src-tauri/tauri.conf.json field: tauri.bundle.macOS.signingIdentity value: ${{ secrets.APPLE_SIGNING_IDENTITY }} - name: Create Signing API Key if: matrix.name == 'macos-11' run: echo "${{ secrets.APPLE_API_KEY_CONTENT }}" > apiKey.p8 - name: Remove onnxruntime from aarch64-apple-darwin builds if: matrix.target == 'aarch64-apple-darwin' uses: jossef/action-set-json-field@v2.1 with: file: apps/desktop/src-tauri/tauri.conf.json field: tauri.bundle.macOS.frameworks value: '[]' parse_json: true - name: get release version id: release-version run: echo "RELEASE_VERSION=$(cat apps/desktop/src-tauri/tauri.conf.json | jq '.package.version' | tr -d '"')" >> "$GITHUB_OUTPUT" - uses: tauri-apps/tauri-action@cb58ba3f65bd456ee564376585a8400bf0b71f47 env: NODE_OPTIONS: "--max-old-space-size=4096" ORT_LIB_LOCATION: ${{ github.workspace }}/lib/${{ matrix.target }}/onnxruntime GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ENABLE_CODE_SIGNING: ${{ secrets.APPLE_CERTIFICATE }} APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }} APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }} APPLE_API_KEY_PATH: /Users/runner/work/bloop/bloop/apiKey.p8 TAURI_BIN_PATH: apps/desktop/src-tauri/bin TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }} TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_RELEASE_VERSION: ${{ steps.release-version.outputs.RELEASE_VERSION }} with: args: -- --target "${{ matrix.target }}" -v - name: Upload artifacts uses: actions/upload-artifact@v3 with: name: ${{ matrix.target }}-app path: target/${{ matrix.target }}/release/bundle retention-days: 5 - name: Setup Sentry CLI uses: mathieu-bour/setup-sentry-cli@v1.3.0 with: token: ${{ secrets.SENTRY_AUTH_TOKEN }} organization: ${{ secrets.SENTRY_ORG }} project: ${{ secrets.SENTRY_PROJECT }} version: 2.21.2 - name: Create Sentry release run: | sentry-cli releases new "${{ steps.release-version.outputs.RELEASE_VERSION }}" sentry-cli releases set-commits "${{ steps.release-version.outputs.RELEASE_VERSION }}" --auto - name: (MacOS) Upload source maps to Sentry if: matrix.name == 'macos-11' run: | sentry-cli debug-files upload \ --log-level debug \ --include-sources \ target/${{ matrix.target }}/release/bloop.dSYM - name: Rename tar.gz in macos if: matrix.name == 'macos-11' run: | new_filename="bloop_${{ steps.release-version.outputs.RELEASE_VERSION }}_$(echo ${{ matrix.target }} | cut -d '-' -f 1).app.tar.gz" mv "target/${{ matrix.target }}/release/bundle/macos/bloop.app.tar.gz" "target/${{ matrix.target }}/release/bundle/macos/${new_filename}" - name: List files run: ls -R target/${{ matrix.target }}/release/bundle - name: Generate Changelog run: | release_version="${{ steps.release-version.outputs.RELEASE_VERSION }}" sed "s/VERSION/${release_version}/g" release_description_template.txt > new_description.txt cat new_description.txt - name: Upload release assets uses: softprops/action-gh-release@v1 if: startsWith(github.ref, 'refs/tags/v') with: files: | target/${{ matrix.target }}/release/bundle/deb/*.deb target/${{ matrix.target }}/release/bundle/appimage/*.AppImage target/${{ matrix.target }}/release/bundle/appimage/*.tar.gz target/${{ matrix.target }}/release/bundle/dmg/*.dmg target/${{ matrix.target }}/release/bundle/macos/*.tar.gz ================================================ FILE: .github/workflows/tauri-test.yml ================================================ name: Tauri Tests on: pull_request: types: [opened, synchronize] branches: [main] paths: - "apps/desktop/**" - ".github/workflows/tauri**" concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true env: CARGO_TERM_COLOR: always jobs: clippy-test: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 with: token: ${{ secrets.GITHUB_TOKEN }} lfs: true - name: Install Rust stable uses: dtolnay/rust-toolchain@stable with: toolchain: 1.73.0 components: rustfmt, clippy - name: Install Tauri deps run: | sudo apt update && \ sudo apt install libwebkit2gtk-4.0-dev \ build-essential \ curl \ wget \ file \ libssl-dev \ libgtk-3-dev \ libayatana-appindicator3-dev \ librsvg2-dev # https://github.com/actions/cache/blob/main/examples.md#rust---cargo - name: Set up cargo cache uses: actions/cache@v3 continue-on-error: false with: path: | ~/.cargo/bin/ ~/.cargo/registry/index/ ~/.cargo/registry/cache/ ~/.cargo/git/db/ target/ key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} restore-keys: ${{ runner.os }}-cargo- - name: Rustfmt run: cargo --locked fmt -p bloop -- --check - name: Clippy run: cargo --locked clippy -p bloop - name: Tests run: cargo --locked test -p bloop --release ================================================ FILE: .gitignore ================================================ # Env .direnv .idea .DS_Store .vscode # Generated by Cargo # will have compiled files and executables debug/ target/ # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html # Cargo.lock # These are backup files generated by rustfmt **/*.rs.bk # MSVC Windows builds of rustc generate these, which store debugging information *.pdb # node /node_modules/ # Index artifacts server/index server/schema server/target server/tmp_index server/bleep/bleep.db* playwright-report .tests.env .env # local config /local_config.json apps/desktop/src-tauri/dylibs/*.so apps/desktop/src-tauri/dylibs/*.dll apps/desktop/src-tauri/frameworks/*.dylib # qdrant /qdrant/storage /qdrant/snapshots .qdrant-initialized ================================================ FILE: .gitpod.Dockerfile ================================================ FROM axonasif/workspace-base ARG NIX_VERSION="2.11.0" ARG NIX_CONFIG="experimental-features = nix-command flakes" ENV NIX_VERSION=${NIX_VERSION} USER root # Dazzle does not rebuild a layer until one of its lines are changed. Increase this counter to rebuild this layer. ENV TRIGGER_REBUILD=1 RUN addgroup --system nixbld \ && adduser gitpod nixbld \ && for i in $(seq 1 30); do useradd -ms /bin/bash nixbld$i && adduser nixbld$i nixbld; done \ && mkdir -m 0755 /nix && chown gitpod /nix \ && mkdir -p /etc/nix && echo 'sandbox = false' > /etc/nix/nix.conf # Install Nix USER gitpod ENV USER gitpod WORKDIR /home/gitpod RUN curl https://nixos.org/releases/nix/nix-$NIX_VERSION/install | sh RUN echo '. /home/gitpod/.nix-profile/etc/profile.d/nix.sh' >> /home/gitpod/.bashrc.d/200-nix RUN mkdir -p /home/gitpod/.config/nixpkgs && echo '{ allowUnfree = true; }' >> /home/gitpod/.config/nixpkgs/config.nix RUN mkdir -p /home/gitpod/.config/nix && echo $NIX_CONFIG >> /home/gitpod/.config/nix/nix.conf # Install cachix RUN . /home/gitpod/.nix-profile/etc/profile.d/nix.sh \ && nix-env -iA cachix -f https://cachix.org/api/v1/install \ && cachix use cachix # Install direnv & other files RUN mkdir -p $HOME/.config/direnv && printf '%s\n' "[whitelist]" 'prefix = [ "/workspace" ]' > $HOME/.config/direnv/config.toml \ && printf '%s\n' 'source <(direnv hook bash)' > $HOME/.bashrc.d/999-direnv \ && printf '%s\n' \ 'dirs=($HOME/.cargo $HOME/.cache/nix) && mkdir -p "${dirs[@]}"' \ 'create-overlay /nix "${dirs[@]}"' > $HOME/.runonce/100-nix \ && . /home/gitpod/.nix-profile/etc/profile.d/nix.sh \ && nix-env -iA nixpkgs.direnv ================================================ FILE: .gitpod.yml ================================================ # This configuration file was automatically generated by Gitpod. # Please adjust to your needs (see https://www.gitpod.io/docs/introduction/learn-gitpod/gitpod-yaml) # and commit this file to your remote git repository to share the goodness with others. # Learn more from ready-to-use templates: https://www.gitpod.io/docs/introduction/getting-started/quickstart image: file: .gitpod.Dockerfile ports: - name: Bloop Web Interface port: 7878 protocol: http additionalRepositories: - url: https://github.com/bloopai/answer-api checkoutLocation: answer-api tasks: - name: Backend init: | nix run nixpkgs#cachix use bloopai # Setup Git LFS git lfs pull model git lfs install --skip-smudge # Cache build artifacts nix develop -c cargo build --locked --features=ee-cloud --bin bleep nix develop -c npm i # this is working around a gitpod bug # https://github.com/gitpod-io/gitpod/issues/524 find target -exec stat --format='%.Y %n' {} + > /workspace/.ts command: | while read -r ts file; do touch -d "@${ts}" "${file}"; done < /workspace/.ts bloop_url=$(gp url 7878 | sed 's;https://;;') git reset --hard nix develop -c cargo watch -w server -- \ cargo run --locked --features=ee-cloud --bin bleep -- \ \ --qdrant-url http://localhost:6334 \ --answer-api-url http://localhost:7879 \ --model-dir /workspace/bloop/model \ --frontend-dist /workspace/bloop/client/dist \ --instance-domain $bloop_url \ --bloop-instance-secret "$BLOOP_INSTANCE_SECRET" \ --bloop-instance-org "$BLOOP_INSTANCE_ORG" \ --cognito-config-url https://cloud-auth-staging.bloop.ai/bloop_config openMode: split-left - name: Frontend command: | nix develop -c npm i nix develop -c npm run build-web -- -- --watch --minify false openMode: split-right - name: Qdrant command: | nix run nixpkgs#qdrant -- --config-path qdrant/config.yaml openMode: tab-after - name: answer-api command: | cd /workspace/answer-api nix run /workspace/answer-api openMode: tab-after github: prebuilds: # enable for the default branch (defaults to true) master: true # enable for all branches in this repo (defaults to false) branches: true # enable for pull requests coming from this repo (defaults to true) pullRequests: true # enable for pull requests coming from forks (defaults to false) pullRequestsFromForks: false # add a check to pull requests (defaults to true) addCheck: true # add a "Review in Gitpod" button as a comment to pull requests (defaults to false) addComment: true # add a "Review in Gitpod" button to the pull request's description (defaults to false) addBadge: true ================================================ FILE: .taurignore ================================================ server/bleep/bleep.db* apps/desktop/src-tauri/dylibs/*.so apps/desktop/src-tauri/dylibs/*.dylib apps/desktop/src-tauri/dylibs/*.dll ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation. We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. ## Our Standards Examples of behavior that contributes to a positive environment for our community include: * Demonstrating empathy and kindness toward other people * Being respectful of differing opinions, viewpoints, and experiences * Giving and gracefully accepting constructive feedback * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience * Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: * The use of sexualized language or imagery, and sexual attention or advances of any kind * Trolling, insulting or derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or email address, without their explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Enforcement Responsibilities Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. ## Scope This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at maintainers@bloop.ai through e-mail, with an appropriate subject line. All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the reporter of any incident. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ### Attribution This Code of Conduct is adapted from the [Next.js project][nextjs-coc] The original text is from the [Contributor Covenant][homepage], version 2.1, available at [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. For answers to common questions about this code of conduct, see the FAQ at [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at [https://www.contributor-covenant.org/translations][translations]. [nextjs-coc]: https://raw.githubusercontent.com/vercel/next.js/canary/CODE_OF_CONDUCT.md [homepage]: https://www.contributor-covenant.org [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html [FAQ]: https://www.contributor-covenant.org/faq [translations]: https://www.contributor-covenant.org/translations ================================================ FILE: CONTRIBUTING.md ================================================ Guide to new contributors ========================= Thanks for your interest in contributing to `bloop`! Before jumping in, please take a look at our [code of conduct](./CODE_OF_CONDUCT.md). ## Have a question? If you have a question about using [bloop](https://github.com/bloopai/bloop), please raise a ticket in the repository, or say hi on our [Discord server](https://discord.gg/kZEgj5pyjm). ## Would like to contribute? If you have a great idea, some novel optimization, or a bug fix ready to be merged, the maintainer team will help you get it into `bloop`. We aim to maintain a high quality of contributions, and therefore all PRs need to go through a review process. During the review period you may be asked to make some changes to the code. When everybody's happy, we'll land the code, and ship it in the next release! After a longer period of inactivity, we will close PRs. Some of the code may eventually make it into `bloop` if there's someone else to champion it, in accordance with the license. In case you are not certain that your code quality or the feature you're working on is suitable for `bloop`, please open an issue with the question, or send in the PR anyway. This allows the maintainers to give you hands on feedback, and work with you on specifics rather than theoretical proposals. A quick list of things maintainers will check during review. The following 2 steps are requirements for all PRs: * Don't break public APIs. * Make sure you use `rustfmt`, follow `clippy`, and check tests, or your PR will fail the CI! Additionally, please pay attention to the following points as it helps with our review. However, these are _not_ required: * Please follow one of the issue/PR templates to make our work easier. * Document your changes as best as you can where appropriate. * Take a look at surrounding code, and try to match the style. * If you implement new high-level features, make sure you have a minimal example either in the documentation or in form of tests/benchmarks. * If you have a short & sweet bug fix, please create a PR and describe instructions to reproduce the bug, or a negative unit test. * Provide tests for logic changes, and benchmarks for performance work if possible. Following these points will help the maintainers to run through your code and merge it in a timely manner. Make sure the description of the PR clearly explains the motivation and link to any other resources we might need to consider when reviewing. ## Get in touch! If you're still in doubt, email us at , and we'll get you started! ================================================ FILE: Cargo.toml ================================================ [workspace] resolver = "2" members = [ "server/bleep", "apps/desktop/src-tauri" ] [profile.dev] opt-level = 3 [profile.release] debug = 1 lto = true split-debuginfo = "packed" [profile.profiler] inherits = "release" debug = true split-debuginfo = "unpacked" strip = "none" [patch.crates-io] esaxx-rs = { git = "https://github.com/bloopai/esaxx-rs" } ================================================ FILE: Dockerfile ================================================ FROM node AS frontend WORKDIR /build COPY package.json package-lock.json ./ RUN npm ci COPY apps/ apps COPY client/ client COPY playwright.config.js . RUN npm run build-web FROM rust:1.73-slim-bookworm as builder WORKDIR /build RUN apt-get update && \ apt-get -y install make clang libc-dev curl cmake python3 protobuf-compiler pkg-config libssl3 libssl-dev git && \ rm -rf /var/lib/apt/lists/* && \ curl -sLo sccache.tar.gz https://github.com/mozilla/sccache/releases/download/v0.3.3/sccache-v0.3.3-x86_64-unknown-linux-musl.tar.gz && \ tar xzf sccache.tar.gz && \ mv sccache-*/sccache /usr/bin/sccache ENV RUSTC_WRAPPER="/usr/bin/sccache" ENV PYTHON /usr/bin/python3 ENV CC /usr/bin/clang ENV CXX /usr/bin/clang++ COPY server server COPY apps/desktop/src-tauri apps/desktop/src-tauri COPY Cargo.lock Cargo.toml . RUN --mount=target=/root/.cache/sccache,type=cache --mount=target=/build/target,type=cache \ cargo --locked build --bin bleep --release && \ cp /build/target/release/bleep / && \ sccache --show-stats && \ mkdir /dylib && \ cp /build/target/release/libonnxruntime.so /dylib/ FROM debian:bookworm-slim VOLUME ["/repos", "/data"] RUN apt-get update && apt-get -y install openssl ca-certificates libprotobuf-lite32 && rm -rf /var/lib/apt/lists/* COPY model /model COPY --from=builder /bleep / COPY --from=builder /dylib /dylib COPY --from=frontend /build/client/dist /frontend ARG OPENAI_API_KEY ARG GITHUB_ACCESS_TOKEN ENTRYPOINT ["/bleep", "--host=0.0.0.0", "--source-dir=/repos", "--index-dir=/data", "--model-dir=/model", "--dylib-dir=/dylib", "--disable-log-write", "--frontend-dist=/frontend", "--openai-api-key=$OPENAI_API_KEY", "--github-access-token=$GITHUB_ACCESS_TOKEN"] ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: README.md ================================================ bloop logo bloop is ChatGPT for your code. Ask questions in natural language, search for code and generate patches using your existing codebase as context. Engineers are increasing their productivity by using bloop to: - Explain how files or features work in simple language - Write new features, using their code as context - Understand how to use poorly documented open source libraries - Pinpoint errors - Ask questions about English language codebases in other languages - Reduce code duplication by checking for existing functionality https://github.com/BloopAI/bloop/assets/7957964/01db3ccb-4af0-49a0-92d6-5a9c42357a51 ## Features - AI-based conversational search - Code Studio, an LLM playground that uses your code as context - Blazing fast regex search - Sync your local and GitHub repositories - Sophisticated query filters so you can narrow down your results - Find functions, variables or traits with symbol search - Precise code navigation (go-to-reference and go-to-definition) for 10+ of the most popular languages built with [Tree-sitter](https://tree-sitter.github.io/tree-sitter/) - Privacy focussed on-device embedding for semantic search bloop stands on the shoulders of the Rust ecosystem. Our search indexes are powered by [Tantivy](https://github.com/quickwit-oss/tantivy) and [Qdrant](https://github.com/qdrant/qdrant), and our multi-platform app is built with [Tauri](https://github.com/tauri-apps/tauri). https://github.com/BloopAI/bloop/assets/7957964/93715188-d8d5-477b-8cd1-95d9cbd368cb ## Get Started The simplest way to get started with bloop is to [download the app](https://github.com/BloopAI/bloop/releases) and follow the onboarding steps. Checkout our [getting started guide](https://bloop.ai/understand/docs/getting-started) and our references for [conversational](https://bloop.ai/understand/docs/natural-language-queries) and [regex](https://bloop.ai/understand/docs/regex-queries) search and [Code Studio](https://bloop.ai/understand/docs/code-studio). For instructions on how to build from source or run bloop from the command line, check out these pages: - [Build bloop app from source](./apps/desktop/README.md) - [Run bloop from the command line](./server/README.md) If you encounter any index issues you can wipe the bloop cache and reindex. Instructions on how to do this on different platforms [are here](./apps/desktop/README.md). ## Building From Source You can build bloop from source and run it with your own OpenAI API key. Clone the repo, make sure the `oss` branch is checked out, and create a file called `local_config.json` at the top-level of the repo. `local_config.json` should contain the following fields: ```json { "github_access_token": "", "openai_api_key": "" } ``` Then follow [these installation instructions](./apps/desktop/README.md). If built from source, bloop will not collect any telemetry. ## Contributing [![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/BloopAI/bloop) [![Open in Codeanywhere](https://codeanywhere.com/img/open-in-codeanywhere-btn.svg)](https://app.codeanywhere.com/#https://github.com/BloopAI/bloop) We welcome contributions big and small! Before jumping in please read [our contributors guide](./CONTRIBUTING.md) and [our code of conduct](./CODE_OF_CONDUCT.md). Here's how to find your way around the repo: - `apps/desktop`: The Tauri app - `server/bleep`: The Rust backend which contains the core search and navigation logic - `client`: The React frontend We use Git LFS for dependencies that are expensive to build. To make sure you have everything you need to start building, you'll need to install the `git-lfs` package for your favourite operating system, then run the following commands in this repo: git lfs install git lfs pull If you find a bug or have a feature request, [open an issue](https://github.com/BloopAI/bloop/issues)! You can find the application logs here: | OS | Logs Path | | ----------- | ----------- | | MacOS | `~/Library/Application\ Support/ai.bloop.bloop/bleep/logs` | | Windows | `%APPDATA%/bloop/bleep/logs` | | Linux | `~/.local/share/bloop/bleep/logs` | ## Privacy We store as little data as possible. We use telemetry to helps us identify bugs and make data-driven product decisions. You can read our full privacy policy [here](https://bloop.ai/privacy). ## License bloop is licensed under the `Apache 2.0` license as defined in [LICENSE](./LICENSE). ================================================ FILE: apps/desktop/.gitignore ================================================ # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* pnpm-debug.log* lerna-debug.log* node_modules dist/* !dist/.keep dist-ssr *.local # Editor directories and files .vscode/* !.vscode/extensions.json .idea .DS_Store *.suo *.ntvs* *.njsproj *.sln *.sw? /.env # Qdrant build relic src-tauri/bin/schema_generator # Auto-generated from Cargo.toml/lock files .crates* ================================================ FILE: apps/desktop/README.md ================================================ # bloop App The bloop app is built using [Tauri](https://github.com/tauri-apps/tauri), a Rust framework for building cross-platform apps. ## Dependencies To build the Tauri app you need the following dependencies: - `rustup` - `clang` `cmake` `wget` - `protobuf` - `onnxruntime` Linux users need to ensure that the following are present: - `AppImageKit` - `atk` - `dbus` - `glib` `gtk3` (including `webkit-gtk`) - `pango` ## Setup All commands should be run from the root directory unless specified otherwise. First make sure dependencies have been downloaded and installed: ``` git lfs install git lfs pull npm install ``` Then to build the app locally: ``` npm run build-app ``` Alternatively, to run the app in dev mode: ``` npm run start-app ``` ## Wiping an index Deleting and re-indexing the bloop index can fix corruption issues. bloop's index is stored: | OS | Index Path | | ----------- | ----------- | | MacOS | `~/Library/Application\ Support/ai.bloop.bloop` | | Windows | `%APPDATA%/bloop` | | Linux | `~/.local/share/bloop` | ================================================ FILE: apps/desktop/index.html ================================================ bloop
================================================ FILE: apps/desktop/package.json ================================================ { "name": "@bloop/desktop", "private": true, "version": "0.0.0", "scripts": { "dev": "vite", "tsc": "tsc", "vite-build": "vite build", "build": "npm-run-all tsc vite-build", "preview": "vite preview", "tauri": "tauri" } } ================================================ FILE: apps/desktop/postcss.config.cjs ================================================ module.exports = { plugins: { tailwindcss: {}, autoprefixer: {}, }, } ================================================ FILE: apps/desktop/src/App.tsx ================================================ import React, { useCallback, useEffect, useMemo, useRef, useState, } from 'react'; import { invoke } from '@tauri-apps/api'; import { open } from '@tauri-apps/api/shell'; import { homeDir } from '@tauri-apps/api/path'; import { relaunch } from '@tauri-apps/api/process'; import { message, open as openDialog } from '@tauri-apps/api/dialog'; import { listen } from '@tauri-apps/api/event'; import * as tauriOs from '@tauri-apps/api/os'; import { getVersion } from '@tauri-apps/api/app'; import { BrowserRouter } from 'react-router-dom'; import ClientApp from '../../../client/src/App'; import '../../../client/src/index.css'; import useKeyboardNavigation from '../../../client/src/hooks/useKeyboardNavigation'; import { LocaleContext } from '../../../client/src/context/localeContext'; import i18n from '../../../client/src/i18n'; import { getPlainFromStorage, LANGUAGE_KEY, savePlainToStorage, USER_FONT_SIZE_KEY, } from '../../../client/src/services/storage'; import { LocaleType } from '../../../client/src/types/general'; import { DeviceContextProvider } from '../../../client/src/context/providers/DeviceContextProvider'; import TextSearch from './TextSearch'; function App() { const [homeDirectory, setHomeDir] = useState(''); const [indexFolder, setIndexFolder] = useState(''); const [os, setOs] = useState({ arch: '', type: '', platform: '', version: '', }); const [release, setRelease] = useState(''); const contentContainer = useRef(null); const [locale, setLocale] = useState( (getPlainFromStorage(LANGUAGE_KEY) as LocaleType | null) || 'en', ); useEffect(() => { i18n.changeLanguage(locale); savePlainToStorage(LANGUAGE_KEY, locale); }, [locale]); const localeContextValue = useMemo( () => ({ locale, setLocale, }), [locale], ); useEffect(() => { homeDir().then(setHomeDir); Promise.all([ tauriOs.arch(), tauriOs.type(), tauriOs.platform(), tauriOs.version(), getVersion(), ]).then(([arch, type, platform, version, appVersion]) => { setOs({ arch, type, platform, version }); setRelease(appVersion); // checkUpdateAndInstall(appVersion); // intervalId = window.setInterval( // () => checkUpdateAndInstall(appVersion), // 1000 * 60 * 60, // ); }); }, []); const handleKeyEvent = useCallback((e: KeyboardEvent) => { if ( (e.key === '=' || e.key === '-' || e.key === '0') && (e.metaKey || e.ctrlKey) && !e.shiftKey ) { const root = document.querySelector(':root'); if (!root) { return; } const style = window .getComputedStyle(root, null) .getPropertyValue('font-size'); const fontSize = parseFloat(style); const newFontSize = e.key === '0' ? 16 : fontSize + (e.key === '=' ? 1 : -1); (root as HTMLElement).style.fontSize = newFontSize + 'px'; savePlainToStorage(USER_FONT_SIZE_KEY, newFontSize); } }, []); useKeyboardNavigation(handleKeyEvent); useEffect(() => { const root = document.querySelector(':root'); if (!root) { return; } const newFontSize = getPlainFromStorage(USER_FONT_SIZE_KEY); if (newFontSize) { (root as HTMLElement).style.fontSize = newFontSize + 'px'; } }, []); useEffect(() => { const onContextMenu = (e: MouseEvent) => { if (!import.meta.env.DEV) { e.preventDefault(); } }; document.addEventListener('contextmenu', onContextMenu); return () => { document.removeEventListener('contextmenu', onContextMenu); }; }, []); const deviceContextValue = useMemo( () => ({ openFolderInExplorer: (path: string) => { invoke('show_folder_in_finder', { path }); }, openLink: (path: string) => { open(path); }, homeDir: homeDirectory, chooseFolder: openDialog, indexFolder, setIndexFolder, listen, os, invokeTauriCommand: invoke, release, apiUrl: 'http://127.0.0.1:7878/api', isRepoManagementAllowed: true, forceAnalytics: false, isSelfServe: false, showNativeMessage: message, relaunch, }), [homeDirectory, indexFolder, os, release], ); return (
); } export default App; ================================================ FILE: apps/desktop/src/TextSearch.tsx ================================================ import React, { useCallback, useEffect, useState } from 'react'; import { ACTIVE_HIGHLIGHT_CLASSNAME, HIGHLIGHT_CLASSNAME, markNode, unmark, } from '../../../client/src/utils/textSearch'; import SearchOnPage from '../../../client/src/components/SearchOnPage'; const TextSearch = ({ contentRoot, }: { contentRoot: HTMLDivElement | null; }) => { const [searchValue, setSearchValue] = useState(''); const [isSearchActive, setSearchActive] = useState(false); const [resultNum, setResultNum] = useState(0); const [currentResult, setCurrentResult] = useState(0); const [currentHighlightParent, setCurrentHighlightParent] = useState(null); useEffect(() => { const toggleSearch = (e: KeyboardEvent) => { const fullCodeInView = !!document.getElementsByClassName('code-full-view').length; if (e.code === 'KeyF' && e.metaKey && !fullCodeInView) { setSearchActive((prev) => !prev); } else if (e.code === 'Enter') { const isNext = !e.shiftKey; setCurrentResult((prev) => isNext ? prev < resultNum ? prev + 1 : 1 : prev > 1 ? prev - 1 : resultNum, ); } else if (e.code === 'Escape') { setSearchActive((prev) => { if (prev) { e.preventDefault(); } return false; }); } }; window.addEventListener('keypress', toggleSearch); return () => { window.removeEventListener('keypress', toggleSearch); }; }, [resultNum]); useEffect(() => { if (!isSearchActive) { unmark(); } }, [isSearchActive]); const doSearch = useCallback( (searchTerm: string) => { unmark(); if (searchTerm === '') { setResultNum(0); setCurrentResult(0); setCurrentHighlightParent(null); return; } const regex = new RegExp( searchTerm.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&'), 'gi', ); if (contentRoot) { markNode(contentRoot, regex); const allHighlights = document.getElementsByClassName(HIGHLIGHT_CLASSNAME); const resNum = allHighlights.length; setResultNum(resNum); let prevIndexInNewHighlights = currentHighlightParent ? [...allHighlights].findIndex( (el) => el.parentNode?.parentNode?.isSameNode(currentHighlightParent), ) : -1; setCurrentResult((prev) => { const newR = resNum ? prevIndexInNewHighlights >= 0 ? prevIndexInNewHighlights + 1 : 1 : 0; if (prev === newR && allHighlights?.[newR - 1]) { allHighlights[newR - 1].classList?.add(ACTIVE_HIGHLIGHT_CLASSNAME); setCurrentHighlightParent( allHighlights[newR - 1].parentNode as HTMLElement, ); } return newR; }); } }, [contentRoot, currentHighlightParent], ); const handleChange = useCallback( (searchTerm: string) => { setSearchValue(searchTerm); if (searchTerm.length > 1) { doSearch(searchTerm); } }, [doSearch], ); useEffect(() => { const highlights = document.getElementsByClassName(HIGHLIGHT_CLASSNAME); [...highlights].forEach((el) => el.classList.remove(ACTIVE_HIGHLIGHT_CLASSNAME), ); const elementToShow = highlights[currentResult - 1]; if (elementToShow) { setCurrentHighlightParent(elementToShow.parentNode as HTMLElement); elementToShow.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'nearest', }); elementToShow.classList.add(ACTIVE_HIGHLIGHT_CLASSNAME); } }, [currentResult]); return ( { handleChange(''); setSearchActive(false); }} handleSearch={handleChange} isSearchActive={isSearchActive} resultNum={resultNum} currentResult={currentResult} setCurrentResult={setCurrentResult} searchValue={searchValue} containerClassName="fixed top-[100px] right-[5px] w-80" /> ); }; export default TextSearch; ================================================ FILE: apps/desktop/src/global.d.ts ================================================ export {}; ================================================ FILE: apps/desktop/src/main.tsx ================================================ import React from 'react'; import ReactDOM from 'react-dom/client'; import App from './App'; ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( , ); ================================================ FILE: apps/desktop/src/vite-env.d.ts ================================================ /// ================================================ FILE: apps/desktop/src-tauri/.gitignore ================================================ # Generated by Cargo # will have compiled files and executables /target/ ================================================ FILE: apps/desktop/src-tauri/Cargo.toml ================================================ [package] name = "bloop" version = "0.6.4" description = "Search code. Fast." authors = ["Bloop AI Developers"] license = "Apache-2.0" repository = "https://github.com/bloopai/bloop" default-run = "bloop" edition = "2021" rust-version = "1.57" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [build-dependencies] tauri-build = { version = "1.4.1", features = [] } [dependencies] serde_json = "1.0" serde = { version = "1.0", features = ["derive"] } tauri = { version = "1.5.0", features = ["dialog-open", "fs-all", "http-all", "native-tls-vendored", "os-all", "path-all", "process-all", "shell-all", "window-all"] } bleep = { path = "../../../server/bleep", package = "bleep" } anyhow = "1.0.75" tokio = { version = "1.32.0", features = ["rt-multi-thread"] } tracing = "0.1.37" tracing-subscriber = { version = "0.3.17", features = ["env-filter"] } color-eyre = "0.6.2" once_cell = "1.18.0" sentry = "0.31.7" qdrant-client = "1.5.0" git-version = "0.3.5" sentry-anyhow = "0.31.7" sysinfo = "0.29.10" [target.'cfg(unix)'.dependencies] nix = { version = "0.26.4", default-features = false, features = [ "resource" ] } [features] # by default Tauri runs in production mode # when `tauri dev` runs it is executed with `cargo run --no-default-features` if `devPath` is an URL default = ["custom-protocol"] # this feature is used for production builds where `devPath` points to the filesystem # DO NOT remove this custom-protocol = ["tauri/custom-protocol"] ================================================ FILE: apps/desktop/src-tauri/bin/qdrant-aarch64-apple-darwin ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:790591ece18fdc99761f59c31282237b07389ce3dd2eecd50cceb95fddbe96f5 size 52682248 ================================================ FILE: apps/desktop/src-tauri/bin/qdrant-x86_64-apple-darwin ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:de8dad5075c1bcbe403e0df610a2b2933ca49457300cda0af1a104a080e5ce76 size 65580008 ================================================ FILE: apps/desktop/src-tauri/bin/qdrant-x86_64-unknown-linux-gnu ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:4abf2e680bf51edde94dee2ba51b7941c9a72f3748303716a94c9b01aaf21d83 size 49078720 ================================================ FILE: apps/desktop/src-tauri/build.rs ================================================ use std::{ env, fs, path::{Path, PathBuf}, thread, time::Duration, }; fn main() { // we do not require libonnx for apple silicon if !is_apple_silicon() { if env::var("ORT_DYLIB_PATH").is_err() { let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap()); let profile_dir = out_dir // "target/.../build/bloop-hash" .parent() .unwrap() // "target/.../build" .parent() .unwrap() // "target/.../" .parent() .unwrap(); copy(profile_dir); } else { println!("cargo:rerun-if-env-changed=ORT_DYLIB_PATH"); } } tauri_build::build() } fn copy(profile_dir: &Path) { let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap(); let (dylib_names, target_parent) = match target_os.as_str() { "macos" => { let name = "libonnxruntime.dylib"; (vec![name], Path::new(".").join("frameworks")) } "linux" => { let name = "libonnxruntime.so"; (vec![name], Path::new(".").join("dylibs")) } "windows" => { let main = "onnxruntime.dll"; let providers = "onnxruntime_providers_shared.dll"; (vec![main, providers], Path::new(".").join("dylibs")) } other => panic!("unknown OS {other}"), }; for dylib_name in dylib_names { let dylib_path = profile_dir.join(dylib_name); let target_path = target_parent.join(dylib_name); wait_for(&dylib_path); println!("target: {target_path:?}, {:?}", env::current_dir()); fs::copy(dylib_path, target_path).unwrap(); } } fn wait_for(dylib_path: &Path) { println!("waiting for: {dylib_path:?}"); for _ in 0..1000 { if dylib_path.exists() { return; } thread::sleep(Duration::from_millis(500)); } panic!("timeout waiting for ort download"); } fn is_apple_silicon() -> bool { let target = env::var("TARGET").unwrap(); let components: Vec<_> = target.split('-').map(|s| s.to_string()).collect(); components[0] == "aarch64" && components[2] == "darwin" } ================================================ FILE: apps/desktop/src-tauri/config/config.json ================================================ {} ================================================ FILE: apps/desktop/src-tauri/dylibs/.keep ================================================ ================================================ FILE: apps/desktop/src-tauri/frameworks/.keep ================================================ ================================================ FILE: apps/desktop/src-tauri/installer.nsi ================================================ Unicode true ; Set the compression algorithm. Default is LZMA. !if "{{compression}}" == "" SetCompressor /SOLID lzma !else SetCompressor /SOLID "{{compression}}" !endif !include MUI2.nsh !include FileFunc.nsh !include x64.nsh !include WordFunc.nsh !include "StrFunc.nsh" ${StrCase} ${StrLoc} !define MANUFACTURER "{{manufacturer}}" !define PRODUCTNAME "{{product_name}}" !define VERSION "{{version}}" !define VERSIONWITHBUILD "{{version_with_build}}" !define SHORTDESCRIPTION "{{short_description}}" !define INSTALLMODE "{{install_mode}}" !define LICENSE "{{license}}" !define INSTALLERICON "{{installer_icon}}" !define SIDEBARIMAGE "{{sidebar_image}}" !define HEADERIMAGE "{{header_image}}" !define MAINBINARYNAME "{{main_binary_name}}" !define MAINBINARYSRCPATH "{{main_binary_path}}" !define BUNDLEID "{{bundle_id}}" !define COPYRIGHT "{{copyright}}" !define OUTFILE "{{out_file}}" !define ARCH "{{arch}}" !define PLUGINSPATH "{{additional_plugins_path}}" !define ALLOWDOWNGRADES "{{allow_downgrades}}" !define DISPLAYLANGUAGESELECTOR "{{display_language_selector}}" !define INSTALLWEBVIEW2MODE "{{install_webview2_mode}}" !define WEBVIEW2INSTALLERARGS "{{webview2_installer_args}}" !define WEBVIEW2BOOTSTRAPPERPATH "{{webview2_bootstrapper_path}}" !define WEBVIEW2INSTALLERPATH "{{webview2_installer_path}}" !define UNINSTKEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCTNAME}" !define MANUPRODUCTKEY "Software\${MANUFACTURER}\${PRODUCTNAME}" !define UNINSTALLERSIGNCOMMAND "{{uninstaller_sign_cmd}}" Name "${PRODUCTNAME}" BrandingText "${COPYRIGHT}" OutFile "${OUTFILE}" VIProductVersion "${VERSIONWITHBUILD}" VIAddVersionKey "ProductName" "${PRODUCTNAME}" VIAddVersionKey "FileDescription" "${SHORTDESCRIPTION}" VIAddVersionKey "LegalCopyright" "${COPYRIGHT}" VIAddVersionKey "FileVersion" "${VERSION}" VIAddVersionKey "ProductVersion" "${VERSION}" ; Plugins path, currently exists for linux only !if "${PLUGINSPATH}" != "" !addplugindir "${PLUGINSPATH}" !endif !if "${UNINSTALLERSIGNCOMMAND}" != "" !uninstfinalize '${UNINSTALLERSIGNCOMMAND}' !endif ; Handle install mode, `perUser`, `perMachine` or `both` !if "${INSTALLMODE}" == "perMachine" RequestExecutionLevel highest !endif !if "${INSTALLMODE}" == "currentUser" RequestExecutionLevel user !endif !if "${INSTALLMODE}" == "both" !define MULTIUSER_MUI !define MULTIUSER_INSTALLMODE_INSTDIR "${PRODUCTNAME}" !define MULTIUSER_INSTALLMODE_COMMANDLINE !if "${ARCH}" == "x64" !define MULTIUSER_USE_PROGRAMFILES64 !else if "${ARCH}" == "arm64" !define MULTIUSER_USE_PROGRAMFILES64 !endif !define MULTIUSER_INSTALLMODE_DEFAULT_REGISTRY_KEY "${UNINSTKEY}" !define MULTIUSER_INSTALLMODE_DEFAULT_REGISTRY_VALUENAME "CurrentUser" !define MULTIUSER_INSTALLMODEPAGE_SHOWUSERNAME !define MULTIUSER_INSTALLMODE_FUNCTION RestorePreviousInstallLocation !define MULTIUSER_EXECUTIONLEVEL Highest !include MultiUser.nsh !endif ; installer icon !if "${INSTALLERICON}" != "" !define MUI_ICON "${INSTALLERICON}" !endif ; installer sidebar image !if "${SIDEBARIMAGE}" != "" !define MUI_WELCOMEFINISHPAGE_BITMAP "${SIDEBARIMAGE}" !endif ; installer header image !if "${HEADERIMAGE}" != "" !define MUI_HEADERIMAGE !define MUI_HEADERIMAGE_BITMAP "${HEADERIMAGE}" !endif ; Define registry key to store installer language !define MUI_LANGDLL_REGISTRY_ROOT "HKCU" !define MUI_LANGDLL_REGISTRY_KEY "${MANUPRODUCTKEY}" !define MUI_LANGDLL_REGISTRY_VALUENAME "Installer Language" ; Installer pages, must be ordered as they appear ; 1. Welcome Page !define MUI_PAGE_CUSTOMFUNCTION_PRE SkipIfPassive !insertmacro MUI_PAGE_WELCOME ; 2. License Page (if defined) !if "${LICENSE}" != "" !define MUI_PAGE_CUSTOMFUNCTION_PRE SkipIfPassive !insertmacro MUI_PAGE_LICENSE "${LICENSE}" !endif ; 3. Install mode (if it is set to `both`) !if "${INSTALLMODE}" == "both" !define MUI_PAGE_CUSTOMFUNCTION_PRE SkipIfPassive !insertmacro MULTIUSER_PAGE_INSTALLMODE !endif ; 4. Custom page to ask user if he wants to reinstall/uninstall ; only if a previous installtion was detected Var ReinstallPageCheck Page custom PageReinstall PageLeaveReinstall Function PageReinstall Call CheckOldMsiBloopInstall StrCmp $R6 "" 0 compare_version ; Check if there is an existing installation, if not, abort the reinstall page ReadRegStr $R0 SHCTX "${UNINSTKEY}" "" ReadRegStr $R1 SHCTX "${UNINSTKEY}" "UninstallString" ${IfThen} "$R0$R1" == "" ${|} Abort ${|} ; Compare this installar version with the existing installation ; and modify the messages presented to the user accordingly compare_version: StrCpy $R4 "$(older)" ${If} $R7 == "msi" ReadRegStr $R0 HKLM "$R6" "DisplayVersion" ${Else} ReadRegStr $R0 SHCTX "${UNINSTKEY}" "DisplayVersion" ${EndIf} ${IfThen} $R0 == "" ${|} StrCpy $R4 "$(unknown)" ${|} nsis_tauri_utils::SemverCompare "${VERSION}" $R0 Pop $R0 ; Reinstalling the same version ${If} $R0 == 0 StrCpy $R1 "$(alreadyInstalledLong)" StrCpy $R2 "$(addOrReinstall)" StrCpy $R3 "$(uninstallApp)" !insertmacro MUI_HEADER_TEXT "$(alreadyInstalled)" "$(chooseMaintenanceOption)" StrCpy $R5 "2" ; Upgrading ${ElseIf} $R0 == 1 StrCpy $R1 "$(olderOrUnknownVersionInstalled)" StrCpy $R2 "$(uninstallBeforeInstalling)" StrCpy $R3 "$(dontUninstall)" !insertmacro MUI_HEADER_TEXT "$(alreadyInstalled)" "$(choowHowToInstall)" StrCpy $R5 "1" ; Downgrading ${ElseIf} $R0 == -1 StrCpy $R1 "$(newerVersionInstalled)" StrCpy $R2 "$(uninstallBeforeInstalling)" !if "${ALLOWDOWNGRADES}" == "true" StrCpy $R3 "$(dontUninstall)" !else StrCpy $R3 "$(dontUninstallDowngrade)" !endif !insertmacro MUI_HEADER_TEXT "$(alreadyInstalled)" "$(choowHowToInstall)" StrCpy $R5 "1" ${Else} Abort ${EndIf} Call SkipIfPassive nsDialogs::Create 1018 Pop $R4 ${IfThen} $(^RTL) == 1 ${|} nsDialogs::SetRTL $(^RTL) ${|} ${NSD_CreateLabel} 0 0 100% 24u $R1 Pop $R1 ${NSD_CreateRadioButton} 30u 50u -30u 8u $R2 Pop $R2 ${NSD_OnClick} $R2 PageReinstallUpdateSelection ${NSD_CreateRadioButton} 30u 70u -30u 8u $R3 Pop $R3 ; disable this radio button if downgrading and downgrades are disabled !if "${ALLOWDOWNGRADES}" == "false" ${IfThen} $R0 == -1 ${|} EnableWindow $R3 0 ${|} !endif ${NSD_OnClick} $R3 PageReinstallUpdateSelection ; Check the first radio button if this the first time ; we enter this page or if the second button wasn't ; selected the last time we were on this page ${If} $ReinstallPageCheck != 2 SendMessage $R2 ${BM_SETCHECK} ${BST_CHECKED} 0 ${Else} SendMessage $R3 ${BM_SETCHECK} ${BST_CHECKED} 0 ${EndIf} ${NSD_SetFocus} $R2 nsDialogs::Show FunctionEnd Function PageReinstallUpdateSelection ${NSD_GetState} $R2 $R1 ${If} $R1 == ${BST_CHECKED} StrCpy $ReinstallPageCheck 1 ${Else} StrCpy $ReinstallPageCheck 2 ${EndIf} FunctionEnd Function PageLeaveReinstall ${NSD_GetState} $R2 $R1 ; $R5 holds whether we are reinstalling the same version or not ; $R5 == "1" -> different versions ; $R5 == "2" -> same version ; ; $R1 holds the radio buttons state. its meaning is dependant on the context StrCmp $R5 "1" 0 +2 ; Existing install is not the same version? StrCmp $R1 "1" reinst_uninstall reinst_done ; $R1 == "1", then user chose to uninstall existing version, otherwise skip uninstalling StrCmp $R1 "1" reinst_done ; Same version? skip uninstalling reinst_uninstall: HideWindow ClearErrors ${If} $R7 == "msi" ReadRegStr $R1 HKLM "$R6" "UninstallString" ExecWait '$R1' $0 ${Else} ReadRegStr $4 SHCTX "${MANUPRODUCTKEY}" "" ReadRegStr $R1 SHCTX "${UNINSTKEY}" "UninstallString" ExecWait '$R1 /P _?=$4' $0 ${EndIf} BringToFront ${IfThen} ${Errors} ${|} StrCpy $0 2 ${|} ; ExecWait failed, set fake exit code ${If} $0 <> 0 ${OrIf} ${FileExists} "$INSTDIR\${MAINBINARYNAME}.exe" ${If} $0 = 1 ; User aborted uninstaller? StrCmp $R5 "2" 0 +2 ; Is the existing install the same version? Quit ; ...yes, already installed, we are done Abort ${EndIf} MessageBox MB_ICONEXCLAMATION "$(unableToUninstall)" Abort ${Else} StrCpy $0 $R1 1 ${IfThen} $0 == '"' ${|} StrCpy $R1 $R1 -1 1 ${|} ; Strip quotes from UninstallString Delete $R1 RMDir $INSTDIR ${EndIf} reinst_done: FunctionEnd ; 5. Choose install directoy page !define MUI_PAGE_CUSTOMFUNCTION_PRE SkipIfPassive !insertmacro MUI_PAGE_DIRECTORY ; 6. Start menu shortcut page !define MUI_PAGE_CUSTOMFUNCTION_PRE SkipIfPassive Var AppStartMenuFolder !insertmacro MUI_PAGE_STARTMENU Application $AppStartMenuFolder ; 7. Installation page !insertmacro MUI_PAGE_INSTFILES ; 8. Finish page ; ; Don't auto jump to finish page after installation page, ; because the installation page has useful info that can be used debug any issues with the installer. !define MUI_FINISHPAGE_NOAUTOCLOSE ; Use show readme button in the finish page as a button create a desktop shortcut !define MUI_FINISHPAGE_SHOWREADME !define MUI_FINISHPAGE_SHOWREADME_TEXT "$(createDesktop)" !define MUI_FINISHPAGE_SHOWREADME_FUNCTION CreateDesktopShortcut ; Show run app after installation. !define MUI_FINISHPAGE_RUN "$INSTDIR\${MAINBINARYNAME}.exe" !define MUI_PAGE_CUSTOMFUNCTION_PRE SkipIfPassive !insertmacro MUI_PAGE_FINISH ; Uninstaller Pages ; 1. Confirm uninstall page Var DeleteAppDataCheckbox Var DeleteAppDataCheckboxState !define /ifndef WS_EX_LAYOUTRTL 0x00400000 !define MUI_PAGE_CUSTOMFUNCTION_SHOW un.ConfirmShow Function un.ConfirmShow FindWindow $1 "#32770" "" $HWNDPARENT ; Find inner dialog ${If} $(^RTL) == 1 System::Call 'USER32::CreateWindowEx(i${__NSD_CheckBox_EXSTYLE}|${WS_EX_LAYOUTRTL},t"${__NSD_CheckBox_CLASS}",t "$(deleteAppData)",i${__NSD_CheckBox_STYLE},i 50,i 100,i 400, i 25,i$1,i0,i0,i0)i.s' ${Else} System::Call 'USER32::CreateWindowEx(i${__NSD_CheckBox_EXSTYLE},t"${__NSD_CheckBox_CLASS}",t "$(deleteAppData)",i${__NSD_CheckBox_STYLE},i 0,i 100,i 400, i 25,i$1,i0,i0,i0)i.s' ${EndIf} Pop $DeleteAppDataCheckbox SendMessage $HWNDPARENT ${WM_GETFONT} 0 0 $1 SendMessage $DeleteAppDataCheckbox ${WM_SETFONT} $1 1 FunctionEnd !define MUI_PAGE_CUSTOMFUNCTION_LEAVE un.ConfirmLeave Function un.ConfirmLeave SendMessage $DeleteAppDataCheckbox ${BM_GETCHECK} 0 0 $DeleteAppDataCheckboxState FunctionEnd !insertmacro MUI_UNPAGE_CONFIRM ; 2. Uninstalling Page !insertmacro MUI_UNPAGE_INSTFILES ;Languages {{#each languages}} !insertmacro MUI_LANGUAGE "{{this}}" {{/each}} !insertmacro MUI_RESERVEFILE_LANGDLL {{#each language_files}} !include "{{this}}" {{/each}} !macro SetContext !if "${INSTALLMODE}" == "currentUser" SetShellVarContext current !else if "${INSTALLMODE}" == "perMachine" SetShellVarContext all !endif ${If} ${RunningX64} !if "${ARCH}" == "x64" SetRegView 64 !else if "${ARCH}" == "arm64" SetRegView 64 !else SetRegView 32 !endif ${EndIf} !macroend Var PassiveMode Function .onInit ${GetOptions} $CMDLINE "/P" $PassiveMode IfErrors +2 0 StrCpy $PassiveMode 1 !if "${DISPLAYLANGUAGESELECTOR}" == "true" !insertmacro MUI_LANGDLL_DISPLAY !endif !insertmacro SetContext ${If} $INSTDIR == "" ; Set default install location !if "${INSTALLMODE}" == "perMachine" ${If} ${RunningX64} !if "${ARCH}" == "x64" StrCpy $INSTDIR "$PROGRAMFILES64\${PRODUCTNAME}" !else if "${ARCH}" == "arm64" StrCpy $INSTDIR "$PROGRAMFILES64\${PRODUCTNAME}" !else StrCpy $INSTDIR "$PROGRAMFILES\${PRODUCTNAME}" !endif ${Else} StrCpy $INSTDIR "$PROGRAMFILES\${PRODUCTNAME}" ${EndIf} !else if "${INSTALLMODE}" == "currentUser" StrCpy $INSTDIR "$LOCALAPPDATA\${PRODUCTNAME}" !endif Call RestorePreviousInstallLocation ${EndIf} !if "${INSTALLMODE}" == "both" !insertmacro MULTIUSER_INIT !endif FunctionEnd Section EarlyChecks ; Abort silent installer if downgrades is disabled !if "${ALLOWDOWNGRADES}" == "false" IfSilent 0 silent_downgrades_done ; If downgrading ${If} $R0 == -1 System::Call 'kernel32::AttachConsole(i -1)i.r0' ${If} $0 != 0 System::Call 'kernel32::GetStdHandle(i -11)i.r0' System::call 'kernel32::SetConsoleTextAttribute(i r0, i 0x0004)' ; set red color FileWrite $0 "$(silentDowngrades)" ${EndIf} Abort ${EndIf} silent_downgrades_done: !endif SectionEnd Section WebView2 ; Check if Webview2 is already installed and skip this section ${If} ${RunningX64} ReadRegStr $4 HKLM "SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv" ${Else} ReadRegStr $4 HKLM "SOFTWARE\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv" ${EndIf} ReadRegStr $5 HKCU "SOFTWARE\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv" StrCmp $4 "" 0 webview2_done StrCmp $5 "" 0 webview2_done ; Webview2 install modes !if "${INSTALLWEBVIEW2MODE}" == "downloadBootstrapper" Delete "$TEMP\MicrosoftEdgeWebview2Setup.exe" DetailPrint "$(webview2Downloading)" nsis_tauri_utils::download "https://go.microsoft.com/fwlink/p/?LinkId=2124703" "$TEMP\MicrosoftEdgeWebview2Setup.exe" Pop $0 ${If} $0 == 0 DetailPrint "$(webview2DownloadSuccess)" ${Else} DetailPrint "$(webview2DownloadError)" Abort "$(webview2AbortError)" ${EndIf} StrCpy $6 "$TEMP\MicrosoftEdgeWebview2Setup.exe" Goto install_webview2 !endif !if "${INSTALLWEBVIEW2MODE}" == "embedBootstrapper" Delete "$TEMP\MicrosoftEdgeWebview2Setup.exe" File "/oname=$TEMP\MicrosoftEdgeWebview2Setup.exe" "${WEBVIEW2BOOTSTRAPPERPATH}" DetailPrint "$(installingWebview2)" StrCpy $6 "$TEMP\MicrosoftEdgeWebview2Setup.exe" Goto install_webview2 !endif !if "${INSTALLWEBVIEW2MODE}" == "offlineInstaller" Delete "$TEMP\MicrosoftEdgeWebView2RuntimeInstaller.exe" File "/oname=$TEMP\MicrosoftEdgeWebView2RuntimeInstaller.exe" "${WEBVIEW2INSTALLERPATH}" DetailPrint "$(installingWebview2)" StrCpy $6 "$TEMP\MicrosoftEdgeWebView2RuntimeInstaller.exe" Goto install_webview2 !endif Goto webview2_done install_webview2: DetailPrint "$(installingWebview2)" ; $6 holds the path to the webview2 installer ExecWait "$6 ${WEBVIEW2INSTALLERARGS} /install" $1 ${If} $1 == 0 DetailPrint "$(webview2InstallSuccess)" ${Else} DetailPrint "$(webview2InstallError)" Abort "$(webview2AbortError)" ${EndIf} webview2_done: SectionEnd !macro CheckIfAppIsRunning nsis_tauri_utils::FindProcess "${MAINBINARYNAME}.exe" Pop $R0 ${If} $R0 = 0 IfSilent kill 0 ${IfThen} $PassiveMode != 1 ${|} MessageBox MB_OKCANCEL "$(appRunningOkKill)" IDOK kill IDCANCEL cancel ${|} kill: nsis_tauri_utils::KillProcess "${MAINBINARYNAME}.exe" Pop $R0 Sleep 500 ${If} $R0 = 0 Goto app_check_done ${Else} IfSilent silent ui silent: System::Call 'kernel32::AttachConsole(i -1)i.r0' ${If} $0 != 0 System::Call 'kernel32::GetStdHandle(i -11)i.r0' System::call 'kernel32::SetConsoleTextAttribute(i r0, i 0x0004)' ; set red color FileWrite $0 "$(appRunning)$\n" ${EndIf} Abort ui: Abort "$(failedToKillApp)" ${EndIf} cancel: Abort "$(appRunning)" ${EndIf} app_check_done: !macroend Var AppSize Section Install SetOutPath $INSTDIR StrCpy $AppSize 0 !insertmacro CheckIfAppIsRunning ; Copy main executable File "${MAINBINARYSRCPATH}" ${GetSize} "$INSTDIR" "/M=${MAINBINARYNAME}.exe /S=0B" $0 $1 $2 IntOp $AppSize $AppSize + $0 ; Copy resources {{#each resources}} CreateDirectory "$INSTDIR\\{{this.[0]}}" File /a "/oname={{this.[1]}}" "{{@key}}" ${GetSize} "$INSTDIR" "/M={{this.[1]}} /S=0B" $0 $1 $2 IntOp $AppSize $AppSize + $0 {{/each}} ; Copy external binaries {{#each binaries}} File /a "/oname={{this}}" "{{@key}}" ${GetSize} "$INSTDIR" "/M={{this}} /S=0B" $0 $1 $2 IntOp $AppSize $AppSize + $0 {{/each}} ; Create uninstaller WriteUninstaller "$INSTDIR\uninstall.exe" ; Save $INSTDIR in registry for future installations WriteRegStr SHCTX "${MANUPRODUCTKEY}" "" $INSTDIR !if "${INSTALLMODE}" == "both" ; Save install mode to be selected by default for the next installation such as updating ; or when uninstalling WriteRegStr SHCTX "${UNINSTKEY}" $MultiUser.InstallMode 1 !endif ; Registry information for add/remove programs WriteRegStr SHCTX "${UNINSTKEY}" "DisplayName" "${PRODUCTNAME}" WriteRegStr SHCTX "${UNINSTKEY}" "DisplayIcon" "$\"$INSTDIR\${MAINBINARYNAME}.exe$\"" WriteRegStr SHCTX "${UNINSTKEY}" "DisplayVersion" "${VERSION}" WriteRegStr SHCTX "${UNINSTKEY}" "Publisher" "${MANUFACTURER}" WriteRegStr SHCTX "${UNINSTKEY}" "InstallLocation" "$\"$INSTDIR$\"" WriteRegStr SHCTX "${UNINSTKEY}" "UninstallString" "$\"$INSTDIR\uninstall.exe$\"" WriteRegDWORD SHCTX "${UNINSTKEY}" "NoModify" "1" WriteRegDWORD SHCTX "${UNINSTKEY}" "NoRepair" "1" IntOp $AppSize $AppSize / 1000 IntFmt $AppSize "0x%08X" $AppSize WriteRegDWORD SHCTX "${UNINSTKEY}" "EstimatedSize" "$AppSize" ; Create start menu shortcut (GUI) !insertmacro MUI_STARTMENU_WRITE_BEGIN Application Call CreateStartMenuShortcut !insertmacro MUI_STARTMENU_WRITE_END ; Create shortcuts for silent and passive installers, which ; can be disabled by passing `/NS` flag ; GUI installer has buttons for users to control creating them IfSilent check_ns_flag 0 ${IfThen} $PassiveMode == 1 ${|} Goto check_ns_flag ${|} Goto shortcuts_done check_ns_flag: ${GetOptions} $CMDLINE "/NS" $R0 IfErrors 0 shortcuts_done Call CreateDesktopShortcut Call CreateStartMenuShortcut shortcuts_done: ; Auto close this page for passive mode ${IfThen} $PassiveMode == 1 ${|} SetAutoClose true ${|} SectionEnd Function .onInstSuccess ; Check for `/R` flag only in silent and passive installers because ; GUI installer has a toggle for the user to (re)start the app IfSilent check_r_flag 0 ${IfThen} $PassiveMode == 1 ${|} Goto check_r_flag ${|} Goto run_done check_r_flag: ${GetOptions} $CMDLINE "/R" $R0 IfErrors run_done 0 Exec '"$INSTDIR\${MAINBINARYNAME}.exe"' run_done: FunctionEnd Function un.onInit !insertmacro SetContext !if "${INSTALLMODE}" == "both" !insertmacro MULTIUSER_UNINIT !endif !insertmacro MUI_UNGETLANGUAGE FunctionEnd Section Uninstall !insertmacro CheckIfAppIsRunning ; Delete the app directory and its content from disk ; Copy main executable Delete "$INSTDIR\${MAINBINARYNAME}.exe" ; Delete resources {{#each resources}} Delete "$INSTDIR\\{{this.[1]}}" RMDir "$INSTDIR\\{{this.[0]}}" {{/each}} ; Delete external binaries {{#each binaries}} Delete "$INSTDIR\\{{this}}" {{/each}} ; Delete uninstaller Delete "$INSTDIR\uninstall.exe" RMDir "$INSTDIR" ; Remove start menu shortcut !insertmacro MUI_STARTMENU_GETFOLDER Application $AppStartMenuFolder Delete "$SMPROGRAMS\$AppStartMenuFolder\${MAINBINARYNAME}.lnk" RMDir "$SMPROGRAMS\$AppStartMenuFolder" ; Remove desktop shortcuts Delete "$DESKTOP\${MAINBINARYNAME}.lnk" ; Remove registry information for add/remove programs !if "${INSTALLMODE}" == "both" DeleteRegKey SHCTX "${UNINSTKEY}" !else if "${INSTALLMODE}" == "perMachine" DeleteRegKey HKLM "${UNINSTKEY}" !else DeleteRegKey HKCU "${UNINSTKEY}" !endif DeleteRegValue HKCU "${MANUPRODUCTKEY}" "Installer Language" ; Delete app data ${If} $DeleteAppDataCheckboxState == 1 SetShellVarContext current RmDir /r "$APPDATA\${BUNDLEID}" RmDir /r "$LOCALAPPDATA\${BUNDLEID}" ${EndIf} ${GetOptions} $CMDLINE "/P" $R0 IfErrors +2 0 SetAutoClose true SectionEnd Function RestorePreviousInstallLocation ReadRegStr $4 SHCTX "${MANUPRODUCTKEY}" "" StrCmp $4 "" +2 0 StrCpy $INSTDIR $4 FunctionEnd Function SkipIfPassive ${IfThen} $PassiveMode == 1 ${|} Abort ${|} FunctionEnd Function CreateDesktopShortcut CreateShortcut "$DESKTOP\${MAINBINARYNAME}.lnk" "$INSTDIR\${MAINBINARYNAME}.exe" ApplicationID::Set "$DESKTOP\${MAINBINARYNAME}.lnk" "${BUNDLEID}" FunctionEnd Function CreateStartMenuShortcut CreateDirectory "$SMPROGRAMS\$AppStartMenuFolder" CreateShortcut "$SMPROGRAMS\$AppStartMenuFolder\${MAINBINARYNAME}.lnk" "$INSTDIR\${MAINBINARYNAME}.exe" ApplicationID::Set "$SMPROGRAMS\$AppStartMenuFolder\${MAINBINARYNAME}.lnk" "${BUNDLEID}" FunctionEnd Var CurrentKey Var KeyIdx Function CheckOldMsiBloopInstall StrCpy $KeyIdx 0 loop: EnumRegKey $CurrentKey HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall" $KeyIdx ; Check against each GUID in the provided list ; 0.5.7 StrCmp $CurrentKey "{2C8429F8-54EB-467C-BE37-C8DCD3FCE6BC}" found ; 0.5.6 StrCmp $CurrentKey "{4A778A50-FD64-4E96-BA76-35F5D66B8899}" found ; 0.5.5 StrCmp $CurrentKey "{ABCE7896-96F3-4DE1-885F-2634F0CAB59A}" found ; 0.5.4 StrCmp $CurrentKey "{6ED61500-55C2-4E6F-83EE-951F91904EA5}" found ; 0.5.3 StrCmp $CurrentKey "{E86D72B5-7DB3-4601-9926-77B2767730DB}" found ; 0.5.2 StrCmp $CurrentKey "{97C55FFD-6A65-4BA1-ABAE-40EDE303A5F6}" found ; 0.5.1 StrCmp $CurrentKey "{C84E9599-EAFF-4F07-B216-9EF97F07A25A}" found ; 0.5.0 StrCmp $CurrentKey "{0F439DE0-93F0-48E9-BEFE-6D43F203A231}" found ; 0.4.17 StrCmp $CurrentKey "{1302A8FE-5796-4B36-AD6C-2F525CC18158} " found ; 0.4.16 StrCmp $CurrentKey "{EC1C0015-E851-4D47-B758-78FAFDCDA9B5}" found ; 0.4.15 StrCmp $CurrentKey "{7BADAB06-87E8-4DF5-9262-0917D61D70F8}" found ; 0.4.14 StrCmp $CurrentKey "{B3030CB3-5DDC-41A8-84FA-A3A02BF1054B}" found ; 0.4.13 StrCmp $CurrentKey "{14515A1F-8824-49A1-80A6-E43F529627D2}" found ; 0.4.12 StrCmp $CurrentKey "{97594AB3-17EB-4B4D-BF6E-BC14AA55439C}" found ; 0.4.11 StrCmp $CurrentKey "{4B855395-4A08-4DF3-9EC9-21BD49658925}" found ; 0.4.10 (had no Windows build) ; ... ; 0.4.9 StrCmp $CurrentKey "{7954AC40-85A5-4705-93C8-7ECE1EA779A8}" found ; 0.4.8 StrCmp $CurrentKey "{756A33B8-AABE-4010-88E1-CD967371B26A}" found StrCmp $CurrentKey "" notfound ; If none of the GUIDs match, continue with the next key IntOp $KeyIdx $KeyIdx + 1 Goto loop notfound: StrCpy $R6 "" Goto done found: StrCpy $R7 "msi" StrCpy $R6 "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$CurrentKey" done: FunctionEnd ================================================ FILE: apps/desktop/src-tauri/model/ggml/tokenizer.json ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:65c14be98b474267fa3b8129ac643ca9356234b54bb19d8d1b95ca67b55f469f size 466248 ================================================ FILE: apps/desktop/src-tauri/model/model.onnx ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:712cda19c5e4803e4d2f1d5ae972083537ffc2531759b67641622251c8ec3caf size 22998413 ================================================ FILE: apps/desktop/src-tauri/model/special_tokens_map.json ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:b6d346be366a7d1d48332dbc9fdf3bf8960b5d879522b7799ddba59e76237ee3 size 125 ================================================ FILE: apps/desktop/src-tauri/model/tokenizer.json ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:d241a60d5e8f04cc1b2b3e9ef7a4921b27bf526d9f6050ab90f9267a1f9e5c66 size 711396 ================================================ FILE: apps/desktop/src-tauri/model/tokenizer_config.json ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:5a0279327ed471a6e4cfb88b6f62f73d8e8ddf3fdc1e9cbb98f692b1b9ae0d91 size 579 ================================================ FILE: apps/desktop/src-tauri/model/vocab.txt ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:07eced375cec144d27c900241f3e339478dec958f92fddbc551f295c992038a3 size 231508 ================================================ FILE: apps/desktop/src-tauri/src/QDRANT_CONFIG_TEMPLATE.yml ================================================ telemetry_disabled: true storage: # Where to store all the data storage_path: {storage} # Where to store snapshots snapshots_path: {snapshots} # If true - point's payload will not be stored in memory. # It will be read from the disk every time it is requested. # This setting saves RAM by (slightly) increasing the response time. # Note: those payload values that are involved in filtering and are indexed - remain in RAM. on_disk_payload: false # Write-ahead-log related configuration wal: # Size of a single WAL segment wal_capacity_mb: 32 # Number of WAL segments to create ahead of actual data requirement wal_segments_ahead: 0 performance: # Number of parallel threads used for search operations. If 0 - auto selection. max_search_threads: 4 optimizers: # The minimal fraction of deleted vectors in a segment, required to perform segment optimization deleted_threshold: 0.2 # The minimal number of vectors in a segment, required to perform segment optimization vacuum_min_vector_number: 1000 # Target amount of segments optimizer will try to keep. # Real amount of segments may vary depending on multiple parameters: # - Amount of stored points # - Current write RPS # # It is recommended to select default number of segments as a factor of the number of search threads, # so that each segment would be handled evenly by one of the threads. # If `default_segment_number = 0`, will be automatically selected by the number of available CPUs default_segment_number: 2 # Do not create segments larger this size (in KiloBytes). # Large segments might require disproportionately long indexation times, # therefore it makes sense to limit the size of segments. # # If indexation speed have more priority for your - make this parameter lower. # If search speed is more important - make this parameter higher. # Note: 1Kb = 1 vector of size 256 # If not set, will be automatically selected considering the number of available CPUs. max_segment_size_kb: null # Maximum size (in KiloBytes) of vectors to store in-memory per segment. # Segments larger than this threshold will be stored as read-only memmaped file. # To enable memmap storage, lower the threshold # Note: 1Kb = 1 vector of size 256 # If not set, mmap will not be used. memmap_threshold_kb: null # Maximum size (in KiloBytes) of vectors allowed for plain index. # Default value based on https://github.com/google-research/google-research/blob/master/scann/docs/algorithms.md # Note: 1Kb = 1 vector of size 256 indexing_threshold_kb: 20000 # Interval between forced flushes. flush_interval_sec: 5 # Max number of threads, which can be used for optimization. max_optimization_threads: 1 # Default parameters of HNSW Index. Could be overridden for each collection individually hnsw_index: # Number of edges per node in the index graph. Larger the value - more accurate the search, more space required. m: 16 # Number of neighbours to consider during the index building. Larger the value - more accurate the search, more time required to build index. ef_construct: 100 # Minimal size (in KiloBytes) of vectors for additional payload-based indexing. # If payload chunk is smaller than `full_scan_threshold_kb` additional indexing won't be used - # in this case full-scan search should be preferred by query planner and additional indexing is not required. # Note: 1Kb = 1 vector of size 256 full_scan_threshold_kb: 10000 # Number of parallel threads used for background index building. If 0 - auto selection. max_indexing_threads: 0 service: # Maximum size of POST data in a single request in megabytes max_request_size_mb: 32 # Number of parallel workers used for serving the api. If 0 - equal to the number of available cores. # If missing - Same as storage.max_search_threads max_workers: 0 # Host to bind the service on host: 127.0.0.1 # HTTP port to bind the service on http_port: 6333 # gRPC port to bind the service on. # If `null` - gRPC is disabled. Default: null # Uncomment to enable gRPC: grpc_port: 6334 # Enable CORS headers in REST API. # If enabled, browsers would be allowed to query REST endpoints regardless of query origin. # More info: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS # Default: true enable_cors: true cluster: # Use `enabled: true` to run Qdrant in distributed deployment mode enabled: false # Configuration of the inter-cluster communication p2p: # Port for internal communication between peers port: 6335 # Configuration related to distributed consensus algorithm consensus: # How frequently peers should ping each other. # Setting this parameter to lower value will allow consensus # to detect disconnected nodes earlier, but too frequent # tick period may create significant network and CPU overhead. # We encourage you NOT to change this parameter unless you know what you are doing. tick_period_ms: 100 ================================================ FILE: apps/desktop/src-tauri/src/backend.rs ================================================ use bleep::{Application, Configuration, Environment}; use tracing::error; use super::{Manager, Payload, Runtime}; use std::thread; use std::time::Duration; #[tauri::command] pub fn get_last_log_file(config: tauri::State) -> Option { let log_dir = config.log_dir(); let mut entries = std::fs::read_dir(log_dir) .ok()? .collect::, _>>() .ok()?; // Sort the entries by modified time (most recent first) entries.sort_by_key(|entry| { entry .metadata() .and_then(|m| m.modified()) .unwrap_or(std::time::SystemTime::UNIX_EPOCH) }); entries.reverse(); // The first entry is the most recent log file let filename = match entries.first() { Some(path) => path.path().to_string_lossy().to_string(), None => { tracing::warn!("No log files found"); return None; } }; std::fs::read_to_string(filename).ok() } pub fn initialize(app: &mut tauri::App) -> tauri::plugin::Result<()> { let handle = app.handle(); let configuration = crate::config::init(&app.handle()).clone(); app.manage(configuration.clone()); let runtime = tokio::runtime::Builder::new_multi_thread() .enable_all() .thread_name("bleep-backend") .build() .unwrap(); runtime.spawn(start_backend(configuration, handle)); app.manage(runtime); Ok(()) } async fn wait_for_qdrant() -> anyhow::Result<()> { use qdrant_client::prelude::*; let qdrant = QdrantClient::new(Some(QdrantClientConfig::from_url("http://127.0.0.1:6334"))).unwrap(); for _ in 0..60 { if qdrant.health_check().await.is_ok() { return Ok(()); } tokio::time::sleep(std::time::Duration::from_secs(1)).await; } anyhow::bail!("qdrant cannot be started"); } async fn start_backend(configuration: Configuration, app: tauri::AppHandle) { tracing::info!("booting bleep back-end"); if let Err(err) = wait_for_qdrant().await { error!(?err, "qdrant failed to come up"); thread::sleep(Duration::from_secs(4)); app.emit_all( "server-crashed", Payload { message: "Failed to start qdrant".into(), }, ) .unwrap(); }; app.manage(configuration.clone()); let initialized = Application::initialize(Environment::insecure_local(), configuration).await; match initialized { Ok(backend) => { if let Err(err) = backend.run().await { error!(?err, "server crashed error"); app.emit_all( "server-crashed", Payload { message: err.to_string(), }, ) .unwrap() } } Err(err) => { error!(?err, "server failed to start"); app.emit_all( "server-crashed", Payload { message: "Something bad happened".into(), }, ) .unwrap(); } } } // ensure that the leading header and trailer are stripped #[cfg(windows)] #[test] fn device_id_on_single_line() { assert_eq!(get_device_id().lines().count(), 1) } ================================================ FILE: apps/desktop/src-tauri/src/config.rs ================================================ use bleep::Configuration; use once_cell::sync::OnceCell; use tauri::Runtime; static CONFIG: OnceCell = OnceCell::new(); pub fn init(app: &tauri::AppHandle) -> &Configuration { CONFIG.get_or_init(|| { let config = create_configuration(app); // only after that set up bleep logging hooks bleep::Application::install_logging(&config); config }) } fn create_configuration(app: &tauri::AppHandle) -> Configuration { let path = app .path_resolver() .resolve_resource("config/config.json") .expect("failed to resolve resource"); let mut bundled = Configuration::read(path).unwrap(); bundled.qdrant_url = "http://127.0.0.1:6334".into(); bundled.max_threads = bleep::default_parallelism() / 2; bundled.model_dir = app .path_resolver() .resolve_resource("model") .expect("bad bundle"); bundled.dylib_dir = Some(if cfg!(all(target_os = "macos", debug_assertions)) { app.path_resolver() .resolve_resource("dylibs") .expect("missing `apps/desktop/src-tauri/dylibs`") .parent() .expect("invalid path") .to_owned() } else if cfg!(target_os = "macos") { app.path_resolver() .resolve_resource("dylibs") .expect("missing `apps/desktop/src-tauri/dylibs`") .parent() .expect("invalid path") .parent() .expect("invalid path") .join("Frameworks") } else { app.path_resolver() .resolve_resource("dylibs") .expect("missing `apps/desktop/src-tauri/dylibs`") }); let data_dir = app.path_resolver().app_data_dir().unwrap(); bundled.index_dir = data_dir.join("bleep"); Configuration::merge( bundled, Configuration::cli_overriding_config_file().unwrap(), ) } ================================================ FILE: apps/desktop/src-tauri/src/main.rs ================================================ #![cfg_attr( all(not(debug_assertions), target_os = "windows"), windows_subsystem = "windows" )] mod backend; mod config; mod qdrant; use sysinfo::{ProcessExt, ProcessRefreshKind, RefreshKind, Signal, System, SystemExt}; pub use tauri::{plugin, App, Manager, Runtime}; use std::{path::PathBuf, thread, time::Duration}; // the payload type must implement `Serialize` and `Clone`. #[derive(Clone, serde::Serialize)] struct Payload { message: String, } fn relative_command_path(command: impl AsRef) -> Option { let cmd = if cfg!(windows) { format!("{}.exe", command.as_ref()) } else { command.as_ref().into() }; std::env::current_exe() .ok()? .parent() .map(|dir| dir.join(cmd)) .filter(|path| path.is_file()) } fn main() { cleanup_old_processes(); _ = color_eyre::install(); tauri::Builder::default() .plugin(qdrant::QdrantSupervisor::default()) .setup(backend::initialize) .invoke_handler(tauri::generate_handler![ show_folder_in_finder, show_main_window, backend::get_last_log_file, ]) .run(tauri::generate_context!()) .expect("error running tauri application"); } #[tauri::command] fn show_main_window(app_handle: tauri::AppHandle) { if let Some(window) = app_handle.get_window("main") { if !cfg!(target_os = "macos") { window.unminimize().unwrap(); } window.unminimize().unwrap(); window.set_focus().unwrap(); window.show().unwrap(); } } #[tauri::command] fn show_folder_in_finder(path: String) { let path = PathBuf::from(path).canonicalize().unwrap(); #[cfg(target_os = "macos")] { std::process::Command::new("open") .arg(path) .arg("-R") // will reveal the file in finder instead of opening it .spawn() .unwrap(); } #[cfg(target_os = "linux")] { std::process::Command::new("xdg-open") .arg(path) .spawn() .unwrap(); } #[cfg(target_os = "windows")] { std::process::Command::new("explorer") .arg(path) .spawn() .unwrap(); } } fn cleanup_old_processes() { const PROCESS_BLACKLIST: &[&str] = &["qdrant", "bleep"]; // Limit total open files from `sysinfo` crate on Linux. sysinfo::set_open_files_limit(10); let mut sys = System::new_with_specifics(RefreshKind::new().with_processes(ProcessRefreshKind::new())); for name in PROCESS_BLACKLIST { for process in sys.processes_by_exact_name(name) { if process.kill_with(Signal::Term).is_none() && !process.kill() { tracing::error!(?name, "was not able to close existing process"); } } } // We now wait for these processes to close. let mut remaining_procs = vec![]; for _ in 0..10 { thread::sleep(Duration::from_millis(500)); sys.refresh_processes(); remaining_procs = PROCESS_BLACKLIST .iter() .flat_map(|name| sys.processes_by_exact_name(name)) .collect(); if remaining_procs.is_empty() { break; } } // As a last-ditch resort, kill any remaining processes. for proc in remaining_procs { proc.kill(); } } ================================================ FILE: apps/desktop/src-tauri/src/qdrant.rs ================================================ use std::{ fs::{create_dir_all, write, File}, path::{Path, PathBuf}, process::{Child, Command}, }; use tauri::{plugin::Plugin, Runtime}; use tracing::{error, info, warn}; use super::relative_command_path; #[derive(Default)] pub(super) struct QdrantSupervisor { child: Option, stdout_file: Option, stderr_file: Option, } impl Plugin for QdrantSupervisor where R: Runtime, { fn name(&self) -> &'static str { "qdrant" } fn initialize( &mut self, app: &tauri::AppHandle, _config: serde_json::Value, ) -> tauri::plugin::Result<()> { // initialize the system configuration let _initialize_config = crate::config::init(app); let data_dir = app.path_resolver().app_data_dir().unwrap(); let qdrant_dir = data_dir.join("qdrant"); let qd_config_dir = qdrant_dir.join("config"); let qd_logs_dir = qdrant_dir.join("logs"); let stdout_file = qd_logs_dir.join("latest.stdout"); let stderr_file = qd_logs_dir.join("latest.stderr"); create_dir_all(&qd_config_dir).unwrap(); create_dir_all(&qd_logs_dir).unwrap(); write( qd_config_dir.join("config.yaml"), format!( include_str!("./QDRANT_CONFIG_TEMPLATE.yml"), storage = &qdrant_dir.join("storage").to_string_lossy(), snapshots = &qdrant_dir.join("snapshots").to_string_lossy() ), ) .unwrap(); let command = relative_command_path("qdrant").expect("bad bundle"); self.stdout_file = Some(stdout_file.clone()); self.stderr_file = Some(stderr_file.clone()); self.child = Some(run_command( &command, &qdrant_dir, &stdout_file, &stderr_file, )); Ok(()) } fn on_event(&mut self, _app: &tauri::AppHandle, event: &tauri::RunEvent) { use tauri::RunEvent::{Exit, ExitRequested}; if matches!(event, Exit | ExitRequested { .. }) { let Some(mut child) = self.child.take() else { warn!("qdrant has been killed"); return; }; if let Err(err) = child.kill() { warn!(?err, "failed to kill qdrant"); } } else if let Some(ref mut child) = self.child { match child.try_wait() { Ok(Some(status)) if status.success() => { // don't fire again _ = self.child.take(); } Ok(Some(_)) => { // don't fire again _ = self.child.take(); } Ok(None) => { // all is normal, this is what we want } Err(err) => { error!(?err, "failed to monitor qdrant subprocess"); } } } } } impl Drop for QdrantSupervisor { fn drop(&mut self) { if let Some(mut child) = self.child.take() { if let Err(err) = child.kill() { warn!(?err, "failed to kill qdrant"); } } } } #[cfg(unix)] fn run_command(command: &Path, qdrant_dir: &Path, stdout: &Path, stderr: &Path) -> Child { use nix::sys::resource::{getrlimit, setrlimit, Resource}; let logs_file = File::create(stdout).unwrap(); let stderr_logs_file = File::create(stderr).unwrap(); match getrlimit(Resource::RLIMIT_NOFILE) { Ok((current_soft, current_hard)) => { info!(current_soft, current_hard, "got rlimit/nofile"); let new_soft = current_soft.max(current_hard.min(10000)); if let Err(err) = setrlimit(Resource::RLIMIT_NOFILE, new_soft, current_hard) { error!( ?err, new_soft, current_soft, current_hard, "failed to set rlimit/nofile" ); } else { info!(new_soft, current_hard, "set rlimit/nofile"); } } Err(err) => { error!(?err, "failed to get rlimit/nofile"); } } Command::new(command) .current_dir(qdrant_dir) .stdout(logs_file) .stderr(stderr_logs_file) .spawn() .expect("failed to start qdrant") } #[cfg(windows)] fn run_command(command: &Path, qdrant_dir: &Path, stdout: &Path, stderr: &Path) -> Child { use std::os::windows::process::CommandExt; let qd_logs_dir = qdrant_dir.join("logs"); create_dir_all(&qd_logs_dir).unwrap(); let logs_file = File::create(stdout).unwrap(); let stderr_logs_file = File::create(stderr).unwrap(); Command::new(command) .current_dir(qdrant_dir) // Add a CREATE_NO_WINDOW flag to prevent qdrant console popup .creation_flags(0x08000000) .stdout(logs_file) .stderr(stderr_logs_file) .spawn() .expect("failed to start qdrant") } ================================================ FILE: apps/desktop/src-tauri/tauri.conf.json ================================================ { "$schema": "../../../node_modules/@tauri-apps/cli/schema.json", "build": { "beforeBuildCommand": "npm run build", "beforeDevCommand": "npm run dev", "devPath": "http://localhost:5173", "distDir": "../dist" }, "package": { "productName": "bloop", "version": "0.6.4" }, "tauri": { "allowlist": { "fs": { "all": true }, "window": { "all": true }, "dialog": { "open": true }, "http": { "all": true }, "os": { "all": true }, "shell": { "all": true }, "path": { "all": true }, "process": { "all": true } }, "bundle": { "active": true, "category": "DeveloperTool", "copyright": "Bloop AI Limited", "deb": { "depends": [] }, "externalBin": [ "bin/qdrant" ], "icon": [ "icons/32x32.png", "icons/128x128.png", "icons/128x128@2x.png", "icons/icon.icns", "icons/icon.ico" ], "identifier": "ai.bloop.bloop", "longDescription": "Helping developers find code faster", "macOS": { "entitlements": null, "exceptionDomain": "", "frameworks": [ "frameworks/libonnxruntime.dylib" ], "providerShortName": null, "signingIdentity": null }, "resources": [ "model/*", "dylibs/*", "config/config.json" ], "shortDescription": "", "targets": "all", "windows": { "certificateThumbprint": "b955de6f8483ad3b14497e798a6eef48a137931b", "digestAlgorithm": "sha256", "timestampUrl": "http://timestamp.sectigo.com", "nsis": { "installMode": "currentUser", "template": "installer.nsi" } } }, "security": { "csp": null }, "windows": [ { "fullscreen": false, "height": 900, "resizable": true, "title": "bloop", "width": 1400, "hiddenTitle": true, "titleBarStyle": "Overlay", "minHeight": 700, "minWidth": 1000, "fileDropEnabled": false } ] } } ================================================ FILE: apps/desktop/tailwind.config.cjs ================================================ /** @type {import('tailwindcss').Config} */ const basicConfig = require("../../client/tailwind.config.cjs"); module.exports = { ...basicConfig, content: ['../../client/src/**/*.{ts,tsx,js,jsx}', './src/**/*.tsx'] } ================================================ FILE: apps/desktop/tsconfig.json ================================================ { "extends": "../../client/tsconfig.json", "include": ["src"], } ================================================ FILE: apps/desktop/tsconfig.node.json ================================================ { "compilerOptions": { "composite": true, "module": "ESNext", "moduleResolution": "Node", "allowSyntheticDefaultImports": true }, "include": ["vite.config.ts"] } ================================================ FILE: apps/desktop/vite.config.ts ================================================ import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; import EnvironmentPlugin from 'vite-plugin-environment'; // https://vitejs.dev/config/ export default defineConfig({ build: { sourcemap: true, // Source map generation must be turned on }, envDir: '../../.', plugins: [ react(), EnvironmentPlugin( { ONBOARDING: '', API_URL: '', }, { defineOn: 'import.meta.env', }, ), ], publicDir: '../../client/public', define: { __APP_SESSION__: (Math.random() * 100000).toString(), }, server: { fs: { strict: false, }, }, }); ================================================ FILE: client/.gitignore ================================================ # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* pnpm-debug.log* lerna-debug.log* node_modules dist dist-ssr *.local # Editor directories and files .vscode/* !.vscode/extensions.json .idea .DS_Store *.suo *.ntvs* *.njsproj *.sln *.sw? storybook-static coverage .env ================================================ FILE: client/.storybook/main.cjs ================================================ module.exports = { "stories": [ "../src/**/*.stories.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)" ], "addons": [ "@storybook/addon-links", "@storybook/addon-essentials", "@storybook/addon-interactions" ], framework: { name: '@storybook/react-vite', options: {}, }, "features": { "storyStoreV7": true } } ================================================ FILE: client/.storybook/preview-head.html ================================================ ================================================ FILE: client/.storybook/preview.cjs ================================================ import '../src/index.css'; export const parameters = { actions: { argTypesRegex: "^on[A-Z].*" }, controls: { matchers: { color: /(background|color)$/i, date: /Date$/, }, }, } ================================================ FILE: client/README.md ================================================ # Client ## Searching in the browser You can use bloop in the browser, without running the Tauri app. First follow [the steps](./../server/README.md) to install and run the search server. Make sure that `API_URL` is set in `.env` (e.g. `API_URL=http://localhost:7878/api`). Then, in the root directory run: ``` npm install npm run start-web ``` Open `localhost:5173` in a browser and, hey presto, you've got bloop in the browser. ================================================ FILE: client/index.html ================================================ bloop
================================================ FILE: client/jest.config.js ================================================ /** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ module.exports = { preset: 'ts-jest', testEnvironment: 'jsdom', setupFilesAfterEnv: ['/tests/setupTests.ts'], collectCoverageFrom: [ 'src/utils/{!(services),}.{js,jsx,ts,tsx}', '!**/node_modules/**', '!**/vendor/**', ], coverageThreshold: { global: { branches: 10, functions: 21, lines: 18, }, }, }; ================================================ FILE: client/package.json ================================================ { "name": "@bloop/client", "private": true, "version": "0.6.4", "scripts": { "dev": "vite", "build": "tsc && vite build", "preview": "vite preview", "lint": "eslint src --ext ts --ext tsx --fix", "type-check": "tsc", "storybook": "sb dev -p 6006", "build-storybook": "sb build", "test": "jest --collect-coverage --passWithNoTests", "chromatic": "npx chromatic --project-token=6115d726666b" } } ================================================ FILE: client/postcss.config.cjs ================================================ module.exports = { plugins: { tailwindcss: {}, autoprefixer: {}, }, } ================================================ FILE: client/src/App.tsx ================================================ import React, { memo } from 'react'; import { DndProvider } from 'react-dnd'; import { HTML5Backend } from 'react-dnd-html5-backend'; import { Toaster } from 'sonner'; import Project from './Project'; import CommandBar from './CommandBar'; import ProjectContextProvider from './context/providers/ProjectContextProvider'; import CommandBarContextProvider from './context/providers/CommandBarContextProvider'; import { UIContextProvider } from './context/providers/UIContextProvider'; import Settings from './Settings'; import ProjectSettings from './ProjectSettings'; import TabsContextProvider from './context/providers/TabsContextProvider'; import { FileHighlightsContextProvider } from './context/providers/FileHighlightsContextProvider'; import RepositoriesContextProvider from './context/providers/RepositoriesContextProvider'; const toastOptions = { unStyled: true, classNames: { toast: 'w-[20.75rem] p-4 pl-5 grid grid-cols-[1rem_1fr] items-start gap-3 rounded-md border border-bg-border bg-bg-base shadow-high', error: 'text-red', info: 'text-label-title', title: 'body-s-b', description: '!text-label-muted body-s mt-1.5', actionButton: 'col-span-full', cancelButton: 'bg-orange-400', closeButton: '!bg-bg-base !text-label-muted !border-none !left-[unset] !right-2 !top-6 !w-6 !h-6', }, }; const App = () => { return ( ); }; export default memo(App); ================================================ FILE: client/src/CloudApp.tsx ================================================ import React, { useEffect, useMemo, useState } from 'react'; import { BrowserRouter } from 'react-router-dom'; import packageJson from '../package.json'; import App from './App'; import { LocaleContext } from './context/localeContext'; import i18n from './i18n'; import './index.css'; import { getPlainFromStorage, LANGUAGE_KEY, savePlainToStorage, } from './services/storage'; import { LocaleType } from './types/general'; import { DeviceContextProvider } from './context/providers/DeviceContextProvider'; const CloudApp = () => { const [locale, setLocale] = useState( (getPlainFromStorage(LANGUAGE_KEY) as LocaleType | null) || 'en', ); const deviceContextValue = useMemo( () => ({ openFolderInExplorer: () => {}, openLink: (p: string) => window.open(p), chooseFolder: () => Promise.resolve(null), homeDir: '$HOME', listen: () => {}, os: { arch: '', type: '', platform: '', version: '', }, invokeTauriCommand: () => Promise.resolve(''), release: packageJson.version, apiUrl: import.meta.env.API_URL || '/api', isRepoManagementAllowed: true, isSelfServe: true, forceAnalytics: true, showNativeMessage: alert, relaunch: () => {}, }), [], ); useEffect(() => { i18n.changeLanguage(locale); savePlainToStorage(LANGUAGE_KEY, locale); }, [locale]); const localeContextValue = useMemo( () => ({ locale, setLocale, }), [locale], ); return ( ); }; export default CloudApp; ================================================ FILE: client/src/CommandBar/Body/Item.tsx ================================================ import React, { memo, ReactElement, useCallback, useContext, useEffect, useRef, } from 'react'; import { CommandBarItemGeneralType, CommandBarStepEnum, } from '../../types/general'; import useShortcuts from '../../hooks/useShortcuts'; import useKeyboardNavigation from '../../hooks/useKeyboardNavigation'; import { checkEventKeys } from '../../utils/keyboardUtils'; import { CommandBarContext } from '../../context/commandBarContext'; import { CheckmarkInSquareIcon } from '../../icons'; import { RECENT_COMMANDS_KEY, updateArrayInStorage, } from '../../services/storage'; import { ArrowNavigationContext } from '../../context/arrowNavigationContext'; type Props = CommandBarItemGeneralType & { index: string; isFirst?: boolean; isWithCheckmark?: boolean; customRightElement?: ReactElement; focusedItemProps?: Record; disableKeyNav?: boolean; itemKey: string; }; const CommandBarItem = ({ Icon, label, shortcut, index, id, footerBtns, isFirst, iconContainerClassName, footerHint, customRightElement, onClick, focusedItemProps, disableKeyNav, isWithCheckmark, closeOnClick, itemKey, }: Props) => { const ref = useRef(null); const shortcutKeys = useShortcuts(shortcut); const { setFocusedItem, setChosenStep, setIsVisible } = useContext( CommandBarContext.Handlers, ); const { setFocusedIndex, focusedIndex } = useContext(ArrowNavigationContext); useEffect(() => { if (focusedIndex === index) { setFocusedItem({ footerHint, footerBtns, focusedItemProps, }); ref.current?.scrollIntoView({ block: 'nearest' }); } }, [focusedIndex, index, footerBtns, footerHint, focusedItemProps]); const handleMouseMove = useCallback( (e: React.MouseEvent) => { if (e.movementX || e.movementY) { setFocusedIndex(index); } }, [index, setFocusedIndex], ); const handleClick = useCallback( (e: React.MouseEvent | KeyboardEvent) => { if (onClick) { onClick(e); if (closeOnClick) { setIsVisible(false); setChosenStep({ id: CommandBarStepEnum.INITIAL }); } } else { setChosenStep({ id: id as Exclude< CommandBarStepEnum, CommandBarStepEnum.ADD_TO_STUDIO | CommandBarStepEnum.SEARCH_DOCS >, }); } updateArrayInStorage(RECENT_COMMANDS_KEY, itemKey); }, [id, onClick, closeOnClick, itemKey], ); const handleKeyEvent = useCallback( (e: KeyboardEvent) => { const shortAction = footerBtns.find((b) => checkEventKeys(e, b.shortcut)); if ( (focusedIndex === index && shortAction && !shortAction.action) || checkEventKeys(e, shortcut) ) { e.preventDefault(); e.stopPropagation(); handleClick(e); return; } if (focusedIndex === index && shortAction?.action) { e.preventDefault(); e.stopPropagation(); shortAction.action(); } }, [focusedIndex === index, shortcut, footerBtns, handleClick], ); useKeyboardNavigation(handleKeyEvent, disableKeyNav); return ( ); }; export default memo(CommandBarItem); ================================================ FILE: client/src/CommandBar/Body/Section.tsx ================================================ import { Dispatch, memo, SetStateAction } from 'react'; import { CommandBarItemCustomType, CommandBarItemGeneralType, } from '../../types/general'; import SectionDivider from './SectionDivider'; import Item from './Item'; type Props = { title?: string; items: (CommandBarItemCustomType | CommandBarItemGeneralType)[]; disableKeyNav?: boolean; index: string; }; const CommandBarBodySection = ({ title, items, disableKeyNav, index, }: Props) => { return (
{!!title && } {items.map(({ key, ...Rest }, i) => 'Component' in Rest ? ( ) : ( ), )}
); }; export default memo(CommandBarBodySection); ================================================ FILE: client/src/CommandBar/Body/SectionDivider.tsx ================================================ import { memo } from 'react'; type Props = { text: string; }; const SectionDivider = ({ text }: Props) => { return (
{text}
); }; export default memo(SectionDivider); ================================================ FILE: client/src/CommandBar/Body/index.tsx ================================================ import { memo, useEffect, useMemo } from 'react'; import { CommandBarSectionType } from '../../types/general'; import useKeyboardNavigation from '../../hooks/useKeyboardNavigation'; import { useArrowNavigation } from '../../hooks/useArrowNavigation'; import { ArrowNavigationContext } from '../../context/arrowNavigationContext'; import { noOp } from '../../utils'; import Section from './Section'; type Props = { sections: CommandBarSectionType[]; disableKeyNav?: boolean; onFocusedIndexChange?: (i: string) => void; }; const CommandBarBody = ({ sections, disableKeyNav, onFocusedIndexChange, }: Props) => { const { focusedIndex, setFocusedIndex, handleArrowKey, navContainerRef } = useArrowNavigation(); useEffect(() => { if (sections?.[0]?.items?.[0]) { setFocusedIndex(`${sections[0].key}-${sections[0].items[0].key}`); } }, [sections]); useEffect(() => { if (onFocusedIndexChange) { onFocusedIndexChange(focusedIndex); } }, [focusedIndex]); useKeyboardNavigation(handleArrowKey, disableKeyNav); const contextValue = useMemo( () => ({ focusedIndex, setFocusedIndex, handleClose: noOp, }), [focusedIndex], ); return (
{sections.map((s) => (
))}
); }; export default memo(CommandBarBody); ================================================ FILE: client/src/CommandBar/Footer/HintButton.tsx ================================================ import { ForwardedRef, forwardRef, memo } from 'react'; import useShortcuts from '../../hooks/useShortcuts'; type Props = { label: string; shortcut?: string[]; }; const HintButton = forwardRef( ({ label, shortcut }: Props, ref: ForwardedRef) => { const shortcutKeys = useShortcuts(shortcut); return (
{label} {shortcutKeys?.map((k) => (
{k}
))}
); }, ); HintButton.displayName = 'HintButtonWithRef'; export default memo(HintButton); ================================================ FILE: client/src/CommandBar/Footer/index.tsx ================================================ import { memo, useCallback, useContext, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import { CommandBarContext } from '../../context/commandBarContext'; import Dropdown from '../../components/Dropdown'; import useKeyboardNavigation from '../../hooks/useKeyboardNavigation'; import HintButton from './HintButton'; type Props = { ActionsDropdown?: (props: any) => JSX.Element | null; actionsDropdownProps?: Record; onDropdownVisibilityChange?: (isVisible: boolean) => void; }; const CommandBarFooter = ({ ActionsDropdown, actionsDropdownProps, onDropdownVisibilityChange, }: Props) => { const { t } = useTranslation(); const { focusedItem } = useContext(CommandBarContext.FooterValues); const actionsBtn = useRef(null); const handleKeyEvent = useCallback((e: KeyboardEvent) => { if ((e.metaKey || e.ctrlKey) && e.key === 'k') { e.preventDefault(); e.stopPropagation(); actionsBtn.current?.click(); } }, []); useKeyboardNavigation(handleKeyEvent, !ActionsDropdown); return (

{focusedItem?.footerHint}

{focusedItem?.footerBtns?.map((b) => )} {!!ActionsDropdown && ( )}
); }; export default memo(CommandBarFooter); ================================================ FILE: client/src/CommandBar/Header/ChipItem.tsx ================================================ import { memo } from 'react'; type Props = { text: string }; const CommandBarChipItem = ({ text }: Props) => { return (
{text}
); }; export default memo(CommandBarChipItem); ================================================ FILE: client/src/CommandBar/Header/index.tsx ================================================ import { ChangeEvent, memo, ReactElement, useCallback, useContext, useState, } from 'react'; import { useTranslation } from 'react-i18next'; import Tooltip from '../../components/Tooltip'; import useKeyboardNavigation from '../../hooks/useKeyboardNavigation'; import { CommandBarContext } from '../../context/commandBarContext'; import { CommandBarStepEnum } from '../../types/general'; import ChipItem from './ChipItem'; type PropsWithoutInput = { noInput: true; customSubmitHandler?: never; onChange?: never; value?: never; placeholder?: never; }; type PropsWithInput = { noInput?: false; value: string; onChange: (e: ChangeEvent) => void; customSubmitHandler?: (value: string) => void; placeholder?: string; }; type GeneralProps = { handleBack?: () => void; breadcrumbs?: string[]; customRightComponent?: ReactElement; disableKeyNav?: boolean; }; type Props = GeneralProps & (PropsWithInput | PropsWithoutInput); const CommandBarHeader = ({ handleBack, breadcrumbs, customRightComponent, customSubmitHandler, onChange, value, placeholder, noInput, disableKeyNav, }: Props) => { const { t } = useTranslation(); const { isVisible } = useContext(CommandBarContext.General); const { setIsVisible, setChosenStep } = useContext( CommandBarContext.Handlers, ); const [isComposing, setIsComposing] = useState(false); const onCompositionStart = useCallback(() => { setIsComposing(true); }, []); const onCompositionEnd = useCallback(() => { // this event comes before keydown and sets state faster causing unintentional submit setTimeout(() => setIsComposing(false), 10); }, []); const handleKeyEvent = useCallback( (e: KeyboardEvent) => { if ( e.key === 'Escape' || (e.key === 'Backspace' && !value && !isComposing) ) { e.stopPropagation(); e.preventDefault(); if (handleBack) { handleBack(); } else { setChosenStep({ id: CommandBarStepEnum.INITIAL }); setIsVisible(false); } } else if (e.key === 'Enter' && customSubmitHandler && !isComposing) { e.stopPropagation(); e.preventDefault(); customSubmitHandler(value); } }, [setIsVisible, handleBack, customSubmitHandler, value, isComposing], ); useKeyboardNavigation(handleKeyEvent, !isVisible || disableKeyNav); return (
{!!handleBack && ( )} {breadcrumbs?.map((b) => )}
{customRightComponent}
{!noInput && ( )}
); }; export default memo(CommandBarHeader); ================================================ FILE: client/src/CommandBar/Tutorial/TutorialBody.tsx ================================================ import React, { memo, useCallback, useContext } from 'react'; import { Trans, useTranslation } from 'react-i18next'; import { UIContext } from '../../context/uiContext'; type Props = { stepNumber: number; title: string; description: string; hint?: string; }; const TutorialBody = ({ stepNumber, title, hint, description }: Props) => { useTranslation(); const { setOnBoardingState } = useContext(UIContext.Onboarding); const onSkip = useCallback(() => { setOnBoardingState({ isCommandBarTutorialFinished: true, isCodeNavigated: true, isCodeExplained: true, isChatOpened: true, isFileExplained: true, }); }, []); return ( e.stopPropagation()} > {stepNumber}

{title}

{description} {!!hint && ( <>
{hint} )}
); }; export default memo(TutorialBody); ================================================ FILE: client/src/CommandBar/Tutorial/TutorialTooltip.tsx ================================================ import React, { memo, PropsWithChildren, useEffect, useState } from 'react'; import Tippy from '@tippyjs/react/headless'; type Props = { content: React.ReactElement; wrapperClassName?: string; }; const TutorialTooltip = ({ children, content, wrapperClassName, }: PropsWithChildren) => { const [isVisible, setIsVisible] = useState(false); useEffect(() => { setTimeout(() => setIsVisible(true), 150); }, []); return ( content} interactive >
{children}
); }; export default memo(TutorialTooltip); ================================================ FILE: client/src/CommandBar/index.tsx ================================================ import { memo, useCallback, useContext, useMemo } from 'react'; import Modal from '../components/Modal'; import useKeyboardNavigation from '../hooks/useKeyboardNavigation'; import { CommandBarStepEnum } from '../types/general'; import { CommandBarContext } from '../context/commandBarContext'; import { useGlobalShortcuts } from '../hooks/useGlobalShortcuts'; import { checkEventKeys } from '../utils/keyboardUtils'; import { UIContext } from '../context/uiContext'; import Initial from './steps/Initial'; import PrivateRepos from './steps/PrivateRepos'; import PublicRepos from './steps/PublicRepos'; import LocalRepos from './steps/LocalRepos'; import Documentation from './steps/Documentation'; import CreateProject from './steps/CreateProject'; import ManageRepos from './steps/ManageRepos'; import AddNewRepo from './steps/AddNewRepo'; import ToggleTheme from './steps/ToggleTheme'; import SearchFiles from './steps/SeachFiles'; import AddFileToStudio from './steps/AddToStudio'; import SearchDocs from './steps/SeachDocs'; type Props = {}; const CommandBar = ({}: Props) => { const { chosenStep } = useContext(CommandBarContext.CurrentStep); const { isVisible } = useContext(CommandBarContext.General); const { setChosenStep, setIsVisible } = useContext( CommandBarContext.Handlers, ); const { onBoardingState } = useContext(UIContext.Onboarding); const globalShortcuts = useGlobalShortcuts(); const handleClose = useCallback(() => { setIsVisible(false); setChosenStep({ id: CommandBarStepEnum.INITIAL, }); }, []); const shouldShowTutorial = useMemo(() => { return !onBoardingState.isCommandBarTutorialFinished; }, [onBoardingState]); const handleKeyEvent = useCallback( (e: KeyboardEvent) => { if (checkEventKeys(e, ['cmd', 'K'])) { e.stopPropagation(); e.preventDefault(); setIsVisible(true); } Object.values(globalShortcuts).forEach((s) => { if (checkEventKeys(e, s.shortcut)) { e.stopPropagation(); e.preventDefault(); s.action(); } }); }, [isVisible, globalShortcuts], ); useKeyboardNavigation(handleKeyEvent); return ( {chosenStep.id === CommandBarStepEnum.INITIAL ? ( ) : chosenStep.id === CommandBarStepEnum.PRIVATE_REPOS ? ( ) : chosenStep.id === CommandBarStepEnum.PUBLIC_REPOS ? ( ) : chosenStep.id === CommandBarStepEnum.LOCAL_REPOS ? ( ) : chosenStep.id === CommandBarStepEnum.DOCS ? ( ) : chosenStep.id === CommandBarStepEnum.CREATE_PROJECT ? ( ) : chosenStep.id === CommandBarStepEnum.MANAGE_REPOS ? ( ) : chosenStep.id === CommandBarStepEnum.ADD_NEW_REPO ? ( ) : chosenStep.id === CommandBarStepEnum.TOGGLE_THEME ? ( ) : chosenStep.id === CommandBarStepEnum.SEARCH_FILES ? ( ) : chosenStep.id === CommandBarStepEnum.SEARCH_DOCS ? ( ) : chosenStep.id === CommandBarStepEnum.ADD_TO_STUDIO ? ( ) : null} ); }; export default memo(CommandBar); ================================================ FILE: client/src/CommandBar/steps/AddNewRepo.tsx ================================================ import React, { memo, useCallback, useContext, useMemo } from 'react'; import { Trans, useTranslation } from 'react-i18next'; import { toast } from 'sonner'; import { useGlobalShortcuts } from '../../hooks/useGlobalShortcuts'; import { CommandBarItemGeneralType, CommandBarStepEnum, } from '../../types/general'; import { GlobeIcon, HardDriveIcon, RepositoryIcon } from '../../icons'; import Header from '../Header'; import Body from '../Body'; import Footer from '../Footer'; import { CommandBarContext } from '../../context/commandBarContext'; import { DeviceContext } from '../../context/deviceContext'; import { scanLocalRepos, syncRepo } from '../../services/api'; import SpinLoaderContainer from '../../components/Loaders/SpinnerLoader'; import TutorialBody from '../Tutorial/TutorialBody'; import TutorialTooltip from '../Tutorial/TutorialTooltip'; import { tutorialSteps } from '../../consts/tutorialSteps'; type Props = { shouldShowTutorial?: boolean; }; const AddNewRepo = ({ shouldShowTutorial }: Props) => { const { t } = useTranslation(); const globalShortcuts = useGlobalShortcuts(); const { setChosenStep } = useContext(CommandBarContext.Handlers); const { homeDir, chooseFolder } = useContext(DeviceContext); const handleBack = useCallback(() => { setChosenStep({ id: CommandBarStepEnum.MANAGE_REPOS }); }, []); const handleChooseFolder = useCallback(async () => { let folder: string | string[] | null; if (chooseFolder) { try { folder = await chooseFolder({ directory: true, defaultPath: homeDir, }); } catch (err) { console.log(err); } } // @ts-ignore if (typeof folder === 'string') { scanLocalRepos(folder).then((data) => { if (data.list.length === 1) { syncRepo(data.list[0].ref); toast(t('Indexing repository'), { description: ( repoName has started indexing. You’ll receive a notification as soon as this process completes. ), icon: , unstyled: true, }); handleBack(); return; } else if (!data.list.length) { toast.error(t('Not a git repository'), { description: t('The folder you selected is not a git repository.'), icon: , unstyled: true, }); } else if (data.list.length > 1) { toast.error(t('Folder too large'), { description: t( 'The folder you selected has multiple git repositories nested inside.', ), icon: , unstyled: true, }); } }); } }, [chooseFolder, homeDir, handleBack]); const initialSections = useMemo(() => { const contextItems: CommandBarItemGeneralType[] = [ { label: t('Private repository'), Icon: RepositoryIcon, id: CommandBarStepEnum.PRIVATE_REPOS, key: 'private', shortcut: globalShortcuts.openPrivateRepos.shortcut, footerHint: '', footerBtns: [{ label: t('Next'), shortcut: ['entr'] }], }, { label: t('Public repository'), Icon: GlobeIcon, id: CommandBarStepEnum.PUBLIC_REPOS, key: 'public', shortcut: globalShortcuts.openPublicRepos.shortcut, footerHint: '', footerBtns: [{ label: t('Next'), shortcut: ['entr'] }], }, { label: t('Local repository'), Icon: HardDriveIcon, id: CommandBarStepEnum.LOCAL_REPOS, onClick: handleChooseFolder, key: 'local', shortcut: globalShortcuts.openLocalRepos.shortcut, footerHint: '', footerBtns: [{ label: t('Next'), shortcut: ['entr'] }], }, ]; return [ { items: contextItems, itemsOffset: 0, key: 'context-items', }, ]; }, [t, globalShortcuts]); return (
{shouldShowTutorial ? ( } wrapperClassName="absolute top-[5rem] left-0 right-0" >
) : null}
); }; export default memo(AddNewRepo); ================================================ FILE: client/src/CommandBar/steps/AddToStudio.tsx ================================================ import { ChangeEvent, memo, useCallback, useContext, useEffect, useMemo, useState, } from 'react'; import { useTranslation } from 'react-i18next'; import { AddDocToStudioDataType, AddFileToStudioDataType, CommandBarItemGeneralType, CommandBarSectionType, CommandBarStepEnum, TabTypesEnum, } from '../../types/general'; import { PlusSignIcon } from '../../icons'; import Header from '../Header'; import Body from '../Body'; import Footer from '../Footer'; import { CommandBarContext } from '../../context/commandBarContext'; import { ProjectContext } from '../../context/projectContext'; import { TabsContext } from '../../context/tabsContext'; import { postCodeStudio } from '../../services/api'; import TokenUsage from '../../components/TokenUsage'; import { TOKEN_LIMIT } from '../../consts/codeStudio'; type Props = (AddFileToStudioDataType | AddDocToStudioDataType) & {}; const AddToStudio = (props: Props) => { const { t } = useTranslation(); const { setChosenStep } = useContext(CommandBarContext.Handlers); const { project, refreshCurrentProjectStudios } = useContext( ProjectContext.Current, ); const { openNewTab } = useContext(TabsContext.Handlers); const [inputValue, setInputValue] = useState(''); const [sectionsToShow, setSectionsToShow] = useState( [], ); const handleChange = useCallback((e: ChangeEvent) => { setInputValue(e.target.value); }, []); const handleBack = useCallback(() => { setChosenStep({ id: CommandBarStepEnum.INITIAL }); }, []); const handleNewCodeStudio = useCallback(async () => { if (project?.id) { const newId = await postCodeStudio(project.id); refreshCurrentProjectStudios(); if ('path' in props) { openNewTab( { type: TabTypesEnum.FILE, studioId: newId, ...props, }, 'left', ); } else { openNewTab( { type: TabTypesEnum.DOC, studioId: newId, ...props, }, 'left', ); } openNewTab({ type: TabTypesEnum.STUDIO, studioId: newId }, 'right'); } }, [project?.id, props, openNewTab, refreshCurrentProjectStudios]); const handleAddToCodeStudio = useCallback( async (studioId: string) => { if (project?.id) { if ('path' in props) { openNewTab( { type: TabTypesEnum.FILE, studioId, ...props, }, 'left', ); } else { openNewTab( { type: TabTypesEnum.DOC, studioId, ...props, }, 'left', ); } openNewTab({ type: TabTypesEnum.STUDIO, studioId }, 'right'); } }, [project?.id, props, openNewTab], ); const initialSections = useMemo(() => { return [ { items: [ { label: t('New studio conversation'), Icon: PlusSignIcon, id: 'new_code_studio', key: 'new_code_studio', onClick: handleNewCodeStudio, closeOnClick: true, footerHint: '', footerBtns: [{ label: t('Create new'), shortcut: ['entr'] }], }, ], itemsOffset: 0, key: 'new-items', }, { items: (project?.studios || []).map((s) => ({ label: s.name, Icon: () => ( ), iconContainerClassName: 'bg-transparent', id: s.id, key: s.id, onClick: () => handleAddToCodeStudio(s.id), closeOnClick: true, footerHint: t('{{count}} context files used', { count: s.context.length, }), footerBtns: [{ label: t('Add to existing'), shortcut: ['entr'] }], })), label: t('Existing studio conversations'), itemsOffset: 1, key: 'studio-items', }, ]; }, [t, project?.studios, handleNewCodeStudio, openNewTab, props]); useEffect(() => { if (!inputValue) { setSectionsToShow(initialSections); return; } const newSectionsToShow: CommandBarSectionType[] = []; initialSections.forEach((s) => { const items = (s.items as CommandBarItemGeneralType[]).filter((item) => { return item.label?.toLowerCase().includes(inputValue?.toLowerCase()); }); if (items.length) { newSectionsToShow.push({ ...s, items, }); } }); setSectionsToShow(newSectionsToShow); }, [initialSections, inputValue]); const breadcrumbs = useMemo(() => { return [t(`Add ${'path' in props ? 'file' : 'doc'} to studio`)]; }, [props, t]); return (
); }; export default memo(AddToStudio); ================================================ FILE: client/src/CommandBar/steps/CreateProject.tsx ================================================ import { ChangeEvent, memo, useCallback, useContext, useEffect, useMemo, useState, } from 'react'; import { useTranslation } from 'react-i18next'; import Header from '../Header'; import Footer from '../Footer'; import { CommandBarStepEnum } from '../../types/general'; import { CommandBarContext } from '../../context/commandBarContext'; import { createProject } from '../../services/api'; import { ProjectContext } from '../../context/projectContext'; type Props = {}; const CreateProject = ({}: Props) => { const { t } = useTranslation(); const [inputValue, setInputValue] = useState(''); const { setChosenStep, setFocusedItem, setIsVisible } = useContext( CommandBarContext.Handlers, ); const { setCurrentProjectId } = useContext(ProjectContext.Current); const { refreshAllProjects } = useContext(ProjectContext.All); const handleInputChange = useCallback((e: ChangeEvent) => { setInputValue(e.target.value); }, []); useEffect(() => { setFocusedItem({ footerHint: t('Provide a short, concise title for your project'), footerBtns: [{ label: t('Create project'), shortcut: ['entr'] }], }); }, [t]); const switchProject = useCallback((id: string) => { setCurrentProjectId(id); setIsVisible(false); refreshAllProjects(); setChosenStep({ id: CommandBarStepEnum.INITIAL, }); }, []); const breadcrumbs = useMemo(() => { return [t('Create project')]; }, [t]); const handleBack = useCallback(() => { setChosenStep({ id: CommandBarStepEnum.INITIAL }); }, []); const submitHandler = useCallback( async (value: string) => { setInputValue(''); const newId = await createProject(value); switchProject(newId); }, [switchProject], ); return (
); }; export default memo(CreateProject); ================================================ FILE: client/src/CommandBar/steps/Documentation/ActionsDropdown.tsx ================================================ import { memo, useContext, useEffect, useMemo } from 'react'; import SectionItem from '../../../components/Dropdown/Section/SectionItem'; import DropdownSection from '../../../components/Dropdown/Section'; import { CommandBarContext } from '../../../context/commandBarContext'; type Props = { handleClose: () => void; }; const ActionsDropDown = ({ handleClose }: Props) => { const { focusedItem } = useContext(CommandBarContext.FooterValues); const focusedDropdownItems = useMemo(() => { return ( (focusedItem && 'focusedItemProps' in focusedItem && focusedItem.focusedItemProps?.dropdownItems) || [] ); }, [focusedItem]); const focusedDropdownItemsLength = useMemo(() => { return focusedDropdownItems.reduce( (prev: number, curr: { items: Record[]; key: string }) => prev + curr.items.length, 0, ); }, [focusedDropdownItems]); useEffect(() => { if (!focusedDropdownItemsLength) { handleClose(); } }, [focusedDropdownItemsLength]); return (
{!!focusedDropdownItems.length && focusedDropdownItems.map( (section: { items: Record[]; key: string }) => ( {section.items.map((item: Record) => ( ))} ), )}
); }; export default memo(ActionsDropDown); ================================================ FILE: client/src/CommandBar/steps/Documentation/index.tsx ================================================ import { ChangeEvent, memo, useCallback, useContext, useEffect, useMemo, useState, } from 'react'; import { useTranslation } from 'react-i18next'; import { CommandBarSectionType, CommandBarStepEnum, } from '../../../types/general'; import { CommandBarContext } from '../../../context/commandBarContext'; import { getIndexedDocs, indexDocsUrl, verifyDocsUrl, } from '../../../services/api'; import { PlusSignIcon } from '../../../icons'; import { DocShortType } from '../../../types/api'; import Header from '../../Header'; import Body from '../../Body'; import Footer from '../../Footer'; import DocItem from '../items/DocItem'; import ActionsDropdown from './ActionsDropdown'; type Props = {}; const Documentation = ({}: Props) => { const { t } = useTranslation(); const [isAddMode, setIsAddMode] = useState(false); const [hasFetched, setHasFetched] = useState(false); const [indexedDocs, setIndexedDocs] = useState([]); const [addedDoc, setAddedDoc] = useState( null, ); const [isDropdownVisible, setIsDropdownVisible] = useState(false); const { setChosenStep, setFocusedItem } = useContext( CommandBarContext.Handlers, ); const [inputValue, setInputValue] = useState(''); const handleInputChange = useCallback((e: ChangeEvent) => { setInputValue(e.target.value); }, []); const enterAddMode = useCallback(() => { setFocusedItem({ footerHint: t('Paste a link to any documentation web page'), footerBtns: [{ label: t('Sync'), shortcut: ['entr'] }], }); setIsAddMode(true); }, []); const addItem = useMemo(() => { return { itemsOffset: 0, key: 'add-docs', items: [ { label: t('Add documentation'), Icon: PlusSignIcon, footerHint: t('Add any library documentation'), footerBtns: [ { label: t('Add'), shortcut: ['entr'], }, ], key: 'add', id: 'Add', onClick: enterAddMode, }, ], }; }, [t]); const [sections, setSections] = useState([addItem]); const breadcrumbs = useMemo(() => { const arr = [t('Docs')]; if (isAddMode) { arr.push(t('Add docs')); } return arr; }, [t, isAddMode]); const handleBack = useCallback(() => { if (isAddMode) { setIsAddMode(false); } else { setChosenStep({ id: CommandBarStepEnum.INITIAL }); } }, [isAddMode]); const refetchDocs = useCallback(() => { getIndexedDocs().then((data) => { setIndexedDocs(data); setHasFetched(true); if (addedDoc && data.find((d) => d.id === addedDoc.id)) { setAddedDoc(null); } }); }, [addedDoc]); useEffect(() => { const mapped = indexedDocs.map((d) => ({ Component: DocItem, componentProps: { doc: d, isIndexed: d.index_status === 'done', refetchDocs, }, key: d.id, })); if (addedDoc) { mapped.unshift({ Component: DocItem, componentProps: { doc: { url: addedDoc.url, id: addedDoc.id, name: '', favicon: '', index_status: 'indexing', }, isIndexed: false, refetchDocs: () => { refetchDocs(); setAddedDoc(null); }, }, key: `doc-${addedDoc.id}`, }); } setSections([ addItem, { key: 'indexed-docs', label: t('Indexed documentation web pages'), items: mapped, }, ]); }, [indexedDocs, addedDoc, hasFetched, refetchDocs]); useEffect(() => { if (!isAddMode || !hasFetched) { refetchDocs(); } }, [isAddMode]); const handleAddSubmit = useCallback(async (inputValue: string) => { setFocusedItem({ footerHint: t('Verifying access...'), footerBtns: [], }); setInputValue(''); try { await verifyDocsUrl(inputValue.trim()); setIsAddMode(false); const newId = await indexDocsUrl(inputValue); setAddedDoc({ id: newId, url: inputValue }); } catch (err) { setFocusedItem({ footerHint: t( "We couldn't find any docs at that link. Try again or make sure the link is correct!", ), footerBtns: [], }); } }, []); const sectionsToShow = useMemo(() => { if (!inputValue) { return sections; } const newSections: CommandBarSectionType[] = []; sections.forEach((s) => { const newItems = s.items.filter( (i) => ('label' in i ? i.label : i.componentProps.doc.name) ?.toLowerCase() .includes(inputValue?.toLowerCase()), ); if (newItems.length) { newSections.push({ ...s, items: newItems }); } }); return newSections; }, [inputValue, sections]); return (
{isAddMode ? (
) : ( )}
); }; export default memo(Documentation); ================================================ FILE: client/src/CommandBar/steps/Initial.tsx ================================================ import React, { ChangeEvent, memo, useCallback, useContext, useMemo, useState, } from 'react'; import { Trans, useTranslation } from 'react-i18next'; import { ProjectContext } from '../../context/projectContext'; import { BugIcon, ChatBubblesIcon, CloseSignInCircleIcon, CodeStudioIcon, CogIcon, ColorSwitchIcon, DocumentsIcon, MagazineIcon, MagnifyToolIcon, PlusSignIcon, RegexIcon, RepositoryIcon, } from '../../icons'; import { CommandBarContext } from '../../context/commandBarContext'; import Header from '../Header'; import Body from '../Body'; import Footer from '../Footer'; import { CommandBarItemGeneralType, CommandBarSectionType, CommandBarStepEnum, TabTypesEnum, } from '../../types/general'; import { UIContext } from '../../context/uiContext'; import { useGlobalShortcuts } from '../../hooks/useGlobalShortcuts'; import { getJsonFromStorage, RECENT_COMMANDS_KEY, } from '../../services/storage'; import { bubbleUpRecentItems } from '../../utils/commandBarUtils'; import { TabsContext } from '../../context/tabsContext'; import TutorialBody from '../Tutorial/TutorialBody'; import TutorialTooltip from '../Tutorial/TutorialTooltip'; import { tutorialSteps } from '../../consts/tutorialSteps'; import { closeTabShortcut, newChatTabShortcut, newStudioTabShortcut, } from '../../consts/shortcuts'; import { postCodeStudio } from '../../services/api'; type Props = { shouldShowTutorial?: boolean; }; const InitialCommandBar = ({ shouldShowTutorial }: Props) => { const { t } = useTranslation(); const { setIsVisible } = useContext(CommandBarContext.Handlers); const { tabItems } = useContext(CommandBarContext.FocusedTab); const { openNewTab, closeCurrentTab } = useContext(TabsContext.Handlers); const { tab: leftTab } = useContext(TabsContext.CurrentLeft); const { tab: rightTab } = useContext(TabsContext.CurrentRight); const { projects } = useContext(ProjectContext.All); const { setCurrentProjectId, project } = useContext(ProjectContext.Current); const { theme } = useContext(UIContext.Theme); const [inputValue, setInputValue] = useState(''); const globalShortcuts = useGlobalShortcuts(); const handleInputChange = useCallback((e: ChangeEvent) => { setInputValue(e.target.value); }, []); const switchProject = useCallback((id: string) => { setCurrentProjectId(id); setIsVisible(false); }, []); const initialSections = useMemo(() => { const recentKeys = getJsonFromStorage(RECENT_COMMANDS_KEY); const contextItems: CommandBarItemGeneralType[] = [ { label: t('Add new repository'), Icon: PlusSignIcon, id: CommandBarStepEnum.ADD_NEW_REPO, key: CommandBarStepEnum.ADD_NEW_REPO, shortcut: ['cmd', 'A'], footerHint: '', footerBtns: [ { label: t('Add'), shortcut: ['entr'], }, ], }, { label: t('Manage repositories'), Icon: RepositoryIcon, id: CommandBarStepEnum.MANAGE_REPOS, key: CommandBarStepEnum.MANAGE_REPOS, shortcut: globalShortcuts.openManageRepos.shortcut, footerHint: '', footerBtns: [{ label: t('Manage'), shortcut: ['entr'] }], }, { label: t('Manage docs'), Icon: MagazineIcon, id: CommandBarStepEnum.DOCS, key: CommandBarStepEnum.DOCS, shortcut: globalShortcuts.openAddDocs.shortcut, footerHint: '', footerBtns: [{ label: t('Manage'), shortcut: ['entr'] }], }, ]; const projectItems: CommandBarItemGeneralType[] = projects .map( (p): CommandBarItemGeneralType => ({ label: p.name, Icon: MagazineIcon, id: `project-${p.id}`, key: `project-${p.id}`, onClick: () => switchProject(p.id), footerHint: project?.id === p.id ? t('Manage project') : t(`Switch to`) + ' ' + p.name, footerBtns: project?.id === p.id ? [{ label: t('Manage'), shortcut: ['entr'] }] : [ { label: t('Open'), shortcut: ['entr'], }, ], }), ) .concat({ label: t('New project'), Icon: MagazineIcon, id: CommandBarStepEnum.CREATE_PROJECT, key: CommandBarStepEnum.CREATE_PROJECT, shortcut: globalShortcuts.createNewProject.shortcut, footerHint: t('Create new project'), footerBtns: [ { label: t('Manage'), shortcut: ['entr'], }, ], }); const themeItems: CommandBarItemGeneralType[] = [ { label: t(`Theme`), Icon: ColorSwitchIcon, id: CommandBarStepEnum.TOGGLE_THEME, key: CommandBarStepEnum.TOGGLE_THEME, shortcut: globalShortcuts.toggleTheme.shortcut, footerHint: t(`Change application colour theme`), footerBtns: [ { label: t('Select'), shortcut: ['entr'], }, ], }, ]; const otherCommands: CommandBarItemGeneralType[] = [ ...(!!leftTab || !!rightTab ? [ { label: t(`Close current tab`), Icon: CloseSignInCircleIcon, id: `close-tab`, key: `close-tab`, onClick: closeCurrentTab, shortcut: closeTabShortcut, closeOnClick: true, footerHint: t(`Close currently focused tab`), footerBtns: [ { label: t('Close'), shortcut: ['entr'], }, ], }, { label: t(`Close all tabs`), Icon: CloseSignInCircleIcon, id: `close-tabs`, key: `close-tabs`, onClick: globalShortcuts.closeAllTabs.action, shortcut: globalShortcuts.closeAllTabs.shortcut, closeOnClick: true, footerHint: t(`Close all open tabs`), footerBtns: [ { label: t('Close'), shortcut: ['entr'], }, ], }, ] : []), { label: t(`Account settings`), Icon: CogIcon, id: `account-settings`, key: `account-settings`, onClick: globalShortcuts.openSettings.action, shortcut: globalShortcuts.openSettings.shortcut, footerHint: t(`Open account settings`), footerBtns: [ { label: t('Open'), shortcut: ['entr'], }, ], }, { label: t(`Documentation`), Icon: DocumentsIcon, id: `app-docs`, key: `app-docs`, onClick: globalShortcuts.openAppDocs.action, shortcut: globalShortcuts.openAppDocs.shortcut, footerHint: t(`View bloop app documentation on our website`), footerBtns: [ { label: t('Open'), shortcut: ['entr'], }, ], }, { label: t(`Report a bug`), Icon: BugIcon, id: `bug`, key: `bug`, onClick: globalShortcuts.reportABug.action, shortcut: globalShortcuts.reportABug.shortcut, footerHint: t(`Report a bug`), footerBtns: [ { label: t('Open'), shortcut: ['entr'], }, ], }, { label: t(`Code search`), Icon: RegexIcon, id: `toggle-regex`, key: `toggle-regex`, onClick: globalShortcuts.toggleRegex.action, shortcut: globalShortcuts.toggleRegex.shortcut, footerHint: t(`Search your repositories using RegExp`), footerBtns: [ { label: t('Toggle'), shortcut: ['entr'], }, ], }, { label: t(`File search`), Icon: MagnifyToolIcon, id: CommandBarStepEnum.SEARCH_FILES, key: CommandBarStepEnum.SEARCH_FILES, shortcut: globalShortcuts.openSearchFiles.shortcut, footerHint: t(`Search your files in this project`), footerBtns: [ { label: t('Search'), shortcut: ['entr'], }, ], }, ]; const commandsItems = [...themeItems, ...otherCommands]; const newTabItems = project?.repos.length ? [ { label: t('New conversation'), Icon: ChatBubblesIcon, id: 'new chat', key: 'new_chat', onClick: () => openNewTab({ type: TabTypesEnum.CHAT }), shortcut: newChatTabShortcut, closeOnClick: true, footerHint: '', footerBtns: [ { label: t('Open'), shortcut: ['entr'], }, ], }, { label: t('New studio conversation'), Icon: CodeStudioIcon, id: 'new studio', key: 'new_studio', onClick: async () => { const newId = await postCodeStudio(project.id); openNewTab({ type: TabTypesEnum.STUDIO, studioId: newId }); }, shortcut: newStudioTabShortcut, closeOnClick: true, footerHint: '', footerBtns: [ { label: t('Open'), shortcut: ['entr'], }, ], }, ] : []; const chatItems: CommandBarItemGeneralType[] = ( project?.conversations || [] ) .slice(-5) .map((c) => ({ label: c.title, id: `chat-${c.id}`, key: `chat-${c.id}`, Icon: ChatBubblesIcon, closeOnClick: true, onClick: () => { openNewTab({ type: TabTypesEnum.CHAT, title: c.title, conversationId: c.id, }); }, footerHint: '', footerBtns: [{ label: t('Open'), shortcut: ['entr'] }], })); return bubbleUpRecentItems( [ ...(newTabItems.length ? [ { items: newTabItems, key: 'new-tab-items', }, ] : []), ...(tabItems.length ? [ { items: tabItems, key: 'tab-items', }, ] : []), ...(chatItems.length ? [ { label: t('Recent conversations'), items: chatItems, key: 'chat-items', }, ] : []), { items: contextItems, label: t('Manage context'), key: 'context-items', }, { items: projectItems, label: t('Recent projects'), key: 'recent-projects', }, { items: commandsItems, label: t('Commands'), key: 'general-commands', }, ], recentKeys || [], t('Recently used'), ); }, [ t, projects, project, theme, globalShortcuts, tabItems, openNewTab, shouldShowTutorial, closeCurrentTab, !!leftTab || !!rightTab, ]); const sectionsToShow = useMemo(() => { if (!inputValue) { return initialSections; } const newSections: CommandBarSectionType[] = []; initialSections.forEach((s) => { const newItems = (s.items as CommandBarItemGeneralType[]).filter( (i) => i.label?.toLowerCase().includes(inputValue?.toLowerCase()), ); if (newItems.length) { newSections.push({ ...s, items: newItems, }); } }); return newSections; }, [inputValue, initialSections]); return (
{shouldShowTutorial ? ( } wrapperClassName="absolute top-[8.5rem] left-0 right-0" >
) : null} {!!sectionsToShow.length ? ( ) : (
No commands found...
)}
); }; export default memo(InitialCommandBar); ================================================ FILE: client/src/CommandBar/steps/LocalRepos.tsx ================================================ import { ChangeEvent, memo, useCallback, useContext, useEffect, useMemo, useState, } from 'react'; import { useTranslation } from 'react-i18next'; import { CommandBarContext } from '../../context/commandBarContext'; import { PlusSignIcon } from '../../icons'; import { CommandBarSectionType, CommandBarStepEnum, RepoProvider, } from '../../types/general'; import { getIndexedRepos, scanLocalRepos, syncRepo } from '../../services/api'; import { DeviceContext } from '../../context/deviceContext'; import Footer from '../Footer'; import Body from '../Body'; import Header from '../Header'; import RepoItem from './items/RepoItem'; type Props = {}; const LocalRepos = ({}: Props) => { const { t } = useTranslation(); const [chosenFolder, setChosenFolder] = useState(null); const [inputValue, setInputValue] = useState(''); const { homeDir, chooseFolder } = useContext(DeviceContext); const { setChosenStep, setFocusedItem } = useContext( CommandBarContext.Handlers, ); const handleInputChange = useCallback((e: ChangeEvent) => { setInputValue(e.target.value); }, []); const handleChooseFolder = useCallback(async () => { let folder: string | string[] | null; if (chooseFolder) { try { folder = await chooseFolder({ directory: true, defaultPath: homeDir, }); } catch (err) { console.log(err); } } // @ts-ignore if (typeof folder === 'string') { setChosenFolder(folder); } }, [chooseFolder, homeDir]); const enterAddMode = useCallback(async () => { setFocusedItem({ footerHint: t('Select a folder containing a git repository'), footerBtns: [{ label: t('Start indexing'), shortcut: ['entr'] }], }); await handleChooseFolder(); }, []); useEffect(() => { if (chosenFolder) { scanLocalRepos(chosenFolder).then((data) => { if (data.list.length === 1) { syncRepo(data.list[0].ref); refetchRepos(); return; } }); } }, [chosenFolder]); const addItem = useMemo(() => { return { itemsOffset: 0, key: 'add', items: [ { label: t('Add local repository'), Icon: PlusSignIcon, footerHint: t('Add a repository from your local machine'), footerBtns: [ { label: t('Select folder'), shortcut: ['entr'], }, ], key: 'add', id: 'Add', onClick: enterAddMode, }, ], }; }, []); const [sections, setSections] = useState([addItem]); const breadcrumbs = useMemo(() => { return [t('Local repositories')]; }, [t]); const handleBack = useCallback(() => { setChosenStep({ id: CommandBarStepEnum.INITIAL }); }, []); const refetchRepos = useCallback(() => { getIndexedRepos().then((data) => { const mapped = data.list .filter((r) => r.provider === RepoProvider.Local) .map((r) => ({ Component: RepoItem, componentProps: { repo: { ...r, shortName: r.name }, refetchRepos }, key: r.ref, })); if (!mapped.length) { enterAddMode(); } setSections([ addItem, { key: 'indexed-repos', label: t('Indexed local repositories'), items: mapped, }, ]); }); }, []); useEffect(() => { refetchRepos(); }, []); const sectionsToShow = useMemo(() => { if (!inputValue) { return sections; } const newSections: CommandBarSectionType[] = []; sections.forEach((s) => { const newItems = s.items.filter( (i) => ('label' in i ? i.label : i.componentProps.repo.shortName) ?.toLowerCase() .includes(inputValue?.toLowerCase()), ); if (newItems.length) { newSections.push({ ...s, items: newItems }); } }); return newSections; }, [inputValue, sections]); return (
); }; export default memo(LocalRepos); ================================================ FILE: client/src/CommandBar/steps/ManageRepos/ActionsDropdown.tsx ================================================ import { Dispatch, memo, SetStateAction, useContext, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import SectionLabel from '../../../components/Dropdown/Section/SectionLabel'; import SectionItem from '../../../components/Dropdown/Section/SectionItem'; import DropdownSection from '../../../components/Dropdown/Section'; import { CommandBarContext } from '../../../context/commandBarContext'; import { HardDriveIcon, ShapesIcon } from '../../../icons'; import GitHubIcon from '../../../icons/GitHubIcon'; import { Filter, Provider } from './index'; type Props = { setRepoType: Dispatch>; repoType: Provider; setFilter: Dispatch>; filter: Filter; handleClose: () => void; }; const ActionsDropDown = ({ setRepoType, repoType, setFilter, filter, handleClose, }: Props) => { const { t } = useTranslation(); const { focusedItem } = useContext(CommandBarContext.FooterValues); const providerIconMap = useMemo( () => ({ [Provider.All]: ShapesIcon, [Provider.GitHub]: GitHubIcon, [Provider.Local]: HardDriveIcon, }), [], ); const providerOptions = useMemo( () => [Provider.All, Provider.GitHub, Provider.Local], [], ); const filterOptions = useMemo( () => [Filter.All, Filter.Indexed, Filter.Indexing, Filter.InThisProject], [], ); const focusedDropdownItems = useMemo(() => { return ( (focusedItem && 'focusedItemProps' in focusedItem && focusedItem.focusedItemProps?.dropdownItems) || [] ); }, [focusedItem]); return (
{!!focusedDropdownItems.length && focusedDropdownItems.map( (section: { items: Record[]; key: string }) => ( {section.items.map((item: Record) => ( { item.onClick(); handleClose(); }} label={item.label} icon={item.icon} index={item.key} /> ))} ), )} {providerOptions.map((type, i) => { const Icon = providerIconMap[type]; return ( { setRepoType(type); handleClose(); }} label={t(type)} icon={} /> ); })} {filterOptions.map((type) => ( { setFilter(type); handleClose(); }} label={t(type)} /> ))}
); }; export default memo(ActionsDropDown); ================================================ FILE: client/src/CommandBar/steps/ManageRepos/index.tsx ================================================ import React, { ChangeEvent, memo, useCallback, useContext, useEffect, useMemo, useState, } from 'react'; import { Trans, useTranslation } from 'react-i18next'; import { CommandBarItemCustomType, CommandBarItemGeneralType, CommandBarItemType, CommandBarSectionType, CommandBarStepEnum, RepoProvider, SyncStatus, } from '../../../types/general'; import { PlusSignIcon } from '../../../icons'; import Header from '../../Header'; import Body from '../../Body'; import Footer from '../../Footer'; import { getIndexedRepos } from '../../../services/api'; import { mapReposBySections } from '../../../utils/mappers'; import { ProjectContext } from '../../../context/projectContext'; import { CommandBarContext } from '../../../context/commandBarContext'; import RepoItem from '../items/RepoItem'; import TutorialTooltip from '../../Tutorial/TutorialTooltip'; import TutorialBody from '../../Tutorial/TutorialBody'; import { tutorialSteps } from '../../../consts/tutorialSteps'; import { UIContext } from '../../../context/uiContext'; import ActionsDropdown from './ActionsDropdown'; type Props = { shouldShowTutorial?: boolean; }; export enum Filter { All = 'All', Indexed = 'Indexed', Indexing = 'Indexing', InThisProject = 'In this project', } export enum Provider { All = 'All', GitHub = 'GitHub', Local = 'Local', } const ManageRepos = ({ shouldShowTutorial }: Props) => { const { t } = useTranslation(); const { project } = useContext(ProjectContext.Current); const { setChosenStep } = useContext(CommandBarContext.Handlers); const { setOnBoardingState } = useContext(UIContext.Onboarding); const [sections, setSections] = useState([]); const [sectionsToShow, setSectionsToShow] = useState( [], ); const [isDropdownVisible, setIsDropdownVisible] = useState(false); const [filter, setFilter] = useState(Filter.All); const [repoType, setRepoType] = useState(Provider.All); const [inputValue, setInputValue] = useState(''); const [tutorialStep, setTutorialStep] = useState(0); const [selectedRepo, setSelectedRepo] = useState(''); const handleInputChange = useCallback((e: ChangeEvent) => { setInputValue(e.target.value); }, []); const addItem = useMemo(() => { return { itemsOffset: 0, key: 'add', items: [ { label: t('Add new repository'), Icon: PlusSignIcon, id: CommandBarStepEnum.ADD_NEW_REPO, shortcut: ['cmd', 'A'], footerHint: '', footerBtns: [ { label: t('Add'), shortcut: ['entr'], }, ], key: 'add', }, ], }; }, []); const refetchRepos = useCallback(() => { getIndexedRepos().then((data) => { const mapped = mapReposBySections(data.list).map((o) => ({ items: o.items.map((r) => ({ Component: RepoItem, componentProps: { repo: r, refetchRepos, onSync: () => { setSelectedRepo(r.shortName); setTutorialStep(3); }, onDone: () => { setTutorialStep(4); }, onAddToProject: () => { setOnBoardingState((prev) => ({ ...prev, isCommandBarTutorialFinished: true, })); setTutorialStep(5); }, }, key: r.ref, })), itemsOffset: o.offset + 1, label: o.org === 'Local' ? t('Local') : o.org, key: o.org, })); setSections([addItem, ...mapped]); }); }, []); useEffect(() => { if (filter === Filter.All && !inputValue && repoType === Provider.All) { setSectionsToShow(sections); return; } const newSectionsToShow: CommandBarSectionType[] = []; const filterByStatus = (item: CommandBarItemType) => { if ('componentProps' in item) { switch (filter) { case Filter.Indexing: return [ SyncStatus.Syncing, SyncStatus.Indexing, SyncStatus.Queued, ].includes(item.componentProps.repo.sync_status); case Filter.Indexed: return item.componentProps.repo.sync_status === SyncStatus.Done; case Filter.InThisProject: return !!project?.repos.find( (r) => r.repo.ref === item.componentProps.repo.ref, ); default: return true; } } return false; }; const filterByProvider = (item: CommandBarItemType) => { if ('componentProps' in item) { switch (repoType) { case Provider.GitHub: return item.componentProps.repo.provider === RepoProvider.GitHub; case Provider.Local: return item.componentProps.repo.provider === RepoProvider.Local; default: return true; } } return false; }; const filterByName = ( item: CommandBarItemGeneralType | CommandBarItemCustomType, ) => { return 'componentProps' in item ? item.componentProps.repo.shortName ?.toLowerCase() .includes(inputValue?.toLowerCase()) : item.label?.toLowerCase().includes(inputValue?.toLowerCase()); }; sections.forEach((s) => { const items = s.items.filter( (item) => filterByProvider(item) && filterByStatus(item) && filterByName(item), ); if (items.length) { newSectionsToShow.push({ ...s, items, }); } }); setSectionsToShow(newSectionsToShow); }, [sections, filter, inputValue, project?.repos, repoType]); useEffect(() => { refetchRepos(); }, []); useEffect(() => { // if user started with non-private repo if (shouldShowTutorial && tutorialStep === 0 && sections.length > 1) { const firstRepo = (sections[1].items[0] as CommandBarItemCustomType) .componentProps.repo; setTutorialStep(firstRepo.isSyncing ? 3 : 4); setSelectedRepo(firstRepo.shortName); } }, [sections, tutorialStep, shouldShowTutorial]); const handleBack = useCallback(() => { setChosenStep({ id: CommandBarStepEnum.INITIAL }); }, []); const actionsDropdownProps = useMemo(() => { return { repoType, setRepoType, filter, setFilter, }; }, [repoType, filter]); return (
{shouldShowTutorial && tutorialStep < 5 ? ( 0 ? t(tutorialSteps[tutorialStep].hint[0]) : t(tutorialSteps[tutorialStep].hint[0]) + t(tutorialSteps[0].hint[1]) + '.' } /> } wrapperClassName="absolute top-[7.5rem] left-0 right-0" >
) : null} {sectionsToShow.length ? ( ) : (
No repositories found...
)}
); }; export default memo(ManageRepos); ================================================ FILE: client/src/CommandBar/steps/PrivateRepos/ActionsDropdown.tsx ================================================ import { memo, useContext, useMemo } from 'react'; import SectionItem from '../../../components/Dropdown/Section/SectionItem'; import DropdownSection from '../../../components/Dropdown/Section'; import { CommandBarContext } from '../../../context/commandBarContext'; type Props = {}; const ActionsDropDown = ({}: Props) => { const { focusedItem } = useContext(CommandBarContext.FooterValues); const focusedDropdownItems = useMemo(() => { return ( (focusedItem && 'focusedItemProps' in focusedItem && focusedItem.focusedItemProps?.dropdownItems) || [] ); }, [focusedItem]); return (
{!!focusedDropdownItems.length && focusedDropdownItems.map( (section: { items: Record[]; key: string }) => ( {section.items.map((item: Record) => ( ))} ), )}
); }; export default memo(ActionsDropDown); ================================================ FILE: client/src/CommandBar/steps/PrivateRepos/index.tsx ================================================ import React, { ChangeEvent, memo, useCallback, useContext, useEffect, useMemo, useState, } from 'react'; import { Trans, useTranslation } from 'react-i18next'; import { CommandBarContext } from '../../../context/commandBarContext'; import { CommandBarItemCustomType, CommandBarSectionType, CommandBarStepEnum, RepoProvider, } from '../../../types/general'; import { getRepos } from '../../../services/api'; import { mapReposBySections } from '../../../utils/mappers'; import Header from '../../Header'; import Body from '../../Body'; import Footer from '../../Footer'; import RepoItem from '../items/RepoItem'; import TutorialBody from '../../Tutorial/TutorialBody'; import TutorialTooltip from '../../Tutorial/TutorialTooltip'; import { tutorialSteps } from '../../../consts/tutorialSteps'; import { UIContext } from '../../../context/uiContext'; import ActionsDropdown from './ActionsDropdown'; type Props = { shouldShowTutorial?: boolean; }; const PrivateReposStep = ({ shouldShowTutorial }: Props) => { const { t } = useTranslation(); const [sections, setSections] = useState([]); const [sectionsToShow, setSectionsToShow] = useState( [], ); const { setChosenStep } = useContext(CommandBarContext.Handlers); const { setOnBoardingState } = useContext(UIContext.Onboarding); const [inputValue, setInputValue] = useState(''); const [tutorialStep, setTutorialStep] = useState(2); const [isDropdownVisible, setIsDropdownVisible] = useState(false); const [selectedRepo, setSelectedRepo] = useState(''); const handleInputChange = useCallback((e: ChangeEvent) => { setInputValue(e.target.value); }, []); const refetchRepos = useCallback(async () => { const data = await getRepos(); const mapped = mapReposBySections( data.list.filter((r) => r.provider !== RepoProvider.Local), ).map((o) => ({ items: o.items.map((r) => ({ Component: RepoItem, componentProps: { repo: r, refetchRepos, onSync: () => { setSelectedRepo(r.shortName); setTutorialStep(3); }, onDone: () => { setTutorialStep(4); }, onAddToProject: () => { setOnBoardingState((prev) => ({ ...prev, isCommandBarTutorialFinished: true, })); setTutorialStep(5); }, }, key: r.ref, })), itemsOffset: o.offset, label: o.org, key: o.org, })); setSections(mapped); }, []); useEffect(() => { if (!inputValue) { setSectionsToShow(sections); return; } const newSectionsToShow: CommandBarSectionType[] = []; sections.forEach((s) => { const items = (s.items as CommandBarItemCustomType[]).filter((item) => { return item.componentProps.repo.shortName ?.toLowerCase() .includes(inputValue?.toLowerCase()); }); if (items.length) { newSectionsToShow.push({ ...s, items, }); } }); setSectionsToShow(newSectionsToShow); }, [sections, inputValue]); useEffect(() => { refetchRepos(); }, []); const breadcrumbs = useMemo(() => { return [t('Add private repository')]; }, [t]); const handleBack = useCallback(() => { setChosenStep({ id: CommandBarStepEnum.MANAGE_REPOS }); }, []); return (
{shouldShowTutorial && tutorialStep < 5 ? ( } wrapperClassName="absolute top-[8.5rem] left-0 right-0" >
) : null} {sectionsToShow.length ? ( ) : (
No repositories found...
)}
); }; export default memo(PrivateReposStep); ================================================ FILE: client/src/CommandBar/steps/PublicRepos.tsx ================================================ import { ChangeEvent, memo, useCallback, useContext, useEffect, useMemo, useState, } from 'react'; import { useTranslation } from 'react-i18next'; import axios from 'axios'; import { CommandBarStepEnum } from '../../types/general'; import { CommandBarContext } from '../../context/commandBarContext'; import { syncRepo } from '../../services/api'; import Header from '../Header'; import Footer from '../Footer'; type Props = {}; const PublicRepos = ({}: Props) => { const { t } = useTranslation(); const { setChosenStep, setFocusedItem } = useContext( CommandBarContext.Handlers, ); const [inputValue, setInputValue] = useState(''); const handleInputChange = useCallback((e: ChangeEvent) => { setInputValue(e.target.value); }, []); useEffect(() => { setFocusedItem({ footerHint: t('Paste a link to any public repository hosted on GitHub'), footerBtns: [{ label: t('Start indexing'), shortcut: ['entr'] }], }); }, []); const breadcrumbs = useMemo(() => { return [t('Add public repository')]; }, [t]); const handleBack = useCallback(() => { setChosenStep({ id: CommandBarStepEnum.MANAGE_REPOS }); }, []); const handleAddSubmit = useCallback((inputValue: string) => { setFocusedItem({ footerHint: t('Verifying access...'), footerBtns: [], }); let cleanRef = inputValue .replace('https://', '') .replace('github.com/', '') .replace(/\.git$/, '') .replace(/"$/, '') .replace(/^"/, '') .replace(/\/$/, ''); if (inputValue.startsWith('git@github.com:')) { cleanRef = inputValue.slice(15).replace(/\.git$/, ''); } axios(`https://api.github.com/repos/${cleanRef}`) .then((resp) => { if (resp?.data?.visibility === 'public') { syncRepo(`github.com/${cleanRef}`); handleBack(); } else { setFocusedItem({ footerHint: t( "This is not a public repository / We couldn't find this repository", ), footerBtns: [], }); } }) .catch((err) => { console.log(err); setFocusedItem({ footerHint: t( "This is not a public repository / We couldn't find this repository", ), footerBtns: [], }); }); }, []); return (
); }; export default memo(PublicRepos); ================================================ FILE: client/src/CommandBar/steps/SeachDocs.tsx ================================================ import { ChangeEvent, memo, useCallback, useContext, useDeferredValue, useEffect, useMemo, useState, } from 'react'; import { useTranslation } from 'react-i18next'; import Header from '../Header'; import { CommandBarStepEnum, TabTypesEnum } from '../../types/general'; import { CommandBarContext } from '../../context/commandBarContext'; import { getDocById, searchDocSections } from '../../services/api'; import { DocSectionType, DocShortType } from '../../types/api'; import Body from '../Body'; import { TabsContext } from '../../context/tabsContext'; import Footer from '../Footer'; import { UIContext } from '../../context/uiContext'; import { MagazineIcon } from '../../icons'; import RenderedSection from '../../Project/CurrentTabContent/DocTab/RenderedSection'; type Props = { studioId?: string; docId: string; }; const SearchDocs = ({ studioId, docId }: Props) => { const { t } = useTranslation(); const [inputValue, setInputValue] = useState(''); const { setChosenStep, setIsVisible } = useContext( CommandBarContext.Handlers, ); const { openNewTab } = useContext(TabsContext.Handlers); const { setIsLeftSidebarFocused } = useContext(UIContext.Focus); const [docSections, setDocSections] = useState([]); const [fullDoc, setFullDoc] = useState(null); const [focusedIndex, setFocusedIndex] = useState(''); const searchValue = useDeferredValue(inputValue); const handleInputChange = useCallback((e: ChangeEvent) => { setInputValue(e.target.value); }, []); useEffect(() => { searchDocSections(docId, searchValue || ',').then(setDocSections); }, [searchValue, docId]); useEffect(() => { getDocById(docId).then(setFullDoc); }, [docId]); const breadcrumbs = useMemo(() => { return studioId ? [t('Add doc to studio')] : [t('Search docs')]; }, [t, studioId]); const handleBack = useCallback(() => { setChosenStep({ id: CommandBarStepEnum.INITIAL }); }, []); const constructSectionTitle = useCallback( (docTitle: string, ancestry: string[], sectionHeader: string) => { let title = docTitle + ' '; if (ancestry.length) { title += '/'; } ancestry.forEach((h, i) => { title += h.replace(/#/g, ''); if (i !== ancestry.length - 1) { title += '/'; } }); if (sectionHeader) { title += '/'; title += sectionHeader.replace(/#/g, ''); } return title; }, [], ); const favIconComponent = useMemo(() => { if (fullDoc?.favicon) { // eslint-disable-next-line react/display-name return (props: { className?: string; sizeClassName?: string }) => ( {fullDoc?.name} ); } return MagazineIcon; }, [fullDoc?.favicon]); const sections = useMemo(() => { return [ { key: 'docs', items: docSections.map( ({ doc_title, header, ancestry, point_id, doc_id, relative_url, }) => ({ key: `${point_id}`, id: `doc-section-${point_id}`, onClick: () => { openNewTab({ type: TabTypesEnum.DOC, docId: doc_id, relativeUrl: relative_url, title: doc_title, studioId, favicon: fullDoc?.favicon, initialSections: [point_id], }); setIsLeftSidebarFocused(false); setIsVisible(false); setChosenStep({ id: CommandBarStepEnum.INITIAL }); }, label: constructSectionTitle(doc_title, ancestry, header), footerHint: ``, footerBtns: [ { label: studioId ? t('Add doc') : t('Open'), shortcut: ['entr'], }, ], Icon: favIconComponent, }), ), itemsOffset: 0, }, ]; }, [docSections, studioId]); const focusedDoc = useMemo(() => { return docSections.find((ds) => `docs-${ds.point_id}` === focusedIndex); }, [docSections, focusedIndex]); return (
{docSections.length ? (
{!!focusedDoc && ( )}
) : (
)} {!!docSections.length &&
}
); }; export default memo(SearchDocs); ================================================ FILE: client/src/CommandBar/steps/SeachFiles.tsx ================================================ import React, { ChangeEvent, memo, useCallback, useContext, useDeferredValue, useEffect, useMemo, useState, } from 'react'; import { Trans, useTranslation } from 'react-i18next'; import Header from '../Header'; import { CommandBarStepEnum, TabTypesEnum } from '../../types/general'; import { CommandBarContext } from '../../context/commandBarContext'; import { getAutocomplete, getCodeStudio, patchCodeStudio, } from '../../services/api'; import { FileResItem } from '../../types/api'; import Body from '../Body'; import FileIcon from '../../components/FileIcon'; import { TabsContext } from '../../context/tabsContext'; import { ProjectContext } from '../../context/projectContext'; import Footer from '../Footer'; import { splitPath } from '../../utils'; import { getJsonFromStorage, RECENT_FILES_KEY } from '../../services/storage'; import { UIContext } from '../../context/uiContext'; import { filterOutDuplicates } from '../../utils/mappers'; type Props = { studioId?: string; }; const SearchFiles = ({ studioId }: Props) => { const { t } = useTranslation(); const [inputValue, setInputValue] = useState(''); const { setChosenStep, setIsVisible } = useContext( CommandBarContext.Handlers, ); const { project, refreshCurrentProjectRepos } = useContext( ProjectContext.Current, ); const { openNewTab } = useContext(TabsContext.Handlers); const { setIsLeftSidebarFocused } = useContext(UIContext.Focus); const [files, setFiles] = useState< { path: string; repo: string; branch: string | null }[] >([]); const searchValue = useDeferredValue(inputValue); const handleInputChange = useCallback((e: ChangeEvent) => { setInputValue(e.target.value); }, []); useEffect(() => { if (!searchValue) { const recentFiles = getJsonFromStorage(RECENT_FILES_KEY); const newFiles: { path: string; repo: string; branch: string | null }[] = []; recentFiles?.forEach((f) => { const [repo, path, branch] = f.split(':'); if (project?.repos.find((r) => r.repo.ref === repo)) { newFiles.push({ repo, path, branch: branch || null }); } }); if (newFiles.length > 1) { setFiles(newFiles.reverse()); return; } } if (project?.id) { getAutocomplete( project.id, `path:${searchValue}&content=false&file=true&page_size=20`, ).then((respPath) => { const fileResults = respPath.data .filter( (d): d is FileResItem => d.kind === 'file_result' && !d.data.is_dir, ) .map((d) => ({ path: d.data.relative_path.text, repo: d.data.repo_ref, branch: d.data.branches || null, })); setFiles(fileResults); }); } }, [searchValue, project?.id]); const breadcrumbs = useMemo(() => { return studioId ? [t('Add file to studio')] : [t('Search files')]; }, [t, studioId]); const handleBack = useCallback(() => { setChosenStep({ id: CommandBarStepEnum.INITIAL }); }, []); const sections = useMemo(() => { return [ { key: 'files', items: filterOutDuplicates( files.map((f) => ({ ...f, key: `${f.path}-${f.repo}-${f.branch}` })), 'key', ).map(({ path, repo, branch, key }) => { const addMultipleFilesToStudio = async () => { if (project?.id && studioId) { const studio = await getCodeStudio(project.id, studioId); const patchedFile = studio?.context.find( (f) => f.path === path && f.repo === repo && f.branch === branch, ); if (!patchedFile) { await patchCodeStudio(project.id, studioId, { context: [ ...(studio?.context || []), { path, branch: branch, repo, hidden: false, ranges: [], }, ], }); refreshCurrentProjectRepos(); openNewTab({ type: TabTypesEnum.FILE, path, repoRef: repo, branch, studioId, isFileInContext: true, initialRanges: [], }); } } }; return { key, id: key, onClick: async (e: React.MouseEvent | KeyboardEvent) => { if (studioId && e.shiftKey && project?.id) { await addMultipleFilesToStudio(); } else { openNewTab({ type: TabTypesEnum.FILE, path, repoRef: repo, branch, studioId, }); setIsLeftSidebarFocused(false); setIsVisible(false); setChosenStep({ id: CommandBarStepEnum.INITIAL }); } }, label: path, footerHint: `${splitPath(repo) .slice(repo.startsWith('local//') ? -1 : -2) .join('/')} ${ branch ? `/ ${splitPath(branch).pop()} ` : '' }/ ${path}`, footerBtns: [ ...(studioId ? [ { label: t('Add multiple files'), shortcut: ['shift', 'entr'], action: addMultipleFilesToStudio, }, ] : []), { label: studioId ? t('Add file') : t('Open'), shortcut: ['entr'], }, ], Icon: (props: { sizeClassName?: string }) => ( ), }; }), itemsOffset: 0, }, ]; }, [files, studioId, project?.id]); return (
{files.length ? ( ) : (
No files found...
)}
); }; export default memo(SearchFiles); ================================================ FILE: client/src/CommandBar/steps/ToggleTheme.tsx ================================================ import { memo, useCallback, useContext, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { CommandBarItemGeneralType, CommandBarStepEnum, } from '../../types/general'; import { MacintoshIcon, ThemeBlackIcon, ThemeDarkIcon, ThemeLightIcon, } from '../../icons'; import Header from '../Header'; import Body from '../Body'; import Footer from '../Footer'; import { CommandBarContext } from '../../context/commandBarContext'; import { Theme } from '../../types'; import { UIContext } from '../../context/uiContext'; type Props = {}; const ToggleTheme = ({}: Props) => { const { t } = useTranslation(); const { setChosenStep } = useContext(CommandBarContext.Handlers); const { setTheme } = useContext(UIContext.Theme); const handleBack = useCallback(() => { setChosenStep({ id: CommandBarStepEnum.INITIAL }); }, []); const initialSections = useMemo(() => { const themeOptions = ['light', 'dark', 'black', 'system'] as Theme[]; const themeMap = { light: ThemeLightIcon, dark: ThemeDarkIcon, black: ThemeBlackIcon, system: MacintoshIcon, }; const themeItems: CommandBarItemGeneralType[] = themeOptions.map((th) => ({ label: t(`Use ${th} theme`), Icon: themeMap[th], id: `${th}-theme`, key: `${th}-theme`, onClick: () => setTheme(th), footerHint: t(`Use ${th} theme`), footerBtns: [ { label: t('Toggle'), shortcut: ['entr'], }, ], })); return [ { items: themeItems, itemsOffset: 0, key: 'theme-commands', }, ]; }, [t]); return (
); }; export default memo(ToggleTheme); ================================================ FILE: client/src/CommandBar/steps/items/DocItem.tsx ================================================ import { memo, useCallback, useContext, useEffect, useMemo, useRef, useState, } from 'react'; import { Trans, useTranslation } from 'react-i18next'; import { CloseSignInCircleIcon, LinkChainIcon, MagazineIcon, PlusSignIcon, TrashCanIcon, } from '../../../icons'; import { DeviceContext } from '../../../context/deviceContext'; import { DocShortType } from '../../../types/api'; import { addDocToProject, API_BASE_URL, cancelDocIndexing, deleteDocProvider, getDocById, removeDocFromProject, resyncDoc, } from '../../../services/api'; import Item from '../../Body/Item'; import SpinLoaderContainer from '../../../components/Loaders/SpinnerLoader'; import Button from '../../../components/Button'; import { ProjectContext } from '../../../context/projectContext'; type Props = { doc: DocShortType; index: string; isFirst: boolean; isIndexed: boolean; disableKeyNav?: boolean; refetchDocs: () => {}; }; const DocItem = ({ doc, isFirst, index, isIndexed, disableKeyNav, refetchDocs, }: Props) => { const { t } = useTranslation(); const [docToShow, setDocToShow] = useState(doc); const [isIndexingFinished, setIsIndexingFinished] = useState( !!doc.id && doc.index_status === 'done', ); const { openLink } = useContext(DeviceContext); const { project, refreshCurrentProjectDocs } = useContext( ProjectContext.Current, ); const eventSourceRef = useRef(null); const refetchDoc = useCallback(() => { getDocById(doc.id).then((data) => { setDocToShow(data); }); }, [doc.id]); const startEventSource = useCallback(() => { setIsIndexingFinished(false); eventSourceRef.current?.close(); eventSourceRef.current = new EventSource( `${API_BASE_URL}/docs/${doc.id}/status`, ); eventSourceRef.current.onmessage = (ev) => { try { const data = JSON.parse(ev.data); console.log(data); if (data.Ok.Done) { eventSourceRef.current?.close(); eventSourceRef.current = null; setIsIndexingFinished(true); refetchDoc(); return; } } catch (err) { console.log(err); eventSourceRef.current?.close(); eventSourceRef.current = null; } }; eventSourceRef.current.onerror = (err) => { console.log(err); eventSourceRef.current?.close(); eventSourceRef.current = null; }; }, [doc.id]); useEffect(() => { return () => { eventSourceRef.current?.close(); eventSourceRef.current = null; }; }, []); useEffect(() => { if (!isIndexed && !eventSourceRef.current && !isIndexingFinished) { startEventSource(); } }, [isIndexed]); const handleCancelSync = useCallback(async () => { await cancelDocIndexing(doc.id); eventSourceRef.current?.close(); eventSourceRef.current = null; setIsIndexingFinished(true); }, [doc.id]); const isIndexing = useMemo(() => { return !isIndexed && !isIndexingFinished; }, [isIndexed, isIndexingFinished]); const handleRemove = useCallback(() => { if (docToShow.id) { deleteDocProvider(docToShow.id).then(() => { refetchDocs(); }); } else { refetchDocs(); } }, [docToShow.id]); const handleResync = useCallback(async () => { await resyncDoc(doc.id); startEventSource(); }, [doc.id]); const favIconComponent = useMemo(() => { // eslint-disable-next-line react/display-name return (props: { className?: string; sizeClassName?: string }) => ( {docToShow.name} ); }, [docToShow.favicon]); const handleAddToProject = useCallback(() => { if (project?.id) { return addDocToProject(project.id, doc.id).finally(() => { refreshCurrentProjectDocs(); }); } }, [doc.id, project?.id, refreshCurrentProjectDocs]); const handleRemoveFromProject = useCallback(() => { if (project?.id) { return removeDocFromProject(project.id, doc.id).finally(() => { refreshCurrentProjectDocs(); }); } }, [doc.id, project?.id, refreshCurrentProjectDocs]); const isInProject = useMemo(() => { return project?.docs.find((d) => d.id === doc.id); }, [project?.docs, doc.id]); const focusedItemProps = useMemo(() => { const dropdownItems1 = []; if (isIndexing) { dropdownItems1.push({ onClick: handleCancelSync, label: t('Stop indexing'), icon: ( ), key: 'stop_indexing', }); } if (isIndexingFinished) { dropdownItems1.push( isInProject ? { onClick: handleRemoveFromProject, label: t('Remove from project'), icon: ( ), key: 'remove_from_project', } : { onClick: handleAddToProject, label: t('Add to project'), icon: ( ), key: 'add_to_project', }, ); dropdownItems1.push({ onClick: handleResync, label: t('Re-sync'), shortcut: ['cmd', 'R'], key: 'resync', }); dropdownItems1.push({ onClick: handleRemove, label: t('Remove'), shortcut: ['cmd', 'D'], key: 'remove', }); } const dropdownItems = []; if (dropdownItems1.length) { dropdownItems.push({ items: dropdownItems1, key: '1', itemsOffset: 0 }); } return { dropdownItems, }; }, [ t, isInProject, handleAddToProject, handleRemoveFromProject, handleCancelSync, isIndexing, handleAddToProject, handleResync, handleRemove, doc, ]); return ( {t(`Indexed`)} ) } onClick={ isIndexing ? handleCancelSync : isInProject ? handleRemoveFromProject : handleAddToProject } iconContainerClassName={ isIndexingFinished ? 'bg-bg-contrast text-label-contrast' : 'bg-bg-border' } footerBtns={ isIndexingFinished ? [ { label: isInProject ? t('Remove from project') : t('Add to project'), shortcut: ['entr'], }, ] : [ { label: t('Stop indexing'), shortcut: ['entr'], }, ] } customRightElement={ isIndexing ? (

{t('Indexing...')}

) : undefined } focusedItemProps={focusedItemProps} disableKeyNav={disableKeyNav} /> ); }; export default memo(DocItem); ================================================ FILE: client/src/CommandBar/steps/items/RepoItem.tsx ================================================ import React, { memo, useCallback, useContext, useEffect, useMemo, } from 'react'; import { useTranslation } from 'react-i18next'; import { CommandBarStepEnum, RepoIndexingStatusType, RepoProvider, RepoUi, SyncStatus, } from '../../../types/general'; import { addRepoToProject, cancelSync, deleteRepo, removeRepoFromProject, syncRepo, } from '../../../services/api'; import { CloseSignInCircleIcon, LinkChainIcon, PlusSignIcon, RepositoryIcon, TrashCanIcon, } from '../../../icons'; import { DeviceContext } from '../../../context/deviceContext'; import { getFileManagerName } from '../../../utils'; import Item from '../../Body/Item'; import SpinLoaderContainer from '../../../components/Loaders/SpinnerLoader'; import { ProjectContext } from '../../../context/projectContext'; import { repoStatusMap } from '../../../consts/general'; import { RepositoriesContext } from '../../../context/repositoriesContext'; type Props = { repo: RepoUi; index: string; isFirst: boolean; refetchRepos: () => void; disableKeyNav?: boolean; indexingStatus?: RepoIndexingStatusType; tutorialPopup?: React.ReactElement; onSync?: () => void; onDone?: () => void; onAddToProject?: () => void; }; const RepoItem = ({ repo, isFirst, index, refetchRepos, disableKeyNav, indexingStatus, onSync, onDone, onAddToProject, }: Props) => { const { t } = useTranslation(); const { project, refreshCurrentProjectRepos } = useContext( ProjectContext.Current, ); const { openFolderInExplorer, os, openLink } = useContext(DeviceContext); const onRepoSync = useCallback( async (e?: MouseEvent | KeyboardEvent | React.MouseEvent) => { e?.preventDefault(); e?.stopPropagation(); await syncRepo(repo.ref); onSync?.(); }, [repo.ref, onSync], ); const status = useMemo(() => { return indexingStatus?.status || repo.sync_status; }, [indexingStatus]); useEffect(() => { if (status === SyncStatus.Done) { onDone?.(); } }, [status]); const handleAddToProject = useCallback(() => { if (project?.id) { onAddToProject?.(); return addRepoToProject( project.id, repo.ref, repo.branch_filter?.select?.[0], ).finally(() => { refreshCurrentProjectRepos(); }); } }, [repo]); const handleOpenInFinder = useCallback(() => { openFolderInExplorer(repo.ref.slice(6)); }, [openFolderInExplorer, repo.ref]); const handleOpenInGitHub = useCallback(() => { openLink('https://' + repo.ref); }, [openLink, repo.ref]); const handleRemoveFromProject = useCallback(() => { if (project?.id) { return removeRepoFromProject(project.id, repo.ref).finally(() => { refreshCurrentProjectRepos(); }); } }, [repo]); const handleCancelSync = useCallback(() => { cancelSync(repo.ref); }, [repo.ref]); const isIndexing = useMemo(() => { return [ SyncStatus.Indexing, SyncStatus.Syncing, SyncStatus.Queued, ].includes(status); }, [status]); const onRepoRemove = useCallback(async () => { await deleteRepo(repo.ref); refetchRepos(); }, [repo.ref]); const isInProject = useMemo(() => { return project?.repos.find((r) => r.repo.ref === repo.ref); }, [project, repo.ref]); const focusedItemProps = useMemo(() => { const dropdownItems1 = []; if (isIndexing) { dropdownItems1.push({ onClick: handleCancelSync, label: t('Stop indexing'), icon: ( ), key: 'stop_indexing', }); } if (status === SyncStatus.Done || status === SyncStatus.Cancelled) { dropdownItems1.push( isInProject ? { onClick: handleRemoveFromProject, label: t('Remove from project'), icon: ( ), key: 'remove_from_project', } : { onClick: handleAddToProject, label: t('Add to project'), icon: ( ), key: 'add_to_project', }, ); dropdownItems1.push({ onClick: onRepoSync, label: t('Re-sync'), shortcut: ['cmd', 'R'], key: 'resync', }); dropdownItems1.push({ onClick: onRepoRemove, label: t('Remove'), shortcut: ['cmd', 'D'], key: 'remove', }); } const dropdownItems2 = [ repo.provider === RepoProvider.Local ? { onClick: handleOpenInFinder, label: t(`Open in {{viewer}}`, { viewer: getFileManagerName(os.type), }), key: 'openInFinder', } : { onClick: handleOpenInGitHub, label: t(`Open in GitHub`), icon: ( ), key: 'openInGitHub', }, ]; const dropdownItems = []; if (dropdownItems1.length) { dropdownItems.push({ items: dropdownItems1, key: '1', itemsOffset: 0 }); } if (dropdownItems2.length) { dropdownItems.push({ items: dropdownItems2, key: '2', itemsOffset: dropdownItems1.length, }); } return { dropdownItems, }; }, [ t, isInProject, handleAddToProject, handleRemoveFromProject, handleCancelSync, status, repo.provider, isIndexing, handleOpenInFinder, handleOpenInGitHub, onRepoRemove, ]); return ( {t(repoStatusMap[status].text)} {indexingStatus?.percentage !== null && indexingStatus?.percentage !== undefined && ` · ${indexingStatus?.percentage}%`}

) : undefined } focusedItemProps={focusedItemProps} disableKeyNav={disableKeyNav} /> ); }; const WithIndexingStatus = (props: Omit) => { const { indexingStatus } = useContext(RepositoriesContext); const repoIndexingStatus = useMemo(() => { return indexingStatus[props.repo.ref]; }, [indexingStatus[props.repo.ref]]); return ; }; export default memo(WithIndexingStatus); ================================================ FILE: client/src/Project/CurrentTabContent/ChatTab/ActionsDropdown.tsx ================================================ import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import DropdownSection from '../../../components/Dropdown/Section'; import SectionItem from '../../../components/Dropdown/Section/SectionItem'; import { SplitViewIcon, TrashCanIcon } from '../../../icons'; import { deleteConversation } from '../../../services/api'; import { openInSplitViewShortcut } from '../../../consts/shortcuts'; type Props = { handleMoveToAnotherSide: () => void; refreshCurrentProjectConversations: () => void; closeTab: (tabKey: string, side: 'left' | 'right') => void; conversationId?: string; projectId?: string; tabKey: string; side: 'left' | 'right'; }; const ActionsDropdown = ({ handleMoveToAnotherSide, refreshCurrentProjectConversations, conversationId, projectId, closeTab, tabKey, side, }: Props) => { const { t } = useTranslation(); const removeConversation = useCallback(async () => { if (projectId && conversationId) { await deleteConversation(projectId, conversationId); refreshCurrentProjectConversations(); closeTab(tabKey, side); } }, [ projectId, conversationId, closeTab, refreshCurrentProjectConversations, tabKey, side, ]); return (
} /> {conversationId && ( } /> )}
); }; export default memo(ActionsDropdown); ================================================ FILE: client/src/Project/CurrentTabContent/ChatTab/ChatPersistentState.tsx ================================================ import { memo, useCallback, useContext, useEffect, useRef, useState, } from 'react'; import { useTranslation } from 'react-i18next'; import { ProjectContext } from '../../../context/projectContext'; import { ChatMessage, ChatMessageAuthor, ChatMessageServer, ChatMessageUser, ChatTabType, InputValueType, ParsedQueryType, ParsedQueryTypeEnum, TabTypesEnum, } from '../../../types/general'; import { conversationsCache } from '../../../services/cache'; import { mapLoadingSteps, mapUserQuery } from '../../../mappers/conversation'; import { focusInput } from '../../../utils/domUtils'; import { ChatsContext } from '../../../context/chatsContext'; import { TabsContext } from '../../../context/tabsContext'; import { API_BASE_URL, getConversation } from '../../../services/api'; import { concatenateParsedQuery, splitUserInputAfterAutocomplete, } from '../../../utils'; type Options = { path: string; lines: [number, number]; repoRef: string; branch?: string | null; }; type Props = { tabKey: string; tabTitle?: string; conversationId?: string; initialQuery?: Options; side: 'left' | 'right'; }; const ChatPersistentState = ({ tabKey, tabTitle, side, initialQuery, conversationId: convId, }: Props) => { const { t } = useTranslation(); const { project, refreshCurrentProjectConversations } = useContext( ProjectContext.Current, ); const { setChats } = useContext(ChatsContext); const { openNewTab, updateTabProperty } = useContext(TabsContext.Handlers); const eventSource = useRef(null); const [conversation, setConversation] = useState([]); useEffect(() => { setChats((prev) => { return { ...prev, [tabKey]: { ...prev[tabKey], conversation } }; }); }, [conversation]); const [selectedLines, setSelectedLines] = useState<[number, number] | null>( null, ); useEffect(() => { setChats((prev) => { return { ...prev, [tabKey]: { ...prev[tabKey], selectedLines } }; }); }, [selectedLines]); const [inputValue, setInputValue] = useState({ plain: '', parsed: [], }); useEffect(() => { setChats((prev) => { return { ...prev, [tabKey]: { ...prev[tabKey], inputValue } }; }); }, [inputValue]); const [submittedQuery, setSubmittedQuery] = useState< InputValueType & { options?: Options } >( initialQuery ? { parsed: [ { type: ParsedQueryTypeEnum.TEXT, text: `#explain_${initialQuery.path}:${initialQuery.lines.join( '-', )}-${Date.now()}`, }, ], plain: `#explain_${initialQuery.path}:${initialQuery.lines.join( '-', )}-${Date.now()}`, options: initialQuery, } : { parsed: [], plain: '', }, ); useEffect(() => { setChats((prev) => { return { ...prev, [tabKey]: { ...prev[tabKey], submittedQuery } }; }); }, [submittedQuery]); const [isLoading, setLoading] = useState(false); useEffect(() => { setChats((prev) => { return { ...prev, [tabKey]: { ...prev[tabKey], isLoading } }; }); }, [isLoading]); const [isDeprecatedModalOpen, setDeprecatedModalOpen] = useState(false); useEffect(() => { setChats((prev) => { return { ...prev, [tabKey]: { ...prev[tabKey], isDeprecatedModalOpen } }; }); }, [isDeprecatedModalOpen]); const [hideMessagesFrom, setHideMessagesFrom] = useState(null); useEffect(() => { setChats((prev) => { return { ...prev, [tabKey]: { ...prev[tabKey], hideMessagesFrom } }; }); }, [hideMessagesFrom]); const [queryIdToEdit, setQueryIdToEdit] = useState(''); useEffect(() => { setChats((prev) => { return { ...prev, [tabKey]: { ...prev[tabKey], queryIdToEdit } }; }); }, [queryIdToEdit]); const [inputImperativeValue, setInputImperativeValue] = useState({ plain: '', parsed: [] }); useEffect(() => { setChats((prev) => { return { ...prev, [tabKey]: { ...prev[tabKey], inputImperativeValue } }; }); }, [inputImperativeValue]); const [threadId, setThreadId] = useState(''); useEffect(() => { setChats((prev) => { return { ...prev, [tabKey]: { ...prev[tabKey], threadId } }; }); }, [threadId]); const [conversationId, setConversationId] = useState(''); useEffect(() => { setChats((prev) => { return { ...prev, [tabKey]: { ...prev[tabKey], conversationId } }; }); }, [conversationId]); const closeDeprecatedModal = useCallback(() => { setDeprecatedModalOpen(false); }, []); useEffect(() => { setChats((prev) => { return { ...prev, [tabKey]: { ...prev[tabKey], setConversation, setInputValue, setSelectedLines, setSubmittedQuery, setThreadId, closeDeprecatedModal, }, }; }); }, []); const setInputValueImperatively = useCallback( (value: ParsedQueryType[] | string) => { setInputValue( typeof value === 'string' ? { plain: value, parsed: splitUserInputAfterAutocomplete(value) } : { parsed: value, plain: concatenateParsedQuery(value) }, ); setInputImperativeValue( typeof value === 'string' ? { plain: value, parsed: splitUserInputAfterAutocomplete(value) } : { parsed: value, plain: concatenateParsedQuery(value) }, ); focusInput(); }, [], ); useEffect(() => { setChats((prev) => { return { ...prev, [tabKey]: { ...prev[tabKey], setInputValueImperatively }, }; }); }, [setInputValueImperatively]); const makeSearch = useCallback( async (query: string, options?: Options) => { if (!query) { return; } eventSource.current?.close(); setInputValue({ plain: '', parsed: [] }); setInputImperativeValue({ plain: '', parsed: [] }); setLoading(true); setQueryIdToEdit(''); setHideMessagesFrom(null); const url = `${API_BASE_URL}/projects/${project?.id}/answer${ options ? `/explain` : `` }`; const queryParams: Record = { answer_model: 'gpt-4-turbo-24k', }; if (conversationId) { queryParams.conversation_id = conversationId; if (queryIdToEdit) { queryParams.parent_query_id = queryIdToEdit; } } if (options) { queryParams.relative_path = options.path; queryParams.repo_ref = options.repoRef; if (options.branch) { queryParams.branch = options.branch; } queryParams.line_start = options.lines[0].toString(); queryParams.line_end = options.lines[1].toString(); queryParams.q = query; } else { queryParams.q = query; } const fullUrl = url + '?' + new URLSearchParams(queryParams).toString(); console.log(fullUrl); eventSource.current = new EventSource(fullUrl); setSelectedLines(null); let firstResultCame: boolean; eventSource.current.onerror = (err) => { console.log('SSE error', err); firstResultCame = false; stopGenerating(); setConversation((prev) => { const newConversation = prev.slice(0, -1); const lastMessage: ChatMessage = { author: ChatMessageAuthor.Server, isLoading: false, error: t( "We couldn't answer your question. You can try asking again in a few moments, or rephrasing your question.", ), loadingSteps: [], queryId: '', responseTimestamp: new Date().toISOString(), }; if (!options) { // setInputValue(prev[prev.length - 2]?.text || submittedQuery); setInputValueImperatively( (prev[prev.length - 2] as ChatMessageUser)?.parsedQuery || prev[prev.length - 2]?.text || submittedQuery.parsed, ); } setSubmittedQuery({ plain: '', parsed: [] }); return [...newConversation, lastMessage]; }); }; let conversation_id = ''; setConversation((prev) => prev[prev.length - 1].author === ChatMessageAuthor.Server && (prev[prev.length - 1] as ChatMessageServer).isLoading ? prev : [ ...prev, { author: ChatMessageAuthor.Server, isLoading: true, loadingSteps: [], text: '', conclusion: '', queryId: '', responseTimestamp: '', }, ], ); eventSource.current.onmessage = (ev) => { console.log(ev.data); if ( ev.data === '{"Err":"incompatible client"}' || ev.data === '{"Err":"failed to check compatibility"}' ) { eventSource.current?.close(); if (ev.data === '{"Err":"incompatible client"}') { setDeprecatedModalOpen(true); } else { setConversation((prev) => { const newConversation = prev.slice(0, -1); const lastMessage: ChatMessage = { author: ChatMessageAuthor.Server, isLoading: false, error: t( "We couldn't answer your question. You can try asking again in a few moments, or rephrasing your question.", ), loadingSteps: [], queryId: '', responseTimestamp: new Date().toISOString(), }; if (!options) { // setInputValue(prev[prev.length - 1]?.text || submittedQuery); setInputValueImperatively( (prev[prev.length - 1] as ChatMessageUser)?.parsedQuery || prev[prev.length - 2]?.text || submittedQuery.parsed, ); } setSubmittedQuery({ plain: '', parsed: [] }); return [...newConversation, lastMessage]; }); } setLoading(false); return; } try { const data = JSON.parse(ev.data); if (data?.Ok?.ChatEvent) { const newMessage = data.Ok.ChatEvent; conversationsCache[conversation_id] = undefined; // clear cache on new answer setConversation((prev) => { const newConversation = prev?.slice(0, -1) || []; const lastMessage = prev?.slice(-1)[0]; const messageToAdd = { author: ChatMessageAuthor.Server, isLoading: true, loadingSteps: mapLoadingSteps(newMessage.search_steps, t), text: newMessage.answer, conclusion: newMessage.conclusion, queryId: newMessage.id, responseTimestamp: newMessage.response_timestamp, explainedFile: newMessage.focused_chunk?.repo_path, }; const lastMessages: ChatMessage[] = lastMessage?.author === ChatMessageAuthor.Server ? [messageToAdd] : [...prev.slice(-1), messageToAdd]; return [...newConversation, ...lastMessages]; }); // workaround: sometimes we get [^summary]: before it is removed from response if (newMessage.answer?.length > 11 && !firstResultCame) { if (newMessage.focused_chunk?.repo_path) { openNewTab( { type: TabTypesEnum.FILE, path: newMessage.focused_chunk.repo_path.path, repoRef: newMessage.focused_chunk.repo_path.repo, scrollToLine: newMessage.focused_chunk.start_line > -1 ? `${newMessage.focused_chunk.start_line}_${newMessage.focused_chunk.end_line}` : undefined, }, side === 'left' ? 'right' : 'left', ); } firstResultCame = true; } } else if (data?.Ok?.StreamEnd) { const message = data.Ok.StreamEnd; conversation_id = message.conversation_id; setThreadId(message.thread_id); setConversationId(message.conversation_id); if (conversation.length < 2) { updateTabProperty( tabKey, 'conversationId', message.conversation_id, side, ); } eventSource.current?.close(); eventSource.current = null; setLoading(false); setConversation((prev) => { const newConversation = prev.slice(0, -1); const lastMessage = { ...prev.slice(-1)[0], isLoading: false, }; return [...newConversation, lastMessage]; }); refreshCurrentProjectConversations(); setTimeout(() => focusInput(), 100); return; } else if (data.Err) { setConversation((prev) => { const lastMessageIsServer = prev[prev.length - 1].author === ChatMessageAuthor.Server; const newConversation = prev.slice( 0, lastMessageIsServer ? -2 : -1, ); const lastMessage: ChatMessageServer = { ...(lastMessageIsServer ? (prev.slice(-1)[0] as ChatMessageServer) : { author: ChatMessageAuthor.Server, loadingSteps: [], queryId: '', responseTimestamp: new Date().toISOString(), }), isLoading: false, error: data.Err === 'request failed 5 times' ? t( 'Failed to get a response from OpenAI. Try again in a few moments.', ) : t( "We couldn't answer your question. You can try asking again in a few moments, or rephrasing your question.", ), }; if (!options) { setInputValueImperatively( ( prev[ prev.length - (lastMessageIsServer ? 2 : 1) ] as ChatMessageUser )?.parsedQuery || prev[prev.length - 2]?.text || submittedQuery.parsed, ); } setSubmittedQuery({ plain: '', parsed: [] }); return [...newConversation, lastMessage]; }); eventSource.current?.close(); eventSource.current = null; setLoading(false); } } catch (err) { console.log('failed to parse response', err); } }; }, [conversationId, t, queryIdToEdit, openNewTab, side], ); useEffect(() => { return () => { eventSource.current?.close(); }; }, []); useEffect(() => { if (!submittedQuery.plain) { return; } let userQuery = submittedQuery.plain; let userQueryParsed = submittedQuery.parsed; const options = submittedQuery.options; if (submittedQuery.plain.startsWith('#explain_')) { const [prefix, ending] = submittedQuery.plain.split(':'); const [lineStart, lineEnd] = ending.split('-'); const filePath = prefix.slice(9); userQuery = t( `Explain lines {{lineStart}} - {{lineEnd}} in {{filePath}}`, { lineStart: Number(lineStart) + 1, lineEnd: Number(lineEnd) + 1, filePath, }, ); userQueryParsed = [{ type: ParsedQueryTypeEnum.TEXT, text: userQuery }]; } setConversation((prev) => { return (prev.length === 1 && submittedQuery.options) || (prev.length === 2 && submittedQuery.options?.lines && submittedQuery.options === initialQuery) ? prev : [ ...prev, { author: ChatMessageAuthor.User, text: userQuery, parsedQuery: userQueryParsed, isLoading: false, }, ]; }); makeSearch(userQuery, options); }, [submittedQuery]); useEffect(() => { if (conversation.length && conversation.length < 3 && !tabTitle) { updateTabProperty( tabKey, 'title', conversation[0].text, side, ); } }, [conversation, tabKey, side, tabTitle]); const stopGenerating = useCallback(() => { eventSource.current?.close(); setLoading(false); setConversation((prev) => { const newConversation = prev.slice(0, -1); const lastMessage = { ...prev.slice(-1)[0], isLoading: false, }; return [...newConversation, lastMessage]; }); setTimeout(focusInput, 100); }, []); useEffect(() => { setChats((prev) => { return { ...prev, [tabKey]: { ...prev[tabKey], stopGenerating } }; }); }, [stopGenerating]); const onMessageEdit = useCallback( (parentQueryId: string, i: number) => { setQueryIdToEdit(parentQueryId); if (isLoading) { stopGenerating(); } setHideMessagesFrom(i); const mes = conversation[i] as ChatMessageUser; setInputValueImperatively(mes.parsedQuery || mes.text!); }, [isLoading, conversation], ); useEffect(() => { setChats((prev) => { return { ...prev, [tabKey]: { ...prev[tabKey], onMessageEdit } }; }); }, [onMessageEdit]); const onMessageEditCancel = useCallback(() => { setQueryIdToEdit(''); setInputValue({ plain: '', parsed: [] }); setInputImperativeValue({ plain: '', parsed: [] }); setHideMessagesFrom(null); }, []); useEffect(() => { setChats((prev) => { return { ...prev, [tabKey]: { ...prev[tabKey], onMessageEditCancel } }; }); }, [onMessageEditCancel]); useEffect(() => { // if it was open from history and not updated from sse message if (convId && project?.id && !conversation.length) { getConversation(project.id, convId).then((resp) => { const conv: ChatMessage[] = []; let hasOpenedTab = false; resp.exchanges.forEach((m) => { // @ts-ignore const userQuery = m.search_steps.find((s) => s.type === 'QUERY'); const parsedQuery = mapUserQuery(m); conv.push({ author: ChatMessageAuthor.User, text: m.query.raw_query || userQuery?.content?.query || '', parsedQuery, isFromHistory: true, }); conv.push({ author: ChatMessageAuthor.Server, isLoading: false, loadingSteps: mapLoadingSteps(m.search_steps, t), text: m.answer, conclusion: m.conclusion, queryId: m.id, responseTimestamp: m.response_timestamp, explainedFile: m.focused_chunk?.repo_path.path, }); if (!hasOpenedTab && m.focused_chunk?.repo_path) { openNewTab( { type: TabTypesEnum.FILE, path: m.focused_chunk.repo_path.path, repoRef: m.focused_chunk.repo_path.repo, scrollToLine: m.focused_chunk.start_line > -1 ? `${m.focused_chunk.start_line}_${m.focused_chunk.end_line}` : undefined, }, side === 'left' ? 'right' : 'left', ); hasOpenedTab = true; } }); setConversation(conv); setThreadId(resp.thread_id); setConversationId(convId); }); } }, [convId, project?.id]); return null; }; export default memo(ChatPersistentState); ================================================ FILE: client/src/Project/CurrentTabContent/ChatTab/Conversation.tsx ================================================ import React, { memo, useContext, useEffect, useMemo, useRef, useState, } from 'react'; import { ChatMessageServer } from '../../../types/general'; import { ProjectContext } from '../../../context/projectContext'; import { ChatContext, ChatsContext } from '../../../context/chatsContext'; import ScrollToBottom from '../../../components/ScrollToBottom'; import Input from './Input'; import ScrollableContent from './ScrollableContent'; import DeprecatedClientModal from './DeprecatedClientModal'; type Props = { side: 'left' | 'right'; tabKey: string; }; const Conversation = ({ side, tabKey }: Props) => { const { project } = useContext(ProjectContext.Current); const { chats } = useContext(ChatsContext); const scrollableRef = useRef(null); const [isScrollable, setIsScrollable] = useState(false); const chatData: ChatContext | undefined = useMemo( () => chats[tabKey], [chats, tabKey], ); useEffect(() => { setTimeout(() => { if (scrollableRef.current) { setIsScrollable( scrollableRef.current.scrollHeight > scrollableRef.current.clientHeight, ); } }, 100); }, [chatData?.conversation, chatData?.hideMessagesFrom]); return !chatData ? null : (
); }; export default memo(Conversation); ================================================ FILE: client/src/Project/CurrentTabContent/ChatTab/DeprecatedClientModal.tsx ================================================ import { useContext } from 'react'; import { Trans, useTranslation } from 'react-i18next'; import { CloseSignIcon } from '../../../icons'; import { DeviceContext } from '../../../context/deviceContext'; import Button from '../../../components/Button'; import Modal from '../../../components/Modal'; type Props = { isOpen: boolean; onClose: () => void; }; const DeprecatedClientModal = ({ isOpen, onClose }: Props) => { const { t } = useTranslation(); const { openLink, relaunch } = useContext(DeviceContext); return (

Update Required

We've made some exciting enhancements to bloop! To continue enjoying the full functionality, including the natural language search feature, please update your app to the latest version.

To update your app, please visit our releases page on GitHub and download the latest version manually. Thank you for using bloop.

); }; export default DeprecatedClientModal; ================================================ FILE: client/src/Project/CurrentTabContent/ChatTab/Input/ProseMirror/index.tsx ================================================ import { memo, useCallback, useEffect, useMemo, useState } from 'react'; import { EditorState, TextSelection, Transaction } from 'prosemirror-state'; import { Schema } from 'prosemirror-model'; import { keymap } from 'prosemirror-keymap'; import { baseKeymap } from 'prosemirror-commands'; import { NodeViewComponentProps, ProseMirror, react, ReactNodeViewConstructor, useNodeViews, } from '@nytimes/react-prosemirror'; import { schema as basicSchema } from 'prosemirror-schema-basic'; // @ts-ignore import * as icons from 'file-icons-js'; import { useTranslation } from 'react-i18next'; import { InputEditorContent, ParsedQueryType, } from '../../../../../types/general'; import { getFileExtensionForLang } from '../../../../../utils'; import { blurInput } from '../../../../../utils/domUtils'; import { MentionOptionType } from '../../../../../types/results'; import { getMentionsPlugin } from './mentionPlugin'; import { addMentionNodes, mapEditorContentToInputValue } from './utils'; import { placeholderPlugin } from './placeholderPlugin'; const schema = new Schema({ nodes: addMentionNodes(basicSchema.spec.nodes), marks: basicSchema.spec.marks, }); function Paragraph({ children }: NodeViewComponentProps) { return

{children}

; } const reactNodeViews: Record = { paragraph: () => ({ component: Paragraph, dom: document.createElement('div'), contentDOM: document.createElement('span'), }), }; type Props = { getDataLang: (search: string) => Promise; getDataPath: (search: string) => Promise; getDataRepo: (search: string) => Promise; initialValue?: Record | null; onChange: (contents: InputEditorContent[]) => void; onSubmit?: (s: { parsed: ParsedQueryType[]; plain: string }) => void; placeholder: string; }; const InputCore = ({ getDataLang, getDataPath, getDataRepo, initialValue, onChange, onSubmit, placeholder, }: Props) => { const { t } = useTranslation(); const mentionPlugin = useMemo( () => getMentionsPlugin({ delay: 10, getSuggestions: async ( type: string, text: string, done: (s: MentionOptionType[]) => void, ) => { const data = await Promise.all([ getDataRepo(text), getDataPath(text), getDataLang(text), ]); done([...data[0], ...data[1], ...data[2]]); }, getSuggestionsHTML: (items) => { return ( '
' + items .map( (i) => `
${ i.isFirst ? `
${t( i.type === 'repo' ? 'Repositories' : i.type === 'dir' ? 'Directories' : i.type === 'lang' ? 'Languages' : 'Files', )}
` : '' }
${ i.type === 'repo' ? ` ` : i.type === 'dir' ? ` ` : `` }${i.display}${ i.hint ? `${i.hint}` : '' }
`, ) .join('') + '
' ); }, }), [], ); const plugins = useMemo(() => { return [ placeholderPlugin(placeholder), react(), mentionPlugin, keymap({ ...baseKeymap, Escape: (state) => { const key = Object.keys(state).find((k) => k.startsWith('autosuggestions'), ); // @ts-ignore if (key && state[key]?.active) { return true; } blurInput(); return true; }, Enter: (state) => { const key = Object.keys(state).find((k) => k.startsWith('autosuggestions'), ); // @ts-ignore if (key && state[key]?.active) { return false; } const parts = state.toJSON().doc?.content?.[0]?.content; // trying to submit with no text if (!parts) { return false; } onSubmit?.(mapEditorContentToInputValue(parts)); return true; }, 'Ctrl-Enter': baseKeymap.Enter, 'Cmd-Enter': baseKeymap.Enter, 'Shift-Enter': baseKeymap.Enter, }), ]; }, [onSubmit]); const { nodeViews, renderNodeViews } = useNodeViews(reactNodeViews); const [mount, setMount] = useState(null); const [state, setState] = useState( EditorState.create({ doc: initialValue ? schema.topNodeType.create(null, [schema.nodeFromJSON(initialValue)]) : undefined, schema, plugins, }), ); useEffect(() => { if (mount) { const newState = EditorState.create({ schema, plugins, doc: initialValue ? schema.topNodeType.create(null, [schema.nodeFromJSON(initialValue)]) : undefined, }); const endPos = newState.selection.$to.after() - 1; newState.selection = new TextSelection(newState.doc.resolve(endPos)); setState(newState); } }, [mount, initialValue, plugins]); const dispatchTransaction = useCallback( (tr: Transaction) => setState((oldState) => oldState.apply(tr)), [], ); useEffect(() => { const newValue = state.toJSON().doc?.content?.[0]?.content || ''; onChange(newValue || []); }, [state]); return (
{renderNodeViews()}
); }; export default memo(InputCore); ================================================ FILE: client/src/Project/CurrentTabContent/ChatTab/Input/ProseMirror/mentionPlugin.ts ================================================ import { Plugin, PluginKey } from 'prosemirror-state'; import { Decoration, DecorationSet, EditorView } from 'prosemirror-view'; import { ResolvedPos } from 'prosemirror-model'; import { MentionOptionType } from '../../../../../types/results'; export function getRegexp(mentionTrigger: string, allowSpace?: boolean) { return allowSpace ? new RegExp('(^|\\s)' + mentionTrigger + '([\\w-\\+]*\\s?[\\w-\\+.]*)$') : new RegExp('(^|\\s)' + mentionTrigger + '([\\w-\\+.]*)$'); } const insertAfterSelect = String.fromCharCode(160); export function getMatch( $position: ResolvedPos, opts: { mentionTrigger: string; allowSpace?: boolean; }, ) { try { // take current para text content upto cursor start. // this makes the regex simpler and parsing the matches easier. const parastart = $position.before(); const text = $position.doc.textBetween( parastart, $position.pos, '\n', '\0', ); const regex = getRegexp(opts.mentionTrigger, opts.allowSpace); const match = text.match(regex); // if match found, return match with useful information. if (match) { // adjust match.index to remove the matched extra space match.index = match[0].startsWith(' ') || match[0].startsWith(insertAfterSelect) ? (match.index || 0) + 1 : match.index; match[0] = match[0].startsWith(' ') || match[0].startsWith(insertAfterSelect) ? match[0].substring(1, match[0].length) : match[0]; // The absolute position of the match in the document const from = $position.start() + match.index!; const to = from + match[0].length; const queryText = match[2]; return { range: { from: from, to: to }, queryText: queryText, type: 'mention', }; } // else if no match don't return anything. } catch (e) { console.log(e); } } /** * Util to debounce call to a function. * >>> debounce(function(){}, 1000, this) */ export const debounce = (function () { let timeoutId: number; return function (func: () => void, timeout: number, context: any): number { // @ts-ignore context = context || this; clearTimeout(timeoutId); timeoutId = window.setTimeout(function () { // @ts-ignore func.apply(context, arguments); }, timeout); return timeoutId; }; })(); type State = { active: boolean; range: { from: number; to: number; }; type: string; text: string; suggestions: Record[]; index: number; }; const getNewState = function () { return { active: false, range: { from: 0, to: 0, }, type: '', text: '', suggestions: [], index: 0, // current active suggestion index }; }; type Options = { mentionTrigger: string; allowSpace?: boolean; activeClass: string; suggestionTextClass?: string; getSuggestions: ( type: string, text: string, done: (s: MentionOptionType[]) => void, ) => void; delay: number; getSuggestionsHTML: (items: MentionOptionType[], type: string) => string; }; export function getMentionsPlugin(opts: Partial) { // default options const defaultOpts = { mentionTrigger: '@', allowSpace: false, getSuggestions: ( type: string, text: string, cb: (s: { name: string }[]) => void, ) => { cb([]); }, getSuggestionsHTML: (items: { name: string }[]) => '
' + items .map((i) => '
' + i.name + '
') .join('') + '
', activeClass: 'suggestion-item-active', suggestionTextClass: 'prosemirror-suggestion', maxNoOfSuggestions: 10, delay: 500, }; const options = Object.assign({}, defaultOpts, opts) as Options; // timeoutId for clearing debounced calls let showListTimeoutId: number; // dropdown element const el = document.createElement('div'); const showList = function ( view: EditorView, state: State, suggestions: MentionOptionType[], opts: Options, ) { try { el.innerHTML = opts.getSuggestionsHTML(suggestions, state.type); // attach new item event handlers el.querySelectorAll('.suggestion-item').forEach( function (itemNode, index) { itemNode.addEventListener('click', function () { select(view, state, opts); view.focus(); }); // TODO: setIndex() needlessly queries. // We already have the itemNode. SHOULD OPTIMIZE. itemNode.addEventListener('mouseover', function () { setIndex(index, state, opts); }); itemNode.addEventListener('mouseout', function () { setIndex(index, state, opts); }); }, ); // highlight first element by default - like Facebook. addClassAtIndex(state.index, opts.activeClass); // TODO: knock off domAtPos usage. It's not documented and is not officially a public API. // It's used currently, only to optimize the the query for textDOM const node = view.domAtPos(view.state.selection.$from.pos); const paraDOM = node.node; const textDOM = (paraDOM as HTMLElement).querySelector( '.' + opts.suggestionTextClass, ); const offset = textDOM?.getBoundingClientRect(); document.body.appendChild(el); el.classList.add('suggestion-item-container'); el.style.position = 'fixed'; el.style.left = -9999 + 'px'; const offsetLeft = offset?.left || 0; const offsetTop = offset?.top || 0; setTimeout(() => { el.style.left = offsetLeft + el.clientWidth < window.innerWidth ? offsetLeft + 'px' : offsetLeft + (window.innerWidth - (offsetLeft + el.clientWidth) - 10) + 'px'; el.style.bottom = window.innerHeight - offsetTop + el.clientHeight > window.innerHeight ? window.innerHeight - offsetTop - el.clientHeight - 20 + 'px' : window.innerHeight - offsetTop + 'px'; }, 10); el.style.display = 'block'; el.style.zIndex = '80'; } catch (e) { console.log(e); } }; const hideList = function () { el.style.display = 'none'; }; const removeClassAtIndex = function (index: number, className: string) { const itemList = el.querySelector('.suggestion-item-list')?.childNodes; const prevItem = itemList?.[index]; (prevItem as HTMLElement)?.classList.remove(className); }; const addClassAtIndex = function (index: number, className: string) { const itemList = el.querySelector('.suggestion-item-list')?.childNodes; const prevItem = itemList?.[index]; (prevItem as HTMLElement)?.classList.add(className); return prevItem as HTMLElement | undefined; }; const setIndex = function (index: number, state: State, opts: Options) { removeClassAtIndex(state.index, opts.activeClass); state.index = index; addClassAtIndex(state.index, opts.activeClass); }; const goNext = function (view: EditorView, state: State, opts: Options) { removeClassAtIndex(state.index, opts.activeClass); state.index++; state.index = state.index === state.suggestions.length ? 0 : state.index; const el = addClassAtIndex(state.index, opts.activeClass); el?.scrollIntoView({ block: 'nearest' }); }; const goPrev = function (view: EditorView, state: State, opts: Options) { removeClassAtIndex(state.index, opts.activeClass); state.index--; state.index = state.index === -1 ? state.suggestions.length - 1 : state.index; const el = addClassAtIndex(state.index, opts.activeClass); el?.scrollIntoView({ block: 'nearest' }); }; const select = function (view: EditorView, state: State, opts: Options) { const item = state.suggestions[state.index]; const attrs = { ...item, }; const node = view.state.schema.nodes[state.type].create(attrs); const spaceNode = view.state.schema.text(insertAfterSelect); const tr = view.state.tr.replaceWith(state.range.from, state.range.to, [ node, spaceNode, ]); //var newState = view.state.apply(tr); //view.updateState(newState); view.dispatch(tr); }; return new Plugin({ key: new PluginKey('autosuggestions'), // we will need state to track if suggestion dropdown is currently active or not state: { init() { return getNewState(); }, apply(tr, state) { try { // compute state.active for current transaction and return const newState = getNewState(); const selection = tr.selection; if (selection.from !== selection.to) { return newState; } const $position = selection.$from; const match = getMatch($position, options); // if match found update state if (match) { newState.active = true; newState.range = match.range; newState.type = match.type!; newState.text = match.queryText; } return newState; } catch (e) { console.log(e); return state; } }, }, // We'll need props to hi-jack keydown/keyup & enter events when suggestion dropdown // is active. props: { handleKeyDown(view, e) { const state = this.getState(view.state); if (!state?.active && !state?.suggestions.length) { return false; } if (e.key === 'ArrowDown') { e.stopPropagation(); goNext(view, state, options); return true; } else if (e.key === 'ArrowUp') { e.stopPropagation(); goPrev(view, state, options); return true; } else if (e.key === 'Enter') { e.stopPropagation(); select(view, state, options); return true; } else if (e.key === 'Escape') { e.stopPropagation(); clearTimeout(showListTimeoutId); hideList(); // @ts-ignore this.state = getNewState(); return true; } else { // didn't handle. handover to prosemirror for handling. return false; } }, // to decorate the currently active @mention text in ui decorations(editorState) { const { active, range } = this.getState(editorState) || {}; if (!active || !range) return null; return DecorationSet.create(editorState.doc, [ Decoration.inline(range.from, range.to, { nodeName: 'span', class: options.suggestionTextClass, }), ]); }, }, // To track down state mutations and add dropdown reactions view() { return { update: (view) => { const state = this.key?.getState(view.state); if (!state.active) { hideList(); clearTimeout(showListTimeoutId); return; } // debounce the call to avoid multiple requests showListTimeoutId = debounce( function () { // get suggestions and set new state options.getSuggestions( state.type, state.text, function (suggestions) { // update `state` argument with suggestions state.suggestions = suggestions; showList(view, state, suggestions, options); }, ); }, options.delay, this, ); }, destroy: () => { hideList(); }, }; }, }); } ================================================ FILE: client/src/Project/CurrentTabContent/ChatTab/Input/ProseMirror/nodes.ts ================================================ // @ts-ignore import * as icons from 'file-icons-js'; import { type AttributeSpec, type NodeSpec } from 'prosemirror-model'; import { getFileExtensionForLang, splitPath } from '../../../../../utils'; export const mentionNode: NodeSpec = { group: 'inline', inline: true, atom: true, attrs: { id: '' as AttributeSpec, display: '' as AttributeSpec, type: 'lang' as AttributeSpec, isFirst: '' as AttributeSpec, }, selectable: false, draggable: false, toDOM: (node) => { const isDir = node.attrs.type === 'dir' || node.attrs.display.endsWith('/') || node.attrs.display.endsWith('\\'); const folderIcon = document.createElement('span'); folderIcon.innerHTML = ` `; folderIcon.className = 'w-4 h-4 flex-shrink-0'; const repoIcon = document.createElement('span'); repoIcon.innerHTML = ` `; repoIcon.className = 'w-4 h-4 flex-shrink-0'; return [ 'span', { 'data-type': node.attrs.type, 'data-id': node.attrs.id, 'data-first': node.attrs.isFirst, 'data-display': node.attrs.display, class: 'prosemirror-tag-node inline-flex gap-1 h-[22px] items-center align-bottom bg-bg-base border border-bg-border rounded px-1', }, isDir ? folderIcon : node.attrs.type === 'repo' ? repoIcon : [ 'span', { class: `text-left w-4 h-4 file-icon flex-shrink-0 inline-flex items-center ${ icons.getClassWithColor( (node.attrs.type === 'lang' ? node.attrs.display.includes(' ') ? '.txt' : getFileExtensionForLang(node.attrs.display, true) : node.attrs.display) || '.txt', ) || icons.getClassWithColor('index.txt') }`, }, '', ], node.attrs.type === 'lang' ? node.attrs.display : isDir ? splitPath(node.attrs.display).slice(-2)[0] : splitPath(node.attrs.display).pop(), ]; }, parseDOM: [ { // match tag with following CSS Selector tag: 'span[data-type][data-id][data-first][data-display]', getAttrs: (dom) => { const id = (dom as HTMLElement).getAttribute('data-id'); const type = (dom as HTMLElement).getAttribute('data-type'); const isFirst = (dom as HTMLElement).getAttribute('data-first'); const display = (dom as HTMLElement).getAttribute('data-display'); return { id, type, isFirst, display, }; }, }, ], }; ================================================ FILE: client/src/Project/CurrentTabContent/ChatTab/Input/ProseMirror/placeholderPlugin.ts ================================================ import { Plugin } from 'prosemirror-state'; import { EditorView } from 'prosemirror-view'; export const placeholderPlugin = (text: string) => { const update = (view: EditorView) => { if (view.state.doc.content.size > 2) { view.dom.removeAttribute('data-placeholder'); } else { view.dom.setAttribute('data-placeholder', text); } }; return new Plugin({ view(view) { update(view); return { update }; }, }); }; ================================================ FILE: client/src/Project/CurrentTabContent/ChatTab/Input/ProseMirror/utils.ts ================================================ import OrderedMap from 'orderedmap'; import { type NodeSpec } from 'prosemirror-model'; import { InputEditorContent, ParsedQueryTypeEnum, } from '../../../../../types/general'; import { mentionNode } from './nodes'; export function addMentionNodes(nodes: OrderedMap) { return nodes.append({ mention: mentionNode, }); } export const mapEditorContentToInputValue = ( inputState: InputEditorContent[], ) => { const getType = (type: string) => type === 'lang' || type === 'repo' ? type : 'path'; const newValue = inputState .map((s) => s.type === 'mention' ? `${getType(s.attrs.type)}:${s.attrs.id}` : s.type === 'text' ? s.text?.replace(new RegExp(String.fromCharCode(160), 'g'), ' ') : '', ) .join(''); const newValueParsed = inputState.map((s) => s.type === 'mention' ? { type: s.attrs.type === 'lang' ? ParsedQueryTypeEnum.LANG : s.attrs.type === 'repo' ? ParsedQueryTypeEnum.REPO : ParsedQueryTypeEnum.PATH, text: s.attrs.id, } : { type: ParsedQueryTypeEnum.TEXT, text: s.text }, ); return { plain: newValue, parsed: newValueParsed, }; }; ================================================ FILE: client/src/Project/CurrentTabContent/ChatTab/Input/ReactMentions/index.tsx ================================================ import React, { memo, ReactNode, useCallback, useEffect, useRef, useState, } from 'react'; import { Mention, MentionsInput, OnChangeHandlerFunc, SuggestionDataItem, } from 'react-mentions'; import { Trans, useTranslation } from 'react-i18next'; import { getFileExtensionForLang, splitPath } from '../../../../../utils'; import FileIcon from '../../../../../components/FileIcon'; import { FolderIcon, RepositoryIcon } from '../../../../../icons'; import { InputValueType } from '../../../../../types/general'; import { blurInput } from '../../../../../utils/domUtils'; import { MentionOptionType } from '../../../../../types/results'; type Props = { placeholder: string; getDataLang: (s: string) => Promise; getDataPath: (s: string) => Promise; getDataRepo: (s: string) => Promise; value?: InputValueType; onChange: (v: string) => void; onSubmit: (v: InputValueType) => void; isDisabled?: boolean; initialValue?: InputValueType; }; const inputStyle = { '&multiLine': { highlighter: { maxHeight: 300, overflow: 'auto', }, input: { maxHeight: 300, overflow: 'auto', outline: 'none', }, }, suggestions: { list: { maxHeight: '40vh', overflowY: 'auto', backgroundColor: 'rgb(var(--bg-shade))', border: '1px solid rgb(var(--bg-border))', boxShadow: 'var(--shadow-high)', padding: 4, zIndex: 100, borderRadius: 6, marginTop: 6, }, }, }; const ReactMentionsInput = ({ placeholder, onSubmit, onChange, getDataPath, getDataRepo, getDataLang, value, isDisabled, initialValue, }: Props) => { const { t } = useTranslation(); const inputRef = useRef(null); const [isComposing, setComposition] = useState(false); const [inputValue, setInputValue] = useState(''); useEffect(() => { if (initialValue) { setInputValue(initialValue.plain); } }, [initialValue]); useEffect(() => { if (inputRef.current) { // We need to reset the height momentarily to get the correct scrollHeight for the textarea inputRef.current.style.height = '56px'; const scrollHeight = inputRef.current.scrollHeight; // We then set the height directly, outside of the render loop // Trying to set this with state or a ref will product an incorrect value. inputRef.current.style.height = Math.max(Math.min(scrollHeight, 300), 56) + 'px'; } }, [inputRef.current, value]); const handleKeyDown = useCallback( (e: React.KeyboardEvent) => { if (isComposing) { return true; } if (e.key === 'Enter' && !e.shiftKey && onSubmit && value) { e.preventDefault(); blurInput(); onSubmit({ plain: value.plain .replace(/\|(repo:.*?)\|/, '$1') .replace(/\|(path:.*?)\|/, '$1') .replace(/\|(lang:.*?)\|/, '$1'), parsed: value.parsed, }); } }, [isComposing, onSubmit, value], ); const repoTransform = useCallback((id: string, trans: string) => { const split = splitPath(trans); return trans.startsWith('local//') ? split.slice(-1)[0] : split.slice(-2).join('/'); }, []); const pathTransform = useCallback((id: string, trans: string) => { const split = splitPath(trans); return `${split[split.length - 1] || split[split.length - 2]}`; }, []); const onCompositionStart = useCallback(() => { setComposition(true); }, []); const onCompositionEnd = useCallback(() => { // this event comes before keydown and sets state faster causing unintentional submit setTimeout(() => setComposition(false), 10); }, []); const handleChange = useCallback((e) => { setInputValue(e.target.value); }, []); useEffect(() => { onChange(inputValue); }, [inputValue]); const renderRepoSuggestion = useCallback( ( entry: SuggestionDataItem, search: string, highlightedDisplay: ReactNode, index: number, focused: boolean, ) => { const d = entry as MentionOptionType; return (
{d.isFirst ? (
Repositories
) : null}
{d.display} {d.hint}
); }, [], ); const renderPathSuggestion = useCallback( ( entry: SuggestionDataItem, search: string, highlightedDisplay: ReactNode, index: number, focused: boolean, ) => { const d = entry as MentionOptionType; return (
{d.isFirst ? (
{d.type === 'dir' ? 'Directories' : 'Files'}
) : null}
{d.type === 'dir' ? ( ) : ( )} {d.display} {d.hint}
); }, [], ); const renderLangSuggestion = useCallback( ( entry: SuggestionDataItem, search: string, highlightedDisplay: ReactNode, index: number, focused: boolean, ) => { const d = entry as MentionOptionType; return (
{d.isFirst ? (
Languages
) : null}
{d.display} {d.hint}
); }, [], ); return (
); }; export default memo(ReactMentionsInput); ================================================ FILE: client/src/Project/CurrentTabContent/ChatTab/Input/index.tsx ================================================ import { Dispatch, memo, SetStateAction, useCallback, useContext, useEffect, useRef, useState, } from 'react'; import { Trans, useTranslation } from 'react-i18next'; import { ChatMessage, ChatMessageServer, FileTabType, InputEditorContent, InputValueType, ParsedQueryType, TabTypesEnum, } from '../../../../types/general'; import { getAutocomplete } from '../../../../services/api'; import { FileResItem, LangItem, RepoItem } from '../../../../types/api'; import useKeyboardNavigation from '../../../../hooks/useKeyboardNavigation'; import KeyboardHint from '../../../../components/KeyboardHint'; import { focusInput } from '../../../../utils/domUtils'; import { MentionOptionType } from '../../../../types/results'; import { splitPath, splitUserInputAfterAutocomplete } from '../../../../utils'; import { openTabsCache } from '../../../../services/cache'; import { CommandBarContext } from '../../../../context/commandBarContext'; import { UIContext } from '../../../../context/uiContext'; import { PersonIcon } from '../../../../icons'; import InputCore from './ProseMirror'; import { mapEditorContentToInputValue } from './ProseMirror/utils'; import ReactMentionsInput from './ReactMentions'; type Props = { value?: InputValueType; valueToEdit?: InputValueType; generationInProgress?: boolean; isStoppable?: boolean; onStop?: () => void; setInputValue: Dispatch>; selectedLines?: [number, number] | null; setSelectedLines?: (l: [number, number] | null) => void; queryIdToEdit?: string; onMessageEditCancel?: () => void; conversation: ChatMessage[]; hideMessagesFrom: number | null; setConversation: Dispatch>; setSubmittedQuery: Dispatch>; submittedQuery: InputValueType; isInputAtBottom?: boolean; projectId: string; }; const ConversationInput = ({ value, valueToEdit, setInputValue, generationInProgress, isStoppable, onStop, queryIdToEdit, onMessageEditCancel, conversation, hideMessagesFrom, setConversation, setSubmittedQuery, submittedQuery, isInputAtBottom, projectId, }: Props) => { const { t } = useTranslation(); const { isVisible } = useContext(CommandBarContext.General); const { chatInputType } = useContext(UIContext.ChatInputType); const { setIsLeftSidebarFocused } = useContext(UIContext.Focus); const [initialProseValue, setInitialProseValue] = useState< Record | null | undefined >({ type: 'paragraph', content: value?.parsed .filter((pq) => ['path', 'lang', 'text'].includes(pq.type)) .map((pq) => pq.type === 'text' ? { type: 'text', text: pq.text } : { type: 'mention', attrs: { id: pq.text, display: pq.text, type: pq.type, isFirst: false, }, }, ), }); const [initialMentionsValue, setInitialMentionsValue] = useState(value); const [hasRendered, setHasRendered] = useState(false); const containerRef = useRef(null); useEffect(() => { setHasRendered(true); setTimeout(focusInput, 500); }, []); useEffect(() => { if (hasRendered && valueToEdit) { setInitialProseValue({ type: 'paragraph', content: valueToEdit?.parsed .filter((pq) => ['path', 'lang', 'text', 'repo'].includes(pq.type)) .map((pq) => pq.type === 'text' ? { type: 'text', text: pq.text } : { type: 'mention', attrs: { id: pq.text, display: pq.text, type: pq.type, isFirst: false, }, }, ), }); setInitialMentionsValue(valueToEdit); } }, [valueToEdit]); // useEffect(() => { // if (containerRef.current) { // setIsInputAtBottom(containerRef.current) // } // }, [conversation]); const onSubmit = useCallback( (value: { parsed: ParsedQueryType[]; plain: string }) => { if ( (conversation[conversation.length - 1] as ChatMessageServer) ?.isLoading || !value.plain.trim() ) { return; } if (hideMessagesFrom !== null) { setConversation((prev) => prev.slice(0, hideMessagesFrom)); } setSubmittedQuery(value); }, [conversation, submittedQuery, hideMessagesFrom], ); const onChangeProseMirrorInput = useCallback( (inputState: InputEditorContent[]) => { setInputValue(mapEditorContentToInputValue(inputState)); setIsLeftSidebarFocused(false); }, [], ); const onChangeReactMentionsInput = useCallback((newVal: string) => { setInputValue({ plain: newVal, parsed: splitUserInputAfterAutocomplete(newVal), }); setIsLeftSidebarFocused(false); }, []); const onSubmitButtonClicked = useCallback(() => { if (value && onSubmit) { onSubmit(value); } }, [value, onSubmit]); const getDataPath = useCallback( async (search: string, callback?: (v: MentionOptionType[]) => void) => { const respPath = await getAutocomplete( projectId, `path:${search}&content=false&page_size=8`, ); const fileResults = respPath.data.filter( (d): d is FileResItem => d.kind === 'file_result', ); const dirResults = fileResults .filter((d) => d.data.is_dir) .map((d) => ({ path: d.data.relative_path.text, repo: d.data.repo_ref, })); const filesResults = openTabsCache.tabs .filter( (t): t is FileTabType => t.type === TabTypesEnum.FILE && (!search || t.path?.toLowerCase().includes(search?.toLowerCase())), ) .map((t) => ({ path: t.path, repo: t.repoRef })); filesResults.push( ...fileResults .filter( (d) => !d.data.is_dir && !filesResults.find( (f) => f.path === d.data.relative_path.text && f.repo === d.data.repo_ref, ), ) .map((d) => ({ path: d.data.relative_path.text, repo: d.data.repo_ref, })), ); const results: MentionOptionType[] = []; filesResults.forEach((fr, i) => { results.push({ id: `${fr.repo}-${fr.path}`, display: fr.path, type: 'file', isFirst: i === 0, hint: splitPath(fr.repo).pop(), }); }); dirResults.forEach((fr, i) => { results.push({ id: `${fr.repo}-${fr.path}`, display: fr.path, type: 'dir', isFirst: i === 0, hint: splitPath(fr.repo).pop(), }); }); callback?.(results); return results; }, [projectId], ); const getDataLang = useCallback( async (search: string, callback?: (v: MentionOptionType[]) => void) => { const respLang = await getAutocomplete( projectId, `lang:${search}&content=false&page_size=8`, ); const langResults = respLang.data .filter((d): d is LangItem => d.kind === 'lang') .map((d) => d.data); const results: MentionOptionType[] = []; langResults.forEach((fr, i) => { results.push({ id: fr, display: fr, type: 'lang', isFirst: i === 0 }); }); callback?.(results); return results; }, [projectId], ); const getDataRepo = useCallback( async (search: string, callback?: (v: MentionOptionType[]) => void) => { const respRepo = await getAutocomplete( projectId, `repo:${search}&content=false&path=false&file=false&page_size=8`, ); const repoResults = respRepo.data .filter((d): d is RepoItem => d.kind === 'repository_result') .map((d) => d.data); const results: MentionOptionType[] = []; repoResults.forEach((rr, i) => { results.push({ id: rr.name.text, display: rr.name.text.replace('github.com/', ''), type: 'repo', isFirst: i === 0, }); }); callback?.(results); return results; }, [projectId], ); const handleKeyEvent = useCallback( (e: KeyboardEvent) => { if ( e.key === 'Escape' && ((onMessageEditCancel && queryIdToEdit) || (isStoppable && onStop)) ) { e.preventDefault(); e.stopPropagation(); onMessageEditCancel?.(); onStop?.(); } }, [onMessageEditCancel, isStoppable, onStop], ); useKeyboardNavigation( handleKeyEvent, (!queryIdToEdit && !isStoppable) || isVisible, ); return (

You

{chatInputType === 'simplified' ? ( ) : generationInProgress ? (
Generating answer...
) : ( )}
{isStoppable && ( )} {!!queryIdToEdit && ( )}
); }; export default memo(ConversationInput); ================================================ FILE: client/src/Project/CurrentTabContent/ChatTab/Message/LoadingStep.tsx ================================================ import { memo, useCallback, useContext } from 'react'; import { useTranslation } from 'react-i18next'; import FileChip from '../../../../components/Chips/FileChip'; import { ChatLoadingStep, TabTypesEnum } from '../../../../types/general'; import { TabsContext } from '../../../../context/tabsContext'; type Props = ChatLoadingStep & { side: 'left' | 'right'; repo?: string; }; const LoadingStep = ({ type, path, displayText, side, repo }: Props) => { const { t } = useTranslation(); const { openNewTab } = useContext(TabsContext.Handlers); const handleClickFile = useCallback(() => { if (type === 'proc' && repo && path) { openNewTab( { type: TabTypesEnum.FILE, repoRef: repo, path, }, side === 'left' ? 'right' : 'left', ); } }, [path, repo, side]); return (
{type === 'proc' ? t('Reading ') : displayText} {type === 'proc' ? ( ) : null}
); }; export default memo(LoadingStep); ================================================ FILE: client/src/Project/CurrentTabContent/ChatTab/Message/UserParsedQuery/LangChip.tsx ================================================ import { getFileExtensionForLang } from '../../../../../utils'; import FileIcon from '../../../../../components/FileIcon'; type Props = { lang: string; }; const LangChip = ({ lang }: Props) => { return ( {lang} ); }; export default LangChip; ================================================ FILE: client/src/Project/CurrentTabContent/ChatTab/Message/UserParsedQuery/PathChip.tsx ================================================ import { useMemo } from 'react'; import { FolderIcon } from '../../../../../icons'; import FileIcon from '../../../../../components/FileIcon'; import { splitPath } from '../../../../../utils'; type Props = { path: string; }; const PathChip = ({ path }: Props) => { const isFolder = useMemo(() => path.endsWith('/'), [path]); return ( {isFolder ? ( ) : ( )} {isFolder ? path.replace(/\/$/, '') : splitPath(path).pop()} ); }; export default PathChip; ================================================ FILE: client/src/Project/CurrentTabContent/ChatTab/Message/UserParsedQuery/RepoChip.tsx ================================================ import { RepositoryIcon } from '../../../../../icons'; import { splitPath } from '../../../../../utils'; type Props = { name: string; }; const RepoChip = ({ name }: Props) => { return ( {splitPath(name).pop()} ); }; export default RepoChip; ================================================ FILE: client/src/Project/CurrentTabContent/ChatTab/Message/UserParsedQuery/index.tsx ================================================ import { memo } from 'react'; import { ParsedQueryType, ParsedQueryTypeEnum, } from '../../../../../types/general'; import PathChip from './PathChip'; import LangChip from './LangChip'; import RepoChip from './RepoChip'; type Props = { textQuery: string; parsedQuery?: ParsedQueryType[]; }; const UserParsedQuery = ({ textQuery, parsedQuery }: Props) => { return ( {parsedQuery ? parsedQuery.map((p, i) => p.type === ParsedQueryTypeEnum.TEXT ? ( p.text ) : p.type === ParsedQueryTypeEnum.PATH ? ( ) : p.type === ParsedQueryTypeEnum.LANG ? ( ) : p.type === ParsedQueryTypeEnum.REPO ? ( ) : null, ) : textQuery} ); }; export default memo(UserParsedQuery); ================================================ FILE: client/src/Project/CurrentTabContent/ChatTab/Message/index.tsx ================================================ import { memo, useCallback, useContext, useEffect, useState } from 'react'; import { Trans, useTranslation } from 'react-i18next'; import { format } from 'date-fns'; import { ChatLoadingStep, ChatMessageAuthor, ParsedQueryType, } from '../../../../types/general'; import MarkdownWithCode from '../../../../components/MarkdownWithCode'; import Button from '../../../../components/Button'; import { CheckListIcon, LikeIcon, PencilIcon, PersonIcon, UnlikeIcon, WarningSignIcon, } from '../../../../icons'; import { getDateFnsLocale } from '../../../../utils'; import SpinLoaderContainer from '../../../../components/Loaders/SpinnerLoader'; import { getPlainFromStorage, LOADING_STEPS_SHOWN_KEY, savePlainToStorage, } from '../../../../services/storage'; import { LocaleContext } from '../../../../context/localeContext'; import { upvoteAnswer } from '../../../../services/api'; import CopyButton from '../../../../components/MarkdownWithCode/CopyButton'; import UserParsedQuery from './UserParsedQuery'; import LoadingStep from './LoadingStep'; type Props = { author: ChatMessageAuthor; text: string; parsedQuery?: ParsedQueryType[]; error?: string; threadId: string; queryId: string; responseTimestamp: string | null; showInlineFeedback: boolean; isLoading?: boolean; loadingSteps?: ChatLoadingStep[]; i: number; onMessageEdit: (queryId: string, i: number) => void; singleFileExplanation?: boolean; side: 'left' | 'right'; projectId: string; }; const ConversationMessage = ({ author, text, parsedQuery, i, queryId, onMessageEdit, singleFileExplanation, threadId, isLoading, loadingSteps, showInlineFeedback, responseTimestamp, error, side, projectId, }: Props) => { const { t } = useTranslation(); const { locale } = useContext(LocaleContext); const [isUpvote, setIsUpvote] = useState(false); const [isDownvote, setIsDownvote] = useState(false); const [isLoadingStepsShown, setLoadingStepsShown] = useState( getPlainFromStorage(LOADING_STEPS_SHOWN_KEY) ? !!Number(getPlainFromStorage(LOADING_STEPS_SHOWN_KEY)) : true, ); useEffect(() => { savePlainToStorage( LOADING_STEPS_SHOWN_KEY, isLoadingStepsShown ? '1' : '0', ); }, [isLoadingStepsShown]); const toggleStepsShown = useCallback(() => { setLoadingStepsShown((prev) => !prev); }, []); const handleEdit = useCallback(() => { onMessageEdit(queryId, i); }, [onMessageEdit, queryId, i]); const handleUpvote = useCallback(() => { setIsUpvote(true); setIsDownvote(false); return upvoteAnswer(projectId, threadId, queryId, { type: 'positive' }); }, [showInlineFeedback, threadId, queryId, projectId]); const handleDownvote = useCallback(() => { setIsUpvote(false); setIsDownvote(true); return upvoteAnswer(projectId, threadId, queryId, { type: 'negative', feedback: '', }); }, [showInlineFeedback, threadId, queryId, projectId]); return (
{error ? (

{error}

) : ( <>
{author === ChatMessageAuthor.User ? ( ) : isLoading ? ( ) : ( bloop )}
{(isUpvote || isDownvote) && (
{isUpvote ? ( ) : ( )}
)}
{author === ChatMessageAuthor.User ? You : 'bloop'} {author === ChatMessageAuthor.Server && (

·{' '} {isLoading ? ( Streaming response... ) : responseTimestamp ? ( format( new Date(responseTimestamp), 'hh:mm aa', getDateFnsLocale(locale), ) ) : null}

)} {author === ChatMessageAuthor.Server && ( )}
{!!loadingSteps?.length && (
{loadingSteps.map((s, i) => ( ))}
)}
{author === ChatMessageAuthor.Server ? ( ) : ( )}
{author === ChatMessageAuthor.User ? ( ) : ( !isLoading && ( <> ) )}
)}
); }; export default memo(ConversationMessage); ================================================ FILE: client/src/Project/CurrentTabContent/ChatTab/ScrollableContent.tsx ================================================ import { Fragment, memo, useContext, useEffect } from 'react'; import { Trans } from 'react-i18next'; import { ChatMessageAuthor, ChatMessageServer } from '../../../types/general'; import { WarningSignIcon } from '../../../icons'; import { ChatContext } from '../../../context/chatsContext'; import FunctionContext from '../../../components/ScrollToBottom/FunctionContext'; import StarterMessage from './StarterMessage'; import Message from './Message'; type Props = { chatData: ChatContext; side: 'left' | 'right'; projectId: string; }; const ScrollableContent = ({ chatData, side, projectId }: Props) => { const { scrollToBottom } = useContext(FunctionContext); useEffect(() => { if (chatData.submittedQuery.plain) { scrollToBottom({ behavior: 'smooth' }); } }, [chatData.submittedQuery]); return ( {(chatData.hideMessagesFrom === null ? chatData.conversation : chatData.conversation.slice(0, chatData.hideMessagesFrom + 1) ).map((m, i) => ( ))} {chatData.hideMessagesFrom !== null && (

Editing previously submitted questions will discard all answers and questions following it

)}
); }; export default memo(ScrollableContent); ================================================ FILE: client/src/Project/CurrentTabContent/ChatTab/StarterMessage.tsx ================================================ import { memo, useCallback, useContext, useEffect, useState } from 'react'; import { Trans, useTranslation } from 'react-i18next'; import { ChatBubblesIcon } from '../../../icons'; import { TutorialQuestionType } from '../../../types/api'; import { getTutorialQuestions } from '../../../services/api'; import { ProjectContext } from '../../../context/projectContext'; type Props = { isEmptyConversation: boolean; setInputValueImperatively: (v: string) => void; }; const StarterMessage = ({ isEmptyConversation, setInputValueImperatively, }: Props) => { useTranslation(); const [tutorials, setTutorials] = useState([]); const { project } = useContext(ProjectContext.Current); const getDiverseTutorials = useCallback(async () => { if (project?.repos.length) { const tutorials = []; let tutorialsPerRepo = Math.floor(10 / project.repos.length); let remainingTutorials = 10; for (const repo of project.repos) { const repoTutorials = await getTutorialQuestions(repo.repo.ref); const tutorialsToAdd = Math.min( tutorialsPerRepo, repoTutorials.questions.length, remainingTutorials, ); tutorials.push(...repoTutorials.questions.slice(0, tutorialsToAdd)); remainingTutorials -= tutorialsToAdd; if (remainingTutorials <= 0) { break; } } setTutorials(tutorials); } }, [project?.repos]); useEffect(() => { getDiverseTutorials(); }, [getDiverseTutorials]); return (
bloop

bloop

Hi, I am bloop! In{' '} Chat mode {' '} I can answer any questions related to any of your repositories.

{isEmptyConversation && !!tutorials.length && (

Below are a few suggestions you can ask me to get started:

)} {isEmptyConversation && !!tutorials.length && (
{tutorials.map((t, i) => ( ))}
)}
); }; export default memo(StarterMessage); ================================================ FILE: client/src/Project/CurrentTabContent/ChatTab/index.tsx ================================================ import React, { memo, useCallback, useContext, useEffect, useMemo, } from 'react'; import { useTranslation } from 'react-i18next'; import Button from '../../../components/Button'; import { ChatBubblesIcon, MoreHorizontalIcon, SplitViewIcon, } from '../../../icons'; import Dropdown from '../../../components/Dropdown'; import { checkEventKeys } from '../../../utils/keyboardUtils'; import useKeyboardNavigation from '../../../hooks/useKeyboardNavigation'; import { TabsContext } from '../../../context/tabsContext'; import { ChatTabType } from '../../../types/general'; import { ProjectContext } from '../../../context/projectContext'; import { CommandBarContext } from '../../../context/commandBarContext'; import { UIContext } from '../../../context/uiContext'; import { openInSplitViewShortcut } from '../../../consts/shortcuts'; import Conversation from './Conversation'; import ActionsDropdown from './ActionsDropdown'; type Props = ChatTabType & { noBorder?: boolean; side: 'left' | 'right'; tabKey: string; handleMoveToAnotherSide: () => void; }; const ChatTab = ({ noBorder, side, title, conversationId, tabKey, handleMoveToAnotherSide, }: Props) => { const { t } = useTranslation(); const { focusedPanel } = useContext(TabsContext.FocusedPanel); const { closeTab } = useContext(TabsContext.Handlers); const { isLeftSidebarFocused } = useContext(UIContext.Focus); const { setFocusedTabItems } = useContext(CommandBarContext.Handlers); const { project, refreshCurrentProjectConversations } = useContext( ProjectContext.Current, ); const dropdownComponentProps = useMemo(() => { return { handleMoveToAnotherSide, conversationId, projectId: project?.id, tabKey, closeTab, refreshCurrentProjectConversations, side, }; }, [ handleMoveToAnotherSide, conversationId, closeTab, project?.id, tabKey, refreshCurrentProjectConversations, side, ]); const handleKeyEvent = useCallback( (e: KeyboardEvent) => { if (checkEventKeys(e, openInSplitViewShortcut)) { handleMoveToAnotherSide(); } }, [handleMoveToAnotherSide], ); useKeyboardNavigation( handleKeyEvent, focusedPanel !== side || isLeftSidebarFocused, ); useEffect(() => { if (focusedPanel === side) { setFocusedTabItems([ { label: t('Open in split view'), Icon: SplitViewIcon, id: 'split_view', key: 'split_view', onClick: handleMoveToAnotherSide, closeOnClick: true, shortcut: openInSplitViewShortcut, footerHint: '', footerBtns: [{ label: t('Move'), shortcut: ['entr'] }], }, ]); } }, [focusedPanel, side, handleMoveToAnotherSide]); return (
{title || t('New conversation')}
{focusedPanel === side && ( )}
); }; export default memo(ChatTab); ================================================ FILE: client/src/Project/CurrentTabContent/DocTab/ActionsDropdown.tsx ================================================ import { memo } from 'react'; import { useTranslation } from 'react-i18next'; import DropdownSection from '../../../components/Dropdown/Section'; import SectionItem from '../../../components/Dropdown/Section/SectionItem'; import { SplitViewIcon, StudioCloseSignIcon, StudioPlusSignIcon, } from '../../../icons'; import { addToStudioShortcut, openInSplitViewShortcut, removeFromStudioShortcut, } from '../../../consts/shortcuts'; type Props = { handleMoveToAnotherSide: () => void; handleAddToStudio: () => void; handleRemoveFromStudio: () => void; isDocInContext: boolean; }; const ActionsDropdown = ({ handleMoveToAnotherSide, handleAddToStudio, handleRemoveFromStudio, isDocInContext, }: Props) => { const { t } = useTranslation(); return (
{isDocInContext ? ( } /> ) : ( } /> )} } />
); }; export default memo(ActionsDropdown); ================================================ FILE: client/src/Project/CurrentTabContent/DocTab/DocSection.tsx ================================================ import React, { Dispatch, memo, SetStateAction, useCallback } from 'react'; import { Trans } from 'react-i18next'; import Button from '../../../components/Button'; import { DocSectionType } from '../../../types/api'; import RenderedSection from './RenderedSection'; type Props = DocSectionType & { isSelected: boolean; isNothingSelected: boolean; isEditingSelection: boolean; setSelectedSections: Dispatch>; }; const DocSection = ({ text, isSelected, setSelectedSections, point_id, isNothingSelected, doc_source, isEditingSelection, }: Props) => { const setSelected = useCallback( (b: boolean) => { setSelectedSections((prev) => { if (b) { return [...prev, point_id]; } return prev.filter((r) => r !== point_id); }); }, [point_id, setSelectedSections], ); const handleClick = useCallback(() => { if (isEditingSelection) { setSelected(!isSelected); } }, [isSelected, isEditingSelection]); return (
{isEditingSelection && (
)}
); }; export default memo(DocSection); ================================================ FILE: client/src/Project/CurrentTabContent/DocTab/RenderedSection.tsx ================================================ import React, { memo, useCallback, useContext, useMemo } from 'react'; import { Remarkable } from 'remarkable'; import { highlightCode } from '../../../utils/prism'; import { DeviceContext } from '../../../context/deviceContext'; const md = new Remarkable({ html: false, highlight(str: string, lang: string): string { try { return highlightCode(str, lang); } catch (err) { console.log(err); return ''; } }, linkTarget: '__blank', }); type Props = { text: string; baseUrl: string; isEditingSelection: boolean; }; const RenderedSection = ({ text, baseUrl, isEditingSelection }: Props) => { const { openLink } = useContext(DeviceContext); const markdown = useMemo(() => md.render(text), [text]); const handleClick = useCallback( (e: React.MouseEvent) => { // @ts-ignore const href = e.target.getAttribute('href'); if (href) { e.preventDefault(); e.stopPropagation(); openLink( href.startsWith('http://') || href.startsWith('https://') ? href : new URL(href, baseUrl).href, ); } }, [openLink, baseUrl], ); return (
); }; export default memo(RenderedSection); ================================================ FILE: client/src/Project/CurrentTabContent/DocTab/index.tsx ================================================ import React, { memo, useCallback, useContext, useEffect, useMemo, useState, } from 'react'; import { Trans, useTranslation } from 'react-i18next'; import { CommandBarStepEnum, DocTabType } from '../../../types/general'; import { MagazineIcon, MoreHorizontalIcon, SplitViewIcon, StudioCloseSignIcon, StudioPlusSignIcon, } from '../../../icons'; import Dropdown from '../../../components/Dropdown'; import Button from '../../../components/Button'; import { TabsContext } from '../../../context/tabsContext'; import { checkEventKeys } from '../../../utils/keyboardUtils'; import useKeyboardNavigation from '../../../hooks/useKeyboardNavigation'; import { UIContext } from '../../../context/uiContext'; import { CommandBarContext } from '../../../context/commandBarContext'; import { addToStudioShortcut, escapeShortcut, openInSplitViewShortcut, removeFromStudioShortcut, saveShortcut, selectLinesShortcut, } from '../../../consts/shortcuts'; import { getCodeStudio, getDocSections, getDocTokenCount, getIndexedPages, patchCodeStudio, } from '../../../services/api'; import { CodeStudioType, DocPageType, DocSectionType, } from '../../../types/api'; import { findElementInCurrentTab } from '../../../utils/domUtils'; import { ProjectContext } from '../../../context/projectContext'; import Badge from '../../../components/Badge'; import { humanNumber } from '../../../utils'; import ActionsDropdown from './ActionsDropdown'; import DocSection from './DocSection'; type Props = DocTabType & { noBorder?: boolean; side: 'left' | 'right'; tabKey: string; handleMoveToAnotherSide: () => void; }; const DocTab = ({ side, tabKey, handleMoveToAnotherSide, docId, title, favicon, noBorder, relativeUrl, studioId, initialSections, isDocInContext, }: Props) => { const { t } = useTranslation(); const { focusedPanel } = useContext(TabsContext.FocusedPanel); const { updateTabProperty } = useContext(TabsContext.Handlers); const { isLeftSidebarFocused } = useContext(UIContext.Focus); const { setFocusedTabItems, setChosenStep, setIsVisible } = useContext( CommandBarContext.Handlers, ); const { isVisible: isCommandBarVisible } = useContext( CommandBarContext.General, ); const { project, refreshCurrentProjectStudios } = useContext( ProjectContext.Current, ); const [fullDoc, setFullDoc] = useState(null); const [studio, setStudio] = useState(null); const [sections, setSections] = useState([]); const [selectedSections, setSelectedSections] = useState( initialSections || [], ); const [tokenCount, setTokenCount] = useState(0); const [isEditingSelection, setIsEditingSelection] = useState(false); const refreshStudio = useCallback(() => { if (studioId && project?.id) { getCodeStudio(project.id, studioId).then(setStudio); } else { setStudio(null); } }, [studioId, project?.id]); useEffect(() => { refreshStudio(); }, [refreshStudio]); useEffect(() => { setSelectedSections(initialSections || []); }, [initialSections]); useEffect(() => { getIndexedPages(docId).then((resp) => { const doc = resp.find((p) => p.relative_url === relativeUrl); if (doc) { setFullDoc(doc); } }); getDocSections(docId, relativeUrl).then((resp) => { setSections(resp); }); }, [docId, relativeUrl]); useEffect(() => { if (project?.id) { getDocTokenCount(project.id, docId, relativeUrl, selectedSections).then( setTokenCount, ); } }, [selectedSections, relativeUrl, docId, project?.id]); useEffect(() => { if (initialSections?.length && sections.length) { const firstSelectedSection = initialSections?.length === 1 ? initialSections[0] : initialSections?.length ? sections.find((s) => initialSections?.includes(s.point_id)) ?.point_id : ''; findElementInCurrentTab( `[data-active="true"] [data-section-id="${firstSelectedSection}"]`, )?.scrollIntoView(); } }, [sections.length, initialSections]); const handleAddToStudio = useCallback(() => { setChosenStep({ id: CommandBarStepEnum.ADD_TO_STUDIO, data: { docId, relativeUrl, favicon, title }, }); setIsVisible(true); }, [docId, relativeUrl, favicon, title]); const handleRemoveFromStudio = useCallback(async () => { if (project?.id && studioId && studio) { const patchedDoc = studio?.doc_context.find( (d) => d.doc_id === docId && d.relative_url === relativeUrl, ); if (patchedDoc) { await patchCodeStudio(project.id, studioId, { doc_context: studio?.doc_context.filter( (d) => d.doc_id !== docId || d.relative_url !== relativeUrl, ), }); refreshCurrentProjectStudios(); refreshStudio(); setIsEditingSelection(false); updateTabProperty( tabKey, 'isDocInContext', false, side, ); updateTabProperty( tabKey, 'initialSections', undefined, side, ); updateTabProperty( tabKey, 'studioId', undefined, side, ); setStudio(null); setSelectedSections([]); } } }, [docId, relativeUrl, project?.id, studioId, studio]); const dropdownComponentProps = useMemo(() => { return { handleMoveToAnotherSide, handleAddToStudio, handleRemoveFromStudio, isDocInContext, }; }, [ handleMoveToAnotherSide, handleAddToStudio, handleRemoveFromStudio, isDocInContext, ]); useEffect(() => { if (focusedPanel === side) { setFocusedTabItems([ { label: t('Open in split view'), Icon: SplitViewIcon, id: 'split_view', key: 'split_view', onClick: handleMoveToAnotherSide, closeOnClick: true, shortcut: openInSplitViewShortcut, footerHint: '', footerBtns: [{ label: t('Move'), shortcut: ['entr'] }], }, ...(studioId ? [ { label: t('Remove from studio'), Icon: StudioCloseSignIcon, id: 'doc_from_studio', key: 'doc_from_studio', onClick: handleRemoveFromStudio, shortcut: removeFromStudioShortcut, footerHint: t('Remove page from code studio context'), footerBtns: [{ label: t('Remove'), shortcut: ['entr'] }], }, ] : [ { label: t('Add to studio'), Icon: StudioPlusSignIcon, id: 'doc_to_studio', key: 'doc_to_studio', onClick: handleAddToStudio, shortcut: addToStudioShortcut, footerHint: t('Add file to code studio context'), footerBtns: [{ label: t('Add'), shortcut: ['entr'] }], }, ]), ]); } }, [ focusedPanel, side, handleMoveToAnotherSide, handleRemoveFromStudio, handleAddToStudio, ]); const hasChanges = useMemo(() => { return ( (studioId && !isDocInContext) || JSON.stringify(initialSections) !== JSON.stringify(selectedSections) ); }, [studioId, isDocInContext, initialSections, selectedSections]); const handleEditRanges = useCallback(() => { setIsEditingSelection(true); }, []); useEffect(() => { if (studioId && !isDocInContext) { handleEditRanges(); } }, [studioId, isDocInContext, handleEditRanges]); const handleCancelStudio = useCallback(() => { setIsEditingSelection(false); if (isDocInContext) { setSelectedSections(initialSections || []); } else { setSelectedSections([]); updateTabProperty( tabKey, 'studioId', undefined, side, ); } }, [tabKey, side, isDocInContext, initialSections]); const handleSubmitToStudio = useCallback(async () => { if (project?.id && studioId && studio) { const patchedDoc = studio?.doc_context.find( (f) => f.doc_id === docId && f.doc_source === fullDoc?.doc_source && f.relative_url === relativeUrl, ); if (!patchedDoc) { await patchCodeStudio(project.id, studioId, { doc_context: [ ...(studio?.doc_context || []), { doc_id: docId, doc_source: fullDoc?.doc_source || '', doc_icon: favicon || '', doc_title: title || '', relative_url: relativeUrl, absolute_url: fullDoc?.absolute_url || '', ranges: selectedSections, hidden: false, }, ], }); } else { patchedDoc.ranges = selectedSections; const newContext = studio?.doc_context .filter( (f) => f.doc_id !== docId || f.doc_source !== fullDoc?.doc_source || f.relative_url !== relativeUrl, ) .concat(patchedDoc); await patchCodeStudio(project.id, studioId, { doc_context: newContext, }); } refreshCurrentProjectStudios(); refreshStudio(); setIsEditingSelection(false); updateTabProperty( tabKey, 'isDocInContext', true, side, ); updateTabProperty( tabKey, 'initialSections', selectedSections, side, ); } }, [ project?.id, studio, docId, relativeUrl, fullDoc, studioId, selectedSections, ]); const handleKeyEvent = useCallback( (e: KeyboardEvent) => { if (checkEventKeys(e, openInSplitViewShortcut)) { handleMoveToAnotherSide(); } else if (checkEventKeys(e, addToStudioShortcut)) { e.preventDefault(); e.stopPropagation(); handleAddToStudio(); } else if (checkEventKeys(e, removeFromStudioShortcut)) { e.preventDefault(); e.stopPropagation(); handleRemoveFromStudio(); } else if (checkEventKeys(e, escapeShortcut) && studioId) { e.preventDefault(); e.stopPropagation(); handleCancelStudio(); } else if (checkEventKeys(e, saveShortcut) && studioId) { e.preventDefault(); e.stopPropagation(); handleSubmitToStudio(); } else if ( checkEventKeys(e, selectLinesShortcut) && studioId && !isEditingSelection ) { e.preventDefault(); e.stopPropagation(); handleEditRanges(); } }, [ handleMoveToAnotherSide, handleAddToStudio, handleRemoveFromStudio, handleCancelStudio, studioId, handleSubmitToStudio, handleEditRanges, ], ); useKeyboardNavigation( handleKeyEvent, focusedPanel !== side || isLeftSidebarFocused || isCommandBarVisible, ); return (
{favicon ? ( {relativeUrl} ) : ( )} {title || relativeUrl}
{!!studio && studioId && (

1500 ? 'text-yellow' : tokenCount <= 1500 ? 'text-green' : 'text-red' } code-mini`} > {humanNumber(tokenCount)}{' '} # tokens

)}
{focusedPanel === side && (studioId && (hasChanges || isEditingSelection) ? (
{!isEditingSelection && ( <>
)}
) : ( studioId && (
) ))} {!isEditingSelection && ( )}
{sections.map((s) => { return ( ); })}
); }; export default memo(DocTab); ================================================ FILE: client/src/Project/CurrentTabContent/DropTarget.tsx ================================================ import { memo } from 'react'; import { useDrop } from 'react-dnd'; import { Trans, useTranslation } from 'react-i18next'; import { TabType } from '../../types/general'; import { SplitViewIcon } from '../../icons'; type Props = { onDrop: (t: TabType) => void; }; const DropTarget = ({ onDrop }: Props) => { useTranslation(); const [{ isOver, canDrop }, drop] = useDrop( () => ({ accept: 'tab-left', drop: (item: { t: TabType }, monitor) => { onDrop(item.t); }, collect: (monitor) => { return { isOver: !!monitor.isOver(), canDrop: !!monitor.canDrop(), }; }, }), [onDrop], ); return (
{isOver && canDrop && (

Release to open in split view

)}
); }; export default memo(DropTarget); ================================================ FILE: client/src/Project/CurrentTabContent/EmptyTab.tsx ================================================ import { memo } from 'react'; import { Trans, useTranslation } from 'react-i18next'; import useShortcuts from '../../hooks/useShortcuts'; type Props = {}; const EmptyTab = ({}: Props) => { useTranslation(); const shortcut = useShortcuts(['cmd']); return (
bloop

No file selected

Select a file or open a new tab to display it here.{' '} Press{' '} cmdKey {' '} K {' '} on your keyboard to open the Command bar.

); }; export default memo(EmptyTab); ================================================ FILE: client/src/Project/CurrentTabContent/FileTab/ActionsDropdown.tsx ================================================ import { memo } from 'react'; import { useTranslation } from 'react-i18next'; import DropdownSection from '../../../components/Dropdown/Section'; import SectionItem from '../../../components/Dropdown/Section/SectionItem'; import { SplitViewIcon, FileWithSparksIcon, StudioPlusSignIcon, StudioCloseSignIcon, } from '../../../icons'; import { addToStudioShortcut, openInSplitViewShortcut, explainFileShortcut, removeFromStudioShortcut, } from '../../../consts/shortcuts'; type Props = { handleExplain: () => void; handleMoveToAnotherSide: () => void; handleAddToStudio: () => void; handleRemoveFromStudio: () => void; isFileInContext: boolean; }; const ActionsDropdown = ({ handleExplain, handleMoveToAnotherSide, handleAddToStudio, handleRemoveFromStudio, isFileInContext, }: Props) => { const { t } = useTranslation(); return (
} /> {isFileInContext ? ( } /> ) : ( } /> )} } />
); }; export default memo(ActionsDropdown); ================================================ FILE: client/src/Project/CurrentTabContent/FileTab/index.tsx ================================================ import React, { memo, useCallback, useContext, useEffect, useMemo, useState, useTransition, } from 'react'; import { Trans, useTranslation } from 'react-i18next'; import { forceFileToBeIndexed, getCodeStudio, getFileContent, getFileTokenCount, getHoverables, patchCodeStudio, } from '../../../services/api'; import FileIcon from '../../../components/FileIcon'; import Button from '../../../components/Button'; import { EyeCutIcon, FileWithSparksIcon, MoreHorizontalIcon, SplitViewIcon, StudioCloseSignIcon, StudioPlusSignIcon, } from '../../../icons'; import { CodeStudioType, FileResponse } from '../../../types/api'; import { mapRanges } from '../../../mappers/results'; import { Range } from '../../../types/results'; import IpynbRenderer from '../../../components/IpynbRenderer'; import SpinLoaderContainer from '../../../components/Loaders/SpinnerLoader'; import { CommandBarStepEnum, FileTabType, SyncStatus, TabTypesEnum, } from '../../../types/general'; import { FileHighlightsContext } from '../../../context/fileHighlightsContext'; import Dropdown from '../../../components/Dropdown'; import { TabsContext } from '../../../context/tabsContext'; import { checkEventKeys } from '../../../utils/keyboardUtils'; import useKeyboardNavigation from '../../../hooks/useKeyboardNavigation'; import { CommandBarContext } from '../../../context/commandBarContext'; import BreadcrumbsPathContainer from '../../../components/Breadcrumbs/PathContainer'; import { RepositoriesContext } from '../../../context/repositoriesContext'; import { UIContext } from '../../../context/uiContext'; import { addToStudioShortcut, escapeShortcut, explainFileShortcut, openInSplitViewShortcut, removeFromStudioShortcut, saveShortcut, selectLinesShortcut, } from '../../../consts/shortcuts'; import { ProjectContext } from '../../../context/projectContext'; import Badge from '../../../components/Badge'; import { humanNumber } from '../../../utils'; import { findElementInCurrentTab } from '../../../utils/domUtils'; import CodeFullSelectable from '../../../components/Code/CodeFullSelectable'; import ActionsDropdown from './ActionsDropdown'; type Props = { tabKey: string; repoRef: string; path: string; scrollToLine?: string; tokenRange?: string; noBorder?: boolean; branch?: string | null; side: 'left' | 'right'; studioId?: string; initialRanges?: [number, number][]; isFileInContext?: boolean; isTemp?: boolean; handleMoveToAnotherSide: () => void; }; const FileTab = ({ path, noBorder, repoRef, scrollToLine, branch = null, side, tokenRange, handleMoveToAnotherSide, tabKey, studioId, initialRanges, isFileInContext, isTemp, }: Props) => { const { t } = useTranslation(); const [file, setFile] = useState(null); const [hoverableRanges, setHoverableRanges] = useState< Record | undefined >(undefined); const [indexRequested, setIndexRequested] = useState(false); const [isFetched, setIsFetched] = useState(false); const [studio, setStudio] = useState(null); const [selectedLines, setSelectedLines] = useState<[number, number][]>( initialRanges || [], ); const [isEditingRanges, setIsEditingRanges] = useState(false); const [tokenCount, setTokenCount] = useState(0); const { setFocusedTabItems, setIsVisible, setChosenStep } = useContext( CommandBarContext.Handlers, ); const { isVisible: isCommandBarVisible } = useContext( CommandBarContext.General, ); const [isPending, startTransition] = useTransition(); const { openNewTab, updateTabProperty } = useContext(TabsContext.Handlers); const { focusedPanel } = useContext(TabsContext.FocusedPanel); const { isLeftSidebarFocused } = useContext(UIContext.Focus); const { setOnBoardingState } = useContext(UIContext.Onboarding); const { project, refreshCurrentProjectStudios } = useContext( ProjectContext.Current, ); const { fileHighlights, hoveredLines } = useContext( FileHighlightsContext.Values, ); const { indexingStatus } = useContext(RepositoriesContext); const refreshStudio = useCallback(() => { if (studioId && project?.id) { getCodeStudio(project.id, studioId).then(setStudio); } else { setStudio(null); } }, [studioId, project?.id]); useEffect(() => { refreshStudio(); }, [refreshStudio]); const highlights = useMemo(() => { return fileHighlights[path]?.sort((a, b) => a && b && a?.lines?.[1] - a?.lines?.[0] < b?.lines?.[1] - b?.lines?.[0] ? -1 : 1, ); }, [path, fileHighlights]); useEffect(() => { setIndexRequested(false); setIsFetched(false); }, [path, repoRef]); const refetchFile = useCallback(async () => { try { setSelectedLines(initialRanges || []); const resp = await getFileContent(repoRef, path, branch); if (!resp) { setIsFetched(true); return; } startTransition(() => { setFile(resp); setIsFetched(true); }); if (initialRanges) { setTimeout( () => { const line = findElementInCurrentTab( `[data-active="true"] [data-line-number="${ initialRanges?.[0] ? initialRanges[0][0] : 0 }"]`, ); line?.scrollIntoView({ behavior: 'auto', block: !!initialRanges?.[0] && initialRanges[0][0] > 1 && initialRanges[0][1] - initialRanges[0][0] > 5 ? 'start' : 'center', }); }, !initialRanges?.[0] ? 100 : initialRanges[0][0] > 1000 ? 1000 : initialRanges[0][0] > 500 ? 800 : 500, ); } // if (item.indexed) { const data = await getHoverables(path, repoRef, branch); setHoverableRanges(mapRanges(data.ranges)); // } } catch (err) { setIsFetched(true); } }, [repoRef, path, branch]); useEffect(() => { refetchFile(); }, [refetchFile]); useEffect(() => { if (project?.id) { const mappedLines: [number, number][] = selectedLines.map((r) => [ r[0], r[1] + 1, ]); getFileTokenCount( project.id, path, repoRef, branch || undefined, mappedLines, ).then(setTokenCount); } }, [path, repoRef, branch, selectedLines]); const handleEditRanges = useCallback(() => { setIsEditingRanges(true); }, []); useEffect(() => { if (studioId && !isFileInContext) { handleEditRanges(); } }, [studioId, isFileInContext, handleEditRanges]); const handleCancelStudio = useCallback(() => { setIsEditingRanges(false); if (isFileInContext) { setSelectedLines(initialRanges || []); } else { setSelectedLines([]); updateTabProperty( tabKey, 'studioId', undefined, side, ); } }, [tabKey, side, isFileInContext, initialRanges]); useEffect(() => { if (indexingStatus[repoRef]?.status === SyncStatus.Done) { setTimeout(refetchFile, 2000); } }, [indexingStatus[repoRef]?.status]); const onIndexRequested = useCallback(async () => { if (path) { setIndexRequested(true); await forceFileToBeIndexed(repoRef, path); setTimeout(() => refetchFile(), 1000); } }, [repoRef, path]); const handleClick = useCallback(() => { if (isTemp) { updateTabProperty(tabKey, 'isTemp', false, side); } }, [updateTabProperty, tabKey, side, isTemp]); const linesNumber = useMemo(() => { return file?.contents?.split(/\n(?!$)/g).length || 0; }, [file?.contents]); const handleExplain = useCallback(() => { openNewTab( { type: TabTypesEnum.CHAT, initialQuery: { path, repoRef, branch, lines: [0, linesNumber - 1], }, }, side === 'left' ? 'right' : 'left', ); setOnBoardingState((prev) => prev.isFileExplained ? prev : { ...prev, isFileExplained: true }, ); }, [path, repoRef, branch, linesNumber, side, openNewTab]); const handleAddToStudio = useCallback(() => { setChosenStep({ id: CommandBarStepEnum.ADD_TO_STUDIO, data: { path, repoRef, branch }, }); setIsVisible(true); }, [path, repoRef, branch]); const handleRemoveFromStudio = useCallback(async () => { if (project?.id && studioId && studio) { const patchedFile = studio?.context.find( (f) => f.path === path && f.repo === repoRef && f.branch === branch, ); if (patchedFile) { await patchCodeStudio(project.id, studioId, { context: studio?.context.filter( (f) => f.path !== path || f.repo !== repoRef || f.branch !== branch, ), }); refreshCurrentProjectStudios(); refreshStudio(); setIsEditingRanges(false); updateTabProperty( tabKey, 'isFileInContext', false, side, ); updateTabProperty( tabKey, 'initialRanges', undefined, side, ); updateTabProperty( tabKey, 'studioId', undefined, side, ); setStudio(null); setSelectedLines([]); } } }, [path, repoRef, branch, project?.id, studioId, studio]); const handleSubmitToStudio = useCallback(async () => { if (project?.id && studioId && studio) { const patchedFile = studio?.context.find( (f) => f.path === path && f.repo === repoRef && f.branch === branch, ); const mappedRanges = selectedLines.map((r) => ({ start: r[0], end: r[1] + 1, })); if (!patchedFile) { await patchCodeStudio(project.id, studioId, { context: [ ...(studio?.context || []), { path, branch: branch || null, repo: repoRef, hidden: false, ranges: mappedRanges || [], }, ], }); } else { patchedFile.ranges = mappedRanges; const newContext = studio?.context .filter( (f) => f.path !== path || f.repo !== repoRef || f.branch !== branch, ) .concat(patchedFile); await patchCodeStudio(project.id, studioId, { context: newContext, }); } refreshCurrentProjectStudios(); refreshStudio(); setIsEditingRanges(false); updateTabProperty( tabKey, 'isFileInContext', true, side, ); updateTabProperty( tabKey, 'initialRanges', selectedLines, side, ); } }, [project?.id, studio, path, repoRef, branch, selectedLines, studioId]); const hasChanges = useMemo(() => { return ( (studioId && !isFileInContext) || JSON.stringify(initialRanges) !== JSON.stringify(selectedLines) ); }, [studioId, isFileInContext, initialRanges, selectedLines]); const handleKeyEvent = useCallback( (e: KeyboardEvent) => { if (checkEventKeys(e, explainFileShortcut)) { handleExplain(); } else if (checkEventKeys(e, openInSplitViewShortcut)) { handleMoveToAnotherSide(); } else if (checkEventKeys(e, addToStudioShortcut)) { e.preventDefault(); e.stopPropagation(); handleAddToStudio(); } else if (checkEventKeys(e, removeFromStudioShortcut)) { e.preventDefault(); e.stopPropagation(); handleRemoveFromStudio(); } else if (checkEventKeys(e, escapeShortcut) && studioId) { e.preventDefault(); e.stopPropagation(); handleCancelStudio(); } else if (checkEventKeys(e, saveShortcut) && studioId) { e.preventDefault(); e.stopPropagation(); handleSubmitToStudio(); } else if ( checkEventKeys(e, selectLinesShortcut) && studioId && !isEditingRanges ) { e.preventDefault(); e.stopPropagation(); handleEditRanges(); } }, [ handleExplain, handleMoveToAnotherSide, handleAddToStudio, handleCancelStudio, studioId, handleSubmitToStudio, handleEditRanges, ], ); useKeyboardNavigation( handleKeyEvent, !file?.contents || focusedPanel !== side || isLeftSidebarFocused || isCommandBarVisible, ); useEffect(() => { if (focusedPanel === side && file?.contents) { setFocusedTabItems([ { label: t('Explain file'), Icon: FileWithSparksIcon, id: 'explain_file', key: 'explain_file', onClick: handleExplain, closeOnClick: true, shortcut: explainFileShortcut, footerHint: '', footerBtns: [{ label: t('Explain'), shortcut: ['entr'] }], }, ...(studioId && isFileInContext ? [ { label: t('Remove from studio'), Icon: StudioCloseSignIcon, id: 'file_from_studio', key: 'file_from_studio', onClick: handleRemoveFromStudio, shortcut: removeFromStudioShortcut, footerHint: t('Remove file from code studio context'), footerBtns: [{ label: t('Remove'), shortcut: ['entr'] }], }, ] : [ { label: t('Add to studio'), Icon: StudioPlusSignIcon, id: 'file_to_studio', key: 'file_to_studio', onClick: studioId ? handleSubmitToStudio : handleAddToStudio, shortcut: studioId ? saveShortcut : addToStudioShortcut, closeOnClick: !!studio, footerHint: t('Add file to code studio context'), footerBtns: [{ label: t('Add'), shortcut: ['entr'] }], }, ]), { label: t('Open in split view'), Icon: SplitViewIcon, id: 'split_view', key: 'split_view', onClick: handleMoveToAnotherSide, closeOnClick: true, shortcut: openInSplitViewShortcut, footerHint: '', footerBtns: [{ label: t('Move'), shortcut: ['entr'] }], }, ]); } }, [ focusedPanel, side, file?.contents, handleExplain, handleMoveToAnotherSide, handleAddToStudio, ]); const dropdownComponentProps = useMemo(() => { return { handleExplain, handleMoveToAnotherSide, handleAddToStudio, handleRemoveFromStudio, isFileInContext, }; }, [ handleExplain, handleMoveToAnotherSide, handleAddToStudio, handleRemoveFromStudio, isFileInContext, ]); return (
{!!studio && studioId && (

1500 ? 'text-yellow' : tokenCount <= 1500 ? 'text-green' : 'text-red' } code-mini flex-shrink-0`} > {humanNumber(tokenCount)}{' '} # tokens

)}
{focusedPanel === side && (studioId && (hasChanges || isEditingRanges) ? (
{!isEditingRanges && ( <>
)}
) : ( studioId && ( ) ))} {!isEditingRanges && ( )}
{file?.lang === 'jupyter notebook' ? ( ) : file ? ( ) : isFetched && !file ? (

File not indexed

This might be because the file is too big or it has one of bloop's excluded file types.

{!indexRequested ? ( ) : (
)}
) : null}
); }; export default memo(FileTab); ================================================ FILE: client/src/Project/CurrentTabContent/Header/AddTabButton.tsx ================================================ import React, { memo, useCallback, useContext, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { PlusSignIcon } from '../../../icons'; import Button from '../../../components/Button'; import { checkEventKeys } from '../../../utils/keyboardUtils'; import { TabTypesEnum } from '../../../types/general'; import useKeyboardNavigation from '../../../hooks/useKeyboardNavigation'; import { TabsContext } from '../../../context/tabsContext'; import Dropdown from '../../../components/Dropdown'; import { newChatTabShortcut, newStudioTabShortcut, } from '../../../consts/shortcuts'; import { postCodeStudio } from '../../../services/api'; import { ProjectContext } from '../../../context/projectContext'; import AddTabDropdown from './AddTabDropdown'; type Props = { tabsLength: number; side: 'left' | 'right'; focusedPanel: 'left' | 'right'; }; const AddTabButton = ({ side, focusedPanel, tabsLength }: Props) => { const { t } = useTranslation(); const { openNewTab } = useContext(TabsContext.Handlers); const { refreshCurrentProjectStudios, project } = useContext( ProjectContext.Current, ); const dropdownComponentProps = useMemo(() => { return { side }; }, [side]); const openChatTab = useCallback(() => { openNewTab({ type: TabTypesEnum.CHAT }, side); }, [openNewTab, side]); const openStudioTab = useCallback(async () => { if (project?.id) { const newId = await postCodeStudio(project?.id); refreshCurrentProjectStudios(); openNewTab({ type: TabTypesEnum.STUDIO, studioId: newId }, side); } }, [openNewTab, side, project?.id]); const handleKeyEvent = useCallback( (e: KeyboardEvent) => { if (checkEventKeys(e, newChatTabShortcut)) { e.stopPropagation(); e.preventDefault(); openChatTab(); } else if (checkEventKeys(e, newStudioTabShortcut)) { e.stopPropagation(); e.preventDefault(); openStudioTab(); } }, [openNewTab], ); useKeyboardNavigation(handleKeyEvent, side !== focusedPanel); return ( 1 ? 'bottom-end' : 'bottom-start'} > ); }; export default memo(AddTabButton); ================================================ FILE: client/src/Project/CurrentTabContent/Header/AddTabDropdown.tsx ================================================ import React, { memo, useCallback, useContext } from 'react'; import { useTranslation } from 'react-i18next'; import SectionItem from '../../../components/Dropdown/Section/SectionItem'; import { ChatBubblesIcon, CodeStudioIcon } from '../../../icons'; import { TabsContext } from '../../../context/tabsContext'; import { TabTypesEnum } from '../../../types/general'; import { postCodeStudio } from '../../../services/api'; import { ProjectContext } from '../../../context/projectContext'; import { newChatTabShortcut, newStudioTabShortcut, } from '../../../consts/shortcuts'; type Props = { side: 'left' | 'right'; }; const AddTabDropdown = ({ side }: Props) => { const { t } = useTranslation(); const { openNewTab } = useContext(TabsContext.Handlers); const { refreshCurrentProjectStudios, project } = useContext( ProjectContext.Current, ); const openChatTab = useCallback(() => { openNewTab({ type: TabTypesEnum.CHAT }, side); }, [openNewTab, side]); const openStudioTab = useCallback(async () => { if (project?.id) { const newId = await postCodeStudio(project?.id); refreshCurrentProjectStudios(); openNewTab({ type: TabTypesEnum.STUDIO, studioId: newId }, side); } }, [openNewTab, side, project?.id]); return (
} label={t('New conversation')} shortcut={newChatTabShortcut} onClick={openChatTab} index={'chat'} /> } label={t('New studio conversation')} shortcut={newStudioTabShortcut} onClick={openStudioTab} index={'studio'} />
); }; export default memo(AddTabDropdown); ================================================ FILE: client/src/Project/CurrentTabContent/Header/TabButton.tsx ================================================ import React, { memo, MouseEvent, useCallback, useContext, useRef, } from 'react'; import { useTranslation } from 'react-i18next'; import { useDrag, useDrop } from 'react-dnd'; import { DraggableTabItem, TabType, TabTypesEnum, } from '../../../types/general'; import FileIcon from '../../../components/FileIcon'; import { splitPath } from '../../../utils'; import Button from '../../../components/Button'; import { ChatBubblesIcon, CloseSignIcon, CodeStudioIcon, MagazineIcon, } from '../../../icons'; import { TabsContext } from '../../../context/tabsContext'; import { closeTabShortcut } from '../../../consts/shortcuts'; type Props = TabType & { tabKey: string; isActive: boolean; side: 'left' | 'right'; isOnlyTab: boolean; moveTab: ( i: number, j: number, sourceSide: 'left' | 'right', targetSide: 'left' | 'right', ) => void; i: number; repoRef?: string; path?: string; title?: string; branch?: string | null; scrollToLine?: string; tokenRange?: string; focusedPanel: 'left' | 'right'; isTemp?: boolean; studioId?: string; initialRanges?: [number, number][]; isFileInContext?: boolean; isDocInContext?: boolean; initialSections?: string[]; conversationId?: string; initialQuery?: { path: string; lines: [number, number]; repoRef: string; branch?: string | null | undefined; }; docId?: string; favicon?: string; relativeUrl?: string; }; const TabButton = ({ isActive, tabKey, repoRef, path, type, title, side, moveTab, isOnlyTab, i, branch, scrollToLine, tokenRange, focusedPanel, isTemp, studioId, initialRanges, initialSections, isFileInContext, isDocInContext, conversationId, initialQuery, relativeUrl, favicon, docId, }: Props) => { const { t } = useTranslation(); const { closeTab, setActiveLeftTab, setActiveRightTab, setFocusedPanel } = useContext(TabsContext.Handlers); const ref = useRef(null); const [{ handlerId }, drop] = useDrop({ accept: [`tab-left`, `tab-right`], canDrop: (item: DraggableTabItem) => true, collect(monitor) { return { handlerId: monitor.getHandlerId(), }; }, hover(item: DraggableTabItem, monitor) { if (!ref.current) { return; } const dragIndex = item.index; const hoverIndex = i; const sourceSide = item.side as 'left' | 'right'; const targetSide = side as 'left' | 'right'; // Don't replace items with themselves if (dragIndex === hoverIndex && sourceSide === targetSide) { return; } // Determine rectangle on screen const hoverBoundingRect = ref.current?.getBoundingClientRect(); // Get vertical middle const hoverMiddleX = (hoverBoundingRect.right - hoverBoundingRect.left) / 2; // Determine mouse position const clientOffset = monitor.getClientOffset(); // Get pixels to the left const hoverClientX = (clientOffset?.x || 0) - hoverBoundingRect.left; // Only perform the move when the mouse has crossed half of the items width // When dragging left, only move when the cursor is below 50% // When dragging right, only move when the cursor is above 50% // Dragging left if ( dragIndex < hoverIndex && hoverClientX < hoverMiddleX && sourceSide === targetSide ) { return; } // Dragging right if ( dragIndex > hoverIndex && hoverClientX > hoverMiddleX && sourceSide === targetSide ) { return; } // Time to actually perform the action moveTab(dragIndex, hoverIndex, sourceSide, targetSide); // Note: we're mutating the monitor item here! // Generally it's better to avoid mutations, // but it's good here for the sake of performance // to avoid expensive index searches. item.index = hoverIndex; if (sourceSide !== targetSide) { item.side = targetSide; } }, }); const [{ isDragging }, drag] = useDrag({ type: `tab-${side}`, canDrag: side !== 'left' || !isOnlyTab, item: (): DraggableTabItem => { return { id: tabKey, index: i, // @ts-ignore t: { key: tabKey, repoRef: repoRef!, path: path!, type, title, branch, scrollToLine, tokenRange, studioId, initialRanges, initialSections, isFileInContext, isDocInContext, conversationId, initialQuery, favicon, relativeUrl, docId, }, side, }; }, collect: (monitor) => ({ isDragging: monitor.isDragging(), }), }); drag(drop(ref)); const handleClose = useCallback( (e: MouseEvent) => { e.stopPropagation(); closeTab(tabKey, side); }, [tabKey, side], ); const handleClick = useCallback(() => { const setAction = side === 'left' ? setActiveLeftTab : setActiveRightTab; // @ts-ignore setAction({ path, repoRef, key: tabKey, type, title, branch, scrollToLine, tokenRange, studioId, initialRanges, initialSections, isFileInContext, isDocInContext, conversationId, initialQuery, docId, relativeUrl, favicon, }); setFocusedPanel(side); }, [ path, repoRef, tabKey, side, branch, scrollToLine, tokenRange, title, studioId, initialRanges, initialSections, isFileInContext, isDocInContext, conversationId, initialQuery, ]); return ( {type === TabTypesEnum.FILE ? ( ) : type === TabTypesEnum.CHAT ? ( ) : type === TabTypesEnum.STUDIO ? ( ) : favicon ? ( {title} ) : ( )}

{type === TabTypesEnum.FILE ? splitPath(path).pop() : type === TabTypesEnum.CHAT ? title || t('New conversation') : type === TabTypesEnum.STUDIO ? title || t('New studio conversation') : title || relativeUrl}

); }; export default memo(TabButton); ================================================ FILE: client/src/Project/CurrentTabContent/Header/index.tsx ================================================ import React, { memo, useCallback, useContext, useMemo } from 'react'; import HeaderRightPart from '../../../components/Header/HeaderRightPart'; import { TabsContext } from '../../../context/tabsContext'; import { TabType, TabTypesEnum } from '../../../types/general'; import AddTabButton from './AddTabButton'; import TabButton from './TabButton'; type Props = { side: 'left' | 'right'; }; const ProjectHeader = ({ side }: Props) => { const { leftTabs, rightTabs } = useContext(TabsContext.All); const { focusedPanel } = useContext(TabsContext.FocusedPanel); const { tab } = useContext( TabsContext[side === 'left' ? 'CurrentLeft' : 'CurrentRight'], ); const { setLeftTabs, setRightTabs, setActiveRightTab, setActiveLeftTab } = useContext(TabsContext.Handlers); const tabs = useMemo(() => { return side === 'left' ? leftTabs : rightTabs; }, [side, rightTabs, leftTabs]); const moveTab = useCallback( ( dragIndex: number, hoverIndex: number, sourceSide: 'left' | 'right', targetSide: 'left' | 'right', ) => { if (sourceSide === targetSide) { const action = side === 'left' ? setLeftTabs : setRightTabs; action((prevTabs) => { const newTabs = JSON.parse(JSON.stringify(prevTabs)); newTabs.splice(dragIndex, 1); const newTab = prevTabs[dragIndex]; newTabs.splice( hoverIndex, 0, newTab.type === TabTypesEnum.FILE && newTab.isTemp ? { ...newTab, isTemp: false } : newTab, ); return newTabs; }); } else { const sourceAction = sourceSide === 'left' ? setLeftTabs : setRightTabs; const sourceTabAction = sourceSide === 'left' ? setActiveLeftTab : setActiveRightTab; const targetAction = targetSide === 'left' ? setLeftTabs : setRightTabs; const targetTabAction = targetSide === 'left' ? setActiveLeftTab : setActiveRightTab; sourceAction((prevSourceTabs) => { const newSourceTabs = JSON.parse(JSON.stringify(prevSourceTabs)); const [movedTab] = newSourceTabs.splice(dragIndex, 1); sourceTabAction( newSourceTabs.length ? newSourceTabs[dragIndex - 1] || newSourceTabs[0] : null, ); targetAction((prevTargetTabs) => { const newTargetTabs = JSON.parse(JSON.stringify(prevTargetTabs)); if (!newTargetTabs.find((t: TabType) => t.key === movedTab.key)) { newTargetTabs.splice(hoverIndex, 0, movedTab); } targetTabAction(movedTab); return newTargetTabs; }); return newSourceTabs; }); } }, [side], ); return (
{tabs.map(({ key, ...t }, i) => ( ))} {!!tabs.length &&
}
{(side === 'right' || !rightTabs.length) && (
)}
); }; export default memo(ProjectHeader); ================================================ FILE: client/src/Project/CurrentTabContent/StudioTab/ActionsDropdown.tsx ================================================ import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import DropdownSection from '../../../components/Dropdown/Section'; import SectionItem from '../../../components/Dropdown/Section/SectionItem'; import { BroomIcon, SplitViewIcon, TrashCanIcon } from '../../../icons'; import { deleteCodeStudio } from '../../../services/api'; import { openInSplitViewShortcut } from '../../../consts/shortcuts'; type Props = { clearConversation: () => void; handleMoveToAnotherSide: () => void; refreshCurrentProjectStudios: () => void; closeTab: (tabKey: string, side: 'left' | 'right') => void; studioId?: string; projectId?: string; tabKey: string; side: 'left' | 'right'; }; const ActionsDropdown = ({ handleMoveToAnotherSide, refreshCurrentProjectStudios, clearConversation, studioId, projectId, closeTab, tabKey, side, }: Props) => { const { t } = useTranslation(); const removeConversation = useCallback(async () => { if (projectId && studioId) { await deleteCodeStudio(projectId, studioId); refreshCurrentProjectStudios(); closeTab(tabKey, side); } }, [ projectId, studioId, closeTab, refreshCurrentProjectStudios, tabKey, side, ]); return (
} /> {studioId && ( } /> )} {studioId && ( } /> )}
); }; export default memo(ActionsDropdown); ================================================ FILE: client/src/Project/CurrentTabContent/StudioTab/Conversation/ContextError.tsx ================================================ import { memo } from 'react'; import { Trans } from 'react-i18next'; import { WarningSignIcon } from '../../../../icons'; type Props = {}; const ContextError = ({}: Props) => { return (

We can’t generate a response because some files have a missing source in your Context files.

); }; export default memo(ContextError); ================================================ FILE: client/src/Project/CurrentTabContent/StudioTab/Conversation/GeneratedDiff.tsx ================================================ import { Dispatch, memo, SetStateAction, useCallback, useContext } from 'react'; import { Trans, useTranslation } from 'react-i18next'; import { GeneratedCodeDiff } from '../../../../types/api'; import { BranchIcon, WarningSignIcon } from '../../../../icons'; import CodeDiff from '../../../../components/Code/CodeDiff'; type Props = { diff: GeneratedCodeDiff; onDiffRemoved: (i: number) => void; onDiffChanged: (i: number, p: string) => void; applyError?: boolean; }; const GeneratedDiff = ({ diff, onDiffRemoved, onDiffChanged, applyError, }: Props) => { useTranslation(); // const { repositories } = useContext(RepositoriesContext); // const onDiffClick = useCallback( // (chunk: DiffChunkType) => { // const repoFull = repositories?.find((r) => r.ref === chunk.repo); // if (repoFull) { // setLeftPanel({ // type: StudioLeftPanelType.DIFF, // data: { // filePath: chunk.file, // repo: repoFull, // branch: chunk.branch, // hunks: chunk.hunks, // }, // }); // } // }, // [repositories], // ); const onDiffClick = useCallback(() => {}, []); return (
{applyError ? ( ) : ( )} {applyError ? 'Failed to apply the diff' : 'Generated diffs to be applied'}

{diff.chunks.find((c) => c.repo.startsWith('local//')) ? ( The following changes can be applied to your repository. Make sure the generated diffs are valid before you apply the changes. ) : ( The following changes represent the git diff for the remote repository. Please note that these changes cannot be applied directly to a remote repository. Use the "Copy" button to copy the changes and apply them locally. )}

{diff.chunks.map((d, i) => ( ))}
); }; export default memo(GeneratedDiff); ================================================ FILE: client/src/Project/CurrentTabContent/StudioTab/Conversation/Input/TemplatesDropdown.tsx ================================================ import { ChangeEvent, MouseEvent, memo, useCallback, useContext, useEffect, useState, useRef, } from 'react'; import { useTranslation } from 'react-i18next'; import DropdownSection from '../../../../../components/Dropdown/Section'; import { StudioTemplateType } from '../../../../../types/api'; import SectionItem from '../../../../../components/Dropdown/Section/SectionItem'; import { CogIcon, TemplatesIcon } from '../../../../../icons'; import { UIContext } from '../../../../../context/uiContext'; import { ProjectSettingSections } from '../../../../../types/general'; import { ArrowNavigationContext } from '../../../../../context/arrowNavigationContext'; type Props = { templates: StudioTemplateType[]; onTemplateSelected: (t: string) => void; handleClose: () => void; }; const TemplatesDropdown = ({ templates, onTemplateSelected }: Props) => { const { t } = useTranslation(); const [inputValue, setInputValue] = useState(''); const [templatesToShow, setTemplatesToShow] = useState(templates); const { setProjectSettingsOpen, setProjectSettingsSection } = useContext( UIContext.ProjectSettings, ); const { focusedIndex } = useContext(ArrowNavigationContext); const inputRef = useRef(null); useEffect(() => { if (!inputValue) { setTemplatesToShow(templates); } else { setTemplatesToShow( templates.filter( (t) => t.name?.toLowerCase().includes(inputValue?.toLowerCase()), ), ); } }, [templates, inputValue]); const handleChange = useCallback((e: ChangeEvent) => { setInputValue(e.target.value); }, []); const handleManage = useCallback(() => { setProjectSettingsSection(ProjectSettingSections.TEMPLATES); setProjectSettingsOpen(true); }, []); const noPropagate = useCallback((e: MouseEvent) => { e.stopPropagation(); }, []); useEffect(() => { if (focusedIndex === 'search-templates') { inputRef.current?.focus(); } }, [focusedIndex]); return (
{templatesToShow.map((t) => ( onTemplateSelected(t.content)} icon={} /> ))} } onClick={handleManage} index={'manage-templ'} />
); }; export default memo(TemplatesDropdown); ================================================ FILE: client/src/Project/CurrentTabContent/StudioTab/Conversation/Input/index.tsx ================================================ import React, { ChangeEvent, memo, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState, } from 'react'; import { Trans, useTranslation } from 'react-i18next'; import { StudioConversationMessageAuthor } from '../../../../../types/general'; import MarkdownWithCode from '../../../../../components/MarkdownWithCode'; import { PencilIcon, PersonIcon, RefreshIcon, TemplatesIcon, TrashCanIcon, WarningSignIcon, } from '../../../../../icons'; import Button from '../../../../../components/Button'; import CopyButton from '../../../../../components/MarkdownWithCode/CopyButton'; import { StudioTemplateType } from '../../../../../types/api'; import Dropdown from '../../../../../components/Dropdown'; import { useTemplateShortcut } from '../../../../../consts/shortcuts'; import SpinLoaderContainer from '../../../../../components/Loaders/SpinnerLoader'; import TemplatesDropdown from './TemplatesDropdown'; type Props = { author: StudioConversationMessageAuthor; message: string; isLoading?: boolean; onMessageChange: (m: string, i?: number) => void; onMessageRemoved?: (i: number, andSubsequent?: boolean) => void; i?: number; inputRef?: React.MutableRefObject; templatesRef?: React.MutableRefObject; isTokenLimitExceeded: boolean; isLast: boolean; isActiveTab: boolean; side: 'left' | 'right'; templates?: StudioTemplateType[]; setIsDropdownShown: (b: boolean) => void; }; const ConversationInput = ({ author, message, onMessageChange, i, onMessageRemoved, inputRef, isLast, isTokenLimitExceeded, side, templates, setIsDropdownShown, templatesRef, isActiveTab, isLoading, }: Props) => { const { t } = useTranslation(); const [isFocused, setFocused] = useState(i === undefined); const [value, setValue] = useState(message); const ref = useRef(null); const cloneRef = useRef(null); useImperativeHandle(inputRef, () => ref.current!); const handleChange = useCallback( (e: ChangeEvent) => { if (isActiveTab) { if (i === undefined) { onMessageChange(e.target.value, i); } setValue(e.target.value); } }, [i, onMessageChange, isActiveTab], ); useEffect(() => { if (isFocused) { setValue(message); } }, [isFocused, message]); useEffect(() => { if (i === undefined) { setFocused(true); } }, [i, message]); const handleBlur = useCallback(() => { setTimeout(() => setFocused(false), 100); // to allow press on top buttons if (i !== undefined) { onMessageChange(value, i); } }, [onMessageChange, value, i]); useEffect(() => { if (!isActiveTab) { inputRef?.current?.blur(); } else { inputRef?.current?.focus(); } }, [isActiveTab]); useEffect(() => { if (ref.current && cloneRef.current) { cloneRef.current.style.height = '19px'; const scrollHeight = cloneRef.current.scrollHeight; // We then set the height directly, outside of the render loop // Trying to set this with state or a ref will product an incorrect value. ref.current.style.height = Math.min(Math.max(scrollHeight, 19), 300) + 'px'; } }, [message, isFocused, value]); const dropdownProps = useMemo(() => { return { templates, onTemplateSelected: onMessageChange, }; }, [templates, onMessageChange, i]); return (
{author === StudioConversationMessageAuthor.USER ? (
) : (
{isLoading ? ( ) : ( bloop )}
)}

{author === StudioConversationMessageAuthor.USER ? ( You ) : ( 'bloop' )}

{i === undefined && ( )}
{isFocused || i === undefined ? ( <>