This file is a merged representation of a subset of the codebase, containing specifically included files, combined into a single document by Repomix.
The content has been processed where content has been compressed (code blocks are separated by ⋮---- delimiter).

================================================================
File Summary
================================================================

Purpose:
--------
This file contains a packed representation of a subset of the repository's contents that is considered the most important context.
It is designed to be easily consumable by AI systems for analysis, code review,
or other automated processes.

File Format:
------------
The content is organized as follows:
1. This summary section
2. Repository information
3. Directory structure
4. Repository files (if enabled)
5. Multiple file entries, each consisting of:
  a. A separator line (================)
  b. The file path (File: path/to/file)
  c. Another separator line
  d. The full contents of the file
  e. A blank line

Usage Guidelines:
-----------------
- This file should be treated as read-only. Any changes should be made to the
  original repository files, not this packed version.
- When processing this file, use the file path to distinguish
  between different files in the repository.
- Be aware that this file may contain sensitive information. Handle it with
  the same level of security as you would the original repository.

Notes:
------
- Some files may have been excluded based on .gitignore rules and Repomix's configuration
- Binary files are not included in this packed representation. Please refer to the Repository Structure section for a complete list of file paths, including binary files
- Only files matching these patterns are included: src/**/*, resources/connector/**, scripts/**, electron-builder.yml, electron.vite.config.ts, tailwind.config.js, package.json
- Files matching patterns in .gitignore are excluded
- Files matching default ignore patterns are excluded
- Content has been compressed - code blocks are separated by ⋮---- delimiter
- Files are sorted by Git change count (files with more changes are at the bottom)


================================================================
Directory Structure
================================================================
resources/
  connector/
    connector.py
scripts/
  after-pack.js
  before-pack.js
  download-probe.mjs
  test-proxy-server.mjs
  validate-locales.mjs
src/
  main/
    agent/
      middlewares/
        extract-reasoning-middleware.ts
      tools/
        aider.ts
        approval-manager.ts
        helpers.ts
        memory.ts
        power.ts
        skills.ts
        subagents.ts
        tasks.ts
        todo.ts
      agent-profile-manager.ts
      agent.ts
      compaction.ts
      index.ts
      mcp-config-manager.ts
      mcp-manager.ts
      mcp-oauth-manager.ts
      optimizer.ts
      smart-compaction.ts
      subagent.ts
      utils.ts
    connector/
      connector-auth.ts
      connector-manager.ts
      connector.ts
      index.ts
    custom-commands/
      custom-command-manager.ts
      index.ts
    data-manager/
      data-manager.ts
      index.ts
    events/
      event-manager.ts
      index.ts
    extensions/
      __fixtures__/
        invalid-syntax.ts
        prohibited-import.ts
        valid-extension.ts
      disposable-store.ts
      extension-context.ts
      extension-fetcher.ts
      extension-library-loader.ts
      extension-loader.ts
      extension-manager.ts
      extension-registry.ts
      index.ts
      project-context.ts
      task-context.ts
    git/
      git-askpass-manager.ts
      git-manager.ts
      index.ts
    logger/
      event-transport.ts
      log-buffer.ts
    memory/
      memory-manager.ts
    models/
      providers/
        alibaba-plan.ts
        anthropic-compatible.ts
        anthropic.ts
        azure.ts
        bedrock.ts
        cerebras.ts
        clinepass.ts
        deepseek.ts
        default.ts
        gemini.ts
        gpustack.ts
        groq.ts
        kimi-plan.ts
        litellm.ts
        llmapi.ts
        lm-studio.ts
        minimax.ts
        mistral.ts
        neuralwatt.ts
        ollama.ts
        openai-compatible.ts
        openai.ts
        opencode-go.ts
        opencode.ts
        openrouter.ts
        requesty.ts
        synthetic.ts
        vertex-ai.ts
        zai-plan.ts
      index.ts
      model-manager.ts
      types.ts
      utils.ts
    project/
      index.ts
      migrations.ts
      project-manager.ts
      project.ts
    prompts/
      helpers/
        conditional.ts
        file.ts
        formatting.ts
        index.ts
      index.ts
      prompts-manager.ts
      types.ts
    server/
      rest-api/
        agent-api.ts
        base-api.ts
        commands-api.ts
        context-api.ts
        extensions-api.ts
        index.ts
        mcp-api.ts
        memory-api.ts
        project-api.ts
        prompt-api.ts
        providers-api.ts
        readonly-api.ts
        settings-api.ts
        skills-api.ts
        system-api.ts
        terminal-api.ts
        todo-api.ts
        usage-api.ts
        voice-api.ts
      cloudflare-tunnel-manager.ts
      cors.ts
      index.ts
      readonly.ts
      runner.ts
      server-controller.ts
    skills/
      skill-manager.ts
    store/
      migrations/
        v0-to-v1.ts
        v1-to-v2.ts
        v10-to-v11.ts
        v11-to-v12.ts
        v12-to-v13.ts
        v13-to-v14.ts
        v14-to-v15.ts
        v15-to-v16.ts
        v16-to-v17.ts
        v17-to-v18.ts
        v18-to-v19.ts
        v19-to-v20.ts
        v2-to-v3.ts
        v20-to-v21.ts
        v21-to-v22.ts
        v3-to-v4.ts
        v4-to-v5.ts
        v5-to-v6.ts
        v6-to-v7.ts
        v7-to-v8.ts
        v8-to-v9.ts
        v9-to-v10.ts
      index.ts
      store.ts
    task/
      migrations/
        v1-to-v2.ts
      aider-manager.ts
      context-manager.ts
      index.ts
      task.ts
    telemetry/
      index.ts
      langfuse.ts
      open-telemetry.ts
      posthog.ts
      telemetry-manager.ts
    terminal/
      index.ts
      terminal-manager.ts
    utils/
      atomic-write.ts
      environment.ts
      errors.ts
      file-system.ts
      file-watch.ts
      http.ts
      i18n.ts
      index.ts
      mutex.ts
      open-url.ts
      probe.ts
      python.ts
      ripgrep.ts
      shell.ts
      web-scrapper.ts
      window-load-retry.ts
    versions/
      index.ts
      versions-manager.ts
    app.ts
    constants.ts
    events-handler.ts
    file-watcher-manager.ts
    index.ts
    ipc-handlers.ts
    logger.ts
    managers.ts
    messages.ts
    network-manager.ts
    paths.ts
    progress-window.ts
    python-dependencies-installer.ts
    start-up.ts
    window-manager.ts
  preload/
    index.d.ts
    index.ts
  renderer/
    public/
      manifest.json
      sw.js
    src/
      api/
        browser-api.ts
        readonly-browser-api.ts
      components/
        AgentSelector/
          AgentSelector.tsx
          ExtensionToolsSelectorItem.tsx
          index.ts
          McpServerSelectorItem.tsx
        common/
          CommandPalette/
            CommandPalette.tsx
            CommandPaletteContent.tsx
            index.ts
          DiffViewer/
            CompactDiffViewer.tsx
            DiffLineCommentPanel.tsx
            DiffViewer.scss
            DiffViewer.tsx
            index.tsx
            PierreDiffViewer.scss
            PierreDiffViewer.tsx
            UDiffViewer.tsx
            utils.ts
          Accordion.tsx
          AppCrashFallback.tsx
          AppErrorBoundary.tsx
          BaseDialog.tsx
          Button.tsx
          Checkbox.tsx
          Chip.tsx
          ChipListInput.tsx
          CodeBlock.tsx
          CodeInline.tsx
          CompactSelect.tsx
          ConfirmDialog.tsx
          DatePicker.css
          DatePicker.tsx
          DevelopmentPerformanceCleanup.tsx
          FileChip.tsx
          FloatingPanel.tsx
          HtmlInfoDialog.tsx
          IconButton.tsx
          InfoIcon.tsx
          InlineEditPanel.tsx
          Input.tsx
          InputPromptDialog.tsx
          InputPromptModal.tsx
          ItemSelector.tsx
          LoadingOverlay.tsx
          LoadingText.tsx
          MarkdownInfoDialog.tsx
          MermaidDiagram.tsx
          ModalOverlayLayout.tsx
          ModalOverlayUrlViewer.tsx
          ModelSelectorWrapper.tsx
          MultiSelect.tsx
          RadioButton.tsx
          SearchableSelect.tsx
          Section.tsx
          Select.tsx
          Slider.tsx
          StyledTooltip.tsx
          Table.tsx
          Tabs.tsx
          TaskStateChip.tsx
          TextArea.tsx
          Toggle.tsx
          TriStateCheckbox.tsx
          VerticalDotsMenu.tsx
          VirtualTable.tsx
          WarningIcon.tsx
        extensions/
          ExtensionComponentRenderer.tsx
          ExtensionComponentWrapper.tsx
          ExtensionUIErrorBoundary.tsx
          FloatingExtensionPanels.tsx
          StringToReactComponent.tsx
          useExtensionComponentsWrapper.tsx
          useExtensionMessageComponent.tsx
        logs/
          LogsPage.tsx
        message/
          ActivateSkillToolMessage.tsx
          AssistantMessageBlock.tsx
          BashToolMessage.tsx
          CommandOutputMessageBlock.tsx
          CompactionSnippetBlock.tsx
          CompactToolMessage.tsx
          CopyMessageButton.tsx
          CreateTaskToolMessage.tsx
          CustomCommandBashBlock.tsx
          DefaultTaskStateActions.tsx
          DeleteMemoryToolMessage.tsx
          DeleteTaskToolMessage.tsx
          ExpandableMessageBlock.tsx
          FetchToolMessage.tsx
          FileEditToolMessage.tsx
          FileReadToolMessage.tsx
          FileWriteToolMessage.tsx
          GetTaskMessageToolMessage.tsx
          GetTaskToolMessage.tsx
          GlobToolMessage.tsx
          GrepToolMessage.tsx
          GroupMessageBlock.tsx
          index.ts
          ListMemoriesToolMessage.tsx
          ListTasksToolMessage.tsx
          LoadingMessageBlock.tsx
          LogMessageBlock.tsx
          MessageActions.tsx
          MessageBar.tsx
          MessageBlock.tsx
          MessageBlockWrapper.tsx
          MessageImages.tsx
          messageMap.ts
          MessageMap.tsx
          MessageMapMarker.tsx
          MessageMapPreview.tsx
          Messages.tsx
          RebaseConflictsActions.tsx
          RebaseResolvedActions.tsx
          ReflectedMessageBlock.tsx
          ResponseMessageBlock.tsx
          RetrieveMemoryToolMessage.tsx
          RunPromptToolMessage.tsx
          SearchParentTaskToolMessage.tsx
          SearchTaskToolMessage.tsx
          SemanticSearchToolMessage.tsx
          StoreMemoryToolMessage.tsx
          StreamingToolMessage.tsx
          SubagentToolMessage.tsx
          TaskInfoPanel.tsx
          TaskStateActions.tsx
          ThinkingAnswerBlock.tsx
          ToolMessageBlock.tsx
          ToolResultImages.tsx
          UpdateMemoryToolMessage.tsx
          UsageInfo.tsx
          UserMessageBlock.tsx
          utils.tsx
          VirtualizedMessages.tsx
        ModelLibrary/
          providers/
            AlibabaPlanModelOverrides.tsx
            AlibabaPlanParameters.tsx
            AlibabaPlanThinkingSettings.tsx
            AnthropicCompatibleParameters.tsx
            AnthropicParameters.tsx
            AzureAdvancedSettings.tsx
            AzureModelOverrides.tsx
            AzureParameters.tsx
            BedrockParameters.tsx
            CerebrasParameters.tsx
            ClinePassParameters.tsx
            DeepseekModelOverrides.tsx
            DeepseekParameters.tsx
            DefaultModelOverrides.tsx
            EnableVoiceControl.tsx
            GeminiAdvancedSettings.tsx
            GeminiModelOverrides.tsx
            GeminiParameters.tsx
            GpustackParameters.tsx
            GroqParameters.tsx
            index.ts
            KimiPlanParameters.tsx
            LitellmParameters.tsx
            LlmApiParameters.tsx
            LmStudioParameters.tsx
            MinimaxParameters.tsx
            MistralParameters.tsx
            NeuralwattAdvancedSettings.tsx
            NeuralwattModelOverrides.tsx
            NeuralwattParameters.tsx
            OllamaParameters.tsx
            OpenAiAdvancedSettings.tsx
            OpenAiCompatibleAdvancedSettings.tsx
            OpenAiCompatibleModelOverrides.tsx
            OpenAiCompatibleParameters.tsx
            OpenAiModelOverrides.tsx
            OpenAiParameters.tsx
            OpenCodeAdvancedSettings.tsx
            OpenCodeGoAdvancedSettings.tsx
            OpenCodeGoModelOverrides.tsx
            OpenCodeGoParameters.tsx
            OpenCodeModelOverrides.tsx
            OpenCodeParameters.tsx
            OpenRouterAdvancedSettings.tsx
            OpenRouterModelOverrides.tsx
            OpenRouterParameters.tsx
            ProviderModels.tsx
            RequestyAdvancedSettings.tsx
            RequestyModelOverrides.tsx
            RequestyParameters.tsx
            SyntheticParameters.tsx
            TlsSettings.tsx
            useArrayField.ts
            VertexAiAdvancedSettings.tsx
            VertexAiModelOverrides.tsx
            VertexAIParameters.tsx
            ZaiPlanAdvancedSettings.tsx
            ZaiPlanModelOverrides.tsx
            ZaiPlanParameters.tsx
            ZaiPlanThinkingSetting.tsx
          DisableStreaming.tsx
          DisableToolCallStreaming.tsx
          index.ts
          ModelDialog.tsx
          ModelLibrary.tsx
          ModelParameterOverrides.tsx
          ModelTableSection.tsx
          ProviderCard.tsx
          ProviderHeader.tsx
          ProviderProfileCard.tsx
          ProviderProfileDialog.tsx
          ProviderProfileForm.tsx
          ProviderSelection.tsx
        onboarding/
          OnboardingPathChoice.tsx
          OnboardingProviderSetup.tsx
          OnboardingStepper.tsx
        project/
          TaskSidebar/
            TaskItem.tsx
            TaskMenuButton.tsx
            TaskSectionHeader.tsx
            TaskSidebar.tsx
            TaskSidebarMultiSelectMenu.tsx
            TaskStatusIcon.tsx
          AddFileDialog.tsx
          FileFinder.tsx
          FilesContextInfoContent.tsx
          GitBranchesButton.tsx
          GitBranchesPopup.tsx
          GitBranchesTooltipContent.tsx
          GitInitButton.tsx
          GitStatusBadges.tsx
          MobileSidebar.tsx
          MountedTaskView.tsx
          NoProjectsOpen.tsx
          OpenProjectDialog.tsx
          ProjectTabContent.tsx
          ProjectTabs.tsx
          ProjectView.tsx
          TaskBar.tsx
          TaskControlBar.tsx
          TaskGitControls.tsx
          TaskView.tsx
          TodoListItem.tsx
          TodoWindow.tsx
          useSidebarWidth.ts
          WelcomeMessage.tsx
          WorktreeActionDialog.tsx
          WorktreeRevertButton.tsx
        PromptField/
          acceptPathSegment.ts
          AudioAnalyzer.tsx
          AutoApprove.tsx
          AutonomySelector.tsx
          CommandSuggestion.tsx
          EditFormatSelector.tsx
          index.ts
          InputHistoryMenu.tsx
          ModeSelector.tsx
          PromptField.tsx
          QueuedPromptsList.tsx
          usePromptFieldText.ts
          wordCompletions.ts
        readonly/
          ReadonlyHeader.tsx
          ReadonlyProjectView.tsx
          ReadonlyTaskSidebar.tsx
          ReadonlyTaskView.tsx
        settings/
          agent/
            sections/
              ProfileGeneralSection.tsx
              ProfilePromptsSection.tsx
              ProfileSubagentsSection.tsx
              ProfileToolsSection.tsx
            AgentProfileItem.tsx
            AgentRules.tsx
            AgentSettings.tsx
            BashToolItem.tsx
            GenericToolGroupItem.tsx
            GenericToolItem.tsx
            SortableAgentProfileItem.tsx
            SystemPromptEditor.tsx
          mcp/
            McpOAuthControls.tsx
            McpServerForm.tsx
            McpServerItem.tsx
            McpServerSidebarItem.tsx
            McpServerToolsPanel.tsx
            McpSettings.tsx
            McpToolCard.tsx
            McpToolItem.tsx
          AboutSettings.tsx
          AiderSettings.tsx
          ExtensionCard.tsx
          ExtensionSettingsDialog.tsx
          ExtensionsSettings.tsx
          GeneralSettings.tsx
          HotkeyConfig.tsx
          HotkeysSettings.tsx
          LanguageSelector.tsx
          MemorySettings.tsx
          NetworkSettings.tsx
          SettingsDialog.tsx
          SettingsPage.tsx
          TaskSettings.tsx
          VoiceSettings.tsx
        terminal/
          Terminal.scss
          Terminal.tsx
          TerminalView.tsx
        ui/
          Tooltip.tsx
        usage/
          CacheHitRateTrendChart.tsx
          CacheSavingsChart.tsx
          CostBreakdownChart.tsx
          MessageBreakdownChart.tsx
          ModelUsageDistributionChart.tsx
          TokenUsageTrendChart.tsx
          UsageDashboard.tsx
          UsageTable.tsx
          utils.ts
        Workspace/
          CommentsPanel.tsx
          ContextFilesSection.tsx
          ContextFilesSectionInner.tsx
          DiffFileItem.tsx
          DiffFilesSidebar.tsx
          EmptyContextInfo.tsx
          fileEditorInlineRequests.tsx
          FileEditorModal.tsx
          FileEditorTabs.tsx
          fileEditorUtils.ts
          group-files.ts
          index.ts
          InlineChangeRequest.tsx
          ProjectFilesSection.tsx
          RulesSection.tsx
          SectionContent.tsx
          SectionHeader.tsx
          SectionLoading.tsx
          SidebarGroupTree.tsx
          SkillsSection.tsx
          Tree.tsx
          TreeItemRenderer.tsx
          types.ts
          UpdatedFilesDiffModal.tsx
          UpdatedFilesSection.tsx
          Workspace.css
          Workspace.tsx
          WorkspaceSection.tsx
        AutocompletionInput.tsx
        CostInfo.tsx
        ModelSelector.tsx
        TelemetryInfoDialog.tsx
        TokenUsageBar.tsx
      constants/
        keyboardKeys.ts
      contexts/
        AgentsContext.tsx
        ApiContext.tsx
        CommandPaletteContext.tsx
        ContextMenuContext.tsx
        DiffsWorkerPoolContext.tsx
        DiffViewerContext.tsx
        ExtensionApiContext.tsx
        ExtensionsContext.tsx
        FileEditorContext.tsx
        McpServersContext.tsx
        ModelProviderContext.tsx
        ProjectSettingsContext.tsx
        ReadonlyApiContext.tsx
        TasksContext.tsx
      fonts/
        Enriqueta.ttf
        FunnelDisplay.ttf
        GoogleSansCode.ttf
        Inter.ttf
        JetBrainsMono.ttf
        Lora.ttf
        Nunito.ttf
        Orbitron.ttf
        PlayfairDisplay.ttf
        Poppins.ttf
        Quicksand.ttf
        RobotoMono.ttf
        Sansation.ttf
        Silkscreen.ttf
        Sono.ttf
        SourceCodePro.ttf
        SpaceGrotesk.ttf
        SpaceMono.ttf
        UbuntuMono.ttf
      hooks/
        useAiderConnectorStatus.ts
        useAudioRecorder.ts
        useBooleanState.ts
        useClickOutside.ts
        useCommandPaletteHotkeys.ts
        useCommands.ts
        useCommitChanges.ts
        useConfiguredHotkeys.ts
        useCustomModes.ts
        useDropdownState.ts
        useEffectiveEnvironmentVariable.ts
        useExtensionTools.ts
        useIndexedDB.ts
        useLmStudioModels.ts
        useModes.ts
        useOllamaModels.ts
        useOpenRouterModels.ts
        useOS.ts
        useOverlayFocusRestore.ts
        usePaletteCommands.ts
        useParsedContent.ts
        usePythonInstallStatus.ts
        useRefractorLanguage.ts
        useRequestyModels.ts
        useResponsive.ts
        useScrollingPaused.ts
        useSearchText.tsx
        useSkills.ts
        useTaskActions.ts
        useTaskCommandHandlers.ts
        useTaskDataHandlers.ts
        useTaskGitStatus.ts
        useTaskLifecycleHandlers.ts
        useTaskLogHandlers.ts
        useTaskMessageHandlers.ts
        useTaskResponseHandlers.ts
        useTaskToolHandlers.ts
        useTypingAnimation.ts
        useUpdatedFileContents.ts
        useUpdatedFileDiff.ts
        useUserMessageNavigation.tsx
        useVersions.ts
      i18n/
        index.ts
      icons/
        AlibabaPlanIcon.tsx
        AnthropicCompatibleIcon.tsx
        AnthropicIcon.tsx
        AzureIcon.tsx
        BedrockIcon.tsx
        CerebrasIcon.tsx
        ClinePassIcon.tsx
        DeepseekIcon.tsx
        GeminiIcon.tsx
        gpustack.png
        GpustackIcon.tsx
        GroqIcon.tsx
        index.ts
        KimiPlanIcon.tsx
        LitellmIcon.tsx
        LlmApiIcon.tsx
        LmStudioIcon.tsx
        MinimaxIcon.tsx
        MistralIcon.tsx
        NeuralwattIcon.tsx
        OllamaIcon.tsx
        OpenAiCompatibleIcon.tsx
        OpenAiIcon.tsx
        OpenCodeGoIcon.tsx
        OpenCodeIcon.tsx
        OpenRouterIcon.tsx
        requesty.png
        RequestyIcon.tsx
        SyntheticIcon.tsx
        VertexAiIcon.tsx
        ZaiPlanIcon.tsx
      pages/
        Home.tsx
        Logs.tsx
        Onboarding.tsx
        ReadonlyApp.tsx
        Settings.tsx
        UpdatedFilesDiff.tsx
      stores/
        actionsStore.ts
        commandPaletteStore.ts
        commitStore.ts
        extensionToolsStore.ts
        extensionUIStore.ts
        fileEditorStore.ts
        overlayStore.ts
        projectStore.ts
        settingsNavigationStore.ts
        settingsStore.ts
        taskFilesStore.ts
        taskSidebarStore.ts
        taskStore.ts
        terminalStore.ts
      themes/
        theme-aurora.scss
        theme-botanical-garden-dark.scss
        theme-botanical-garden.scss
        theme-bw.scss
        theme-cappuccino.scss
        theme-charcoal.scss
        theme-crimson.scss
        theme-dark.scss
        theme-forest.scss
        theme-fresh.scss
        theme-lavender.scss
        theme-light.scss
        theme-midnight.scss
        theme-neon.scss
        theme-neopunk.scss
        theme-obsidian.scss
        theme-ocean.scss
        theme-serenity.scss
        themes.scss
      utils/
        agents.ts
        browser-notifications.ts
        date-utils.ts
        extension-icons.ts
        extension-library-loader.ts
        highlighter.tsx
        hotkeys.ts
        indexedDB.ts
        notifications.ts
        path-utils.ts
        readonly-events.ts
        refractor-languages.ts
        routes.ts
        string-utils.ts
        task-messages.ts
        task-utils.ts
      voice/
        gemini.ts
        index.ts
        openai.ts
        types.ts
      App.tsx
      env.d.ts
      fonts.css
      main.css
      main.tsx
    favicon.ico
    index.html
    progress.html
electron-builder.yml
electron.vite.config.ts
package.json
tailwind.config.js

================================================================
Files
================================================================

================
File: resources/connector/connector.py
================
#!/usr/bin/env python
⋮----
def apply_tls_overrides()
⋮----
"""Apply AiderDesk TLS settings before litellm/httpx create their SSL contexts.

  Must run before `import litellm` so module-level HTTP clients pick up the patched
  `ssl.create_default_context`. Re-applying later only affects newly created contexts.
  """
⋮----
insecure = os.getenv("AIDER_DESK_INSECURE_TLS") == "1"
ca_bundle = os.getenv("AIDER_DESK_CA_BUNDLE_PATH")
⋮----
original_create_default_context = ssl.create_default_context
⋮----
def patched_create_default_context(*args, **kwargs)
⋮----
context = original_create_default_context(*args, **kwargs)
⋮----
THINKING_MARKER = re.compile(r'[-]{3,}\s*\n\s*►\s*\*\*THINKING\*\*\s*\n', re.IGNORECASE)
ANSWER_MARKER = re.compile(r'[-]{3,}\s*\n\s*►\s*\*\*ANSWER\*\*\s*\n', re.IGNORECASE)
⋮----
class PromptContext
⋮----
def __init__(self, id: str, group=None, auto_approve=False, deny_commands=False)
⋮----
confirmation_result = None
⋮----
def update_model_params(models_info, model)
⋮----
model_id = model.name
⋮----
info = models_info[model_id]
max_input_tokens = info.get("maxInputTokens")
⋮----
# skip models that don't have maxInputTokens defined
⋮----
model_info = {
⋮----
max_input_tokens = model.info.get('max_input_tokens') if model.info else None
⋮----
max_output_tokens = model.info.get('max_output_tokens') if model.info else None
⋮----
temperature = info.get('temperature')
⋮----
task_id = os.environ.get("TASK_ID")
openai_base = os.environ.get("OPENAI_API_BASE", "")
anthropic_base = os.environ.get("ANTHROPIC_BASE_URL", "")
⋮----
extra_headers = model.extra_params.setdefault("extra_headers", {})
⋮----
def wait_for_async(connector, coroutine)
⋮----
# Called from the main thread
task = connector.loop.create_task(coroutine)
result = connector.loop.run_until_complete(task)
⋮----
# Called from another thread
future = asyncio.run_coroutine_threadsafe(coroutine, connector.loop)
result = future.result()
⋮----
class PromptExecutor
⋮----
"""Manages prompt execution as concurrent asyncio tasks."""
⋮----
def __init__(self, connector)
⋮----
def get_executor(self)
⋮----
# create a new executor with 100 workers for 100 concurrent prompts
⋮----
async def run_prompt(self, prompt: str, prompt_context: PromptContext, mode=None, architect_model=None, messages=None, files=None, coder=None)
⋮----
prompt_coro = self._run_prompt_task(prompt, prompt_context, mode, architect_model, messages, files, coder)
⋮----
# Submit the coroutine to the executor, which turns it into a background Task.
# If a prompt with the same ID is already running, cancel it first.
⋮----
# Wrap the coroutine in a task that will handle its own cleanup.
task = self.connector.loop.create_task(self._execute_prompt_wrapper(prompt_context, prompt_coro))
⋮----
# This new method contains the logic that used to be in _run_prompt_sync and _run_prompt_async
async def _run_prompt_task(self, prompt: str, prompt_context: PromptContext, mode=None, architect_model=None, messages=None, files=None, coder=None)
⋮----
"""The actual prompt execution logic, designed to be run as a task."""
⋮----
# The core async logic from your old `_run_prompt_async`
# Now you can directly await async functions without any special handling.
⋮----
# This is important! The task must be allowed to handle its own cancellation.
⋮----
# Propagate the cancellation to ensure the wrapper knows about it.
⋮----
async def _stream_and_send_responses(self, coder, prompt_context, prompt_to_run, log_context, extra_response_data=None)
⋮----
extra_response_data = {}
⋮----
whole_content = ""
whole_reasoning = ""
response_id = str(uuid.uuid4())
⋮----
# Track parsing state across chunks
reasoning_started = False
answer_started = False
buffer = ""
⋮----
queue = asyncio.Queue()
executor = self.get_executor()
⋮----
def _sync_worker()
⋮----
# Put chunk into queue, handling potential queue full
fut = asyncio.run_coroutine_threadsafe(queue.put(chunk), self.connector.loop)
fut.result() # Wait for it to be put in queue
asyncio.run_coroutine_threadsafe(queue.put(None), self.connector.loop).result() # Sentinel for end
⋮----
future = self.connector.loop.run_in_executor(executor, _sync_worker)
⋮----
chunk = await queue.get()
⋮----
# Parse reasoning/content from buffer
reasoning_chunk = ""
content_chunk = ""
should_send = True
⋮----
# Check for THINKING marker
thinking_match = THINKING_MARKER.search(buffer)
⋮----
before = buffer[:thinking_match.start()]
reasoning_started = True
buffer = buffer[thinking_match.end():]
⋮----
content_chunk = before
⋮----
reasoning_chunk = buffer
⋮----
should_send = False
⋮----
# Check for ANSWER marker
answer_match = ANSWER_MARKER.search(buffer)
⋮----
# Send reasoning before the marker as a separate chunk
reasoning_before_marker = buffer[:answer_match.start()]
⋮----
reasoning_payload = {
⋮----
answer_started = True
buffer = buffer[answer_match.end():]
⋮----
content_chunk = buffer
⋮----
# No marker found, stream the appropriate field
⋮----
response_payload = {
⋮----
# Handle any remaining buffered content (e.g., reasoning without an answer marker)
⋮----
async def _run_prompt_async(self, prompt: str, prompt_context: PromptContext, mode=None, architect_model=None, messages=None, files=None, coder=None)
⋮----
coder_provided = coder is not None
sequence_number = 0
⋮----
coder_model = self.connector.coder.main_model
running_model = self.connector.coder.main_model
⋮----
running_model = models.Model(architect_model, weak_model=coder_model.weak_model.name, editor_model=coder_model.name)
⋮----
# check if we have temperature info for the architect model
⋮----
sequence_number = -1
⋮----
coder = clone_coder(
⋮----
# we are sending all the additional messages after the prompt finishes
⋮----
# we need to disable auto accept as this does not work properly with AiderDesk
⋮----
# setting usage report to None to avoid no attribute error
⋮----
# if there was no content, use the partial_response_content value (case for non streaming models)
whole_content = coder.partial_response_content
⋮----
# Parse reasoning/answer from raw content (handles both streaming and non-streaming)
whole_reasoning = whole_reasoning or ""
whole_answer = whole_content
⋮----
thinking_match = THINKING_MARKER.search(whole_content)
⋮----
answer_match = ANSWER_MARKER.search(whole_content)
⋮----
whole_reasoning = whole_reasoning or whole_content[thinking_match.end():answer_match.start()].strip()
whole_answer = whole_content[answer_match.end():].strip()
⋮----
whole_reasoning = whole_reasoning or whole_content[thinking_match.end():].strip()
whole_answer = ""
⋮----
def get_usage_report()
⋮----
# Send final response with complete data
response_data = {
⋮----
# Add commit info if there was one
⋮----
# Add diff if there was a commit
commits = f"{coder.last_aider_commit_hash}~1"
⋮----
diff = coder.repo.diff_commits(
⋮----
# Check for reflections
⋮----
# send newly added context files
⋮----
current_reflection = 0
⋮----
reflection_prompt = coder.reflected_message
⋮----
# await self.connector.send_update_context_files()
⋮----
# Send prompt-finished message
⋮----
# Send command outputs as context messages
⋮----
async def _execute_prompt_wrapper(self, prompt_context: PromptContext, prompt_coro: Coroutine[Any, Any, Any])
⋮----
"""
    Wrapper that executes the prompt coroutine and handles completion or cancellation.
    """
prompt_id = prompt_context.id
⋮----
# The main logic is now simply awaiting the coroutine
⋮----
# This is the clean way to handle cancellation.
⋮----
# You could add more specific cleanup logic here if needed.
⋮----
# Potentially re-raise or handle as needed
⋮----
# Always clean up the task from the active prompts dict.
⋮----
def _cleanup_prompt(self, prompt_id: str)
⋮----
"""Clean up completed or cancelled prompt."""
⋮----
def get_coder(self, prompt_id: str) -> Optional[Coder]
⋮----
"""Retrieve the coder instance for a given prompt ID."""
⋮----
async def cancel_prompt(self, prompt_id: str) -> bool
⋮----
"""Cancel a specific prompt task."""
⋮----
# Cancel the executor future if it exists
⋮----
future = self.active_futures[prompt_id]
⋮----
# Set the coder's IO to cancelled if it exists
⋮----
coder = self.active_coders[prompt_id]
⋮----
task = self.active_prompts[prompt_id]
⋮----
# The cancel() method schedules the task to be cancelled.
# It will raise CancelledError at the next `await`.
⋮----
# You can optionally wait for it to be fully cancelled.
⋮----
pass # Expected
⋮----
async def interrupt_all_prompts(self)
⋮----
"""Interrupt all active prompts."""
# Create a copy of keys to avoid issues with modifying the dict while iterating
prompt_ids = list(self.active_prompts.keys())
⋮----
def is_prompt_interrupted(self, prompt_id: str) -> bool
⋮----
"""Check if a prompt task has been cancelled."""
⋮----
# If it's not in active_prompts, it's finished or was cancelled.
⋮----
async def shutdown(self)
⋮----
"""Shutdown the executor by cancelling all active tasks."""
⋮----
async def run_editor_coder_stream(architect_coder, connector, prompt_context)
⋮----
# Use the editor_model from the main_model if it exists, otherwise use the main_model itself
editor_model = architect_coder.main_model.editor_model or architect_coder.main_model
# Generate a prompt info for the editor coder
editor_prompt_context = PromptContext(str(uuid.uuid4()), prompt_context.group)
⋮----
editor_coder = clone_coder(
⋮----
# Start the prompt execution (non-blocking)
⋮----
# Wait for completion by awaiting the task
⋮----
task = connector.prompt_executor.active_prompts[editor_prompt_context.id]
⋮----
pass # The task was cancelled, which is an expected way for it to end.
⋮----
class ConnectorInputOutput(InputOutput)
⋮----
def __init__(self, connector=None, prompt_context=None, **kwargs)
⋮----
# Aider's InputOutput initializer can call overridable output hooks.
# Initialize AiderDesk state first so early hook calls do not hit missing
# attributes and leave the connector stuck during startup or tool handling.
⋮----
def add_to_input_history(self, input_text)
⋮----
# handled by AiderDesk
⋮----
def tool_output(self, *messages, log_only=False, bold=False)
⋮----
# Extract current command from "Running" messages
⋮----
async def send_use_command_output()
⋮----
def is_warning_ignored(self, message)
⋮----
def tool_warning(self, message="", strip=True)
⋮----
def is_error_ignored(self, message)
⋮----
def tool_error(self, message="", strip=True)
⋮----
result = None
⋮----
result = "y"
⋮----
result = "n"
⋮----
# Reset the result
⋮----
# Create coroutine for emitting the question
async def ask_question()
⋮----
# we are handling this in AiderDesk differently
⋮----
result = wait_for_async(self.connector, ask_question())
⋮----
# Get the specific coder for this prompt
coder_for_prompt = self.connector.prompt_executor.get_coder(self.prompt_context.id) if self.prompt_context else None
if coder_for_prompt: # Ensure we have a valid coder
# Process architect coder
⋮----
def reset_state(self, add_command_to_context=None)
⋮----
def interrupt_input(self)
⋮----
async def process_changes()
⋮----
lock_file_path = Path(self.connector.base_dir) / '.aider-desk' / 'watch-files.lock'
lock_file = None
⋮----
# Create lock file directory if it doesn't exist
⋮----
# Try to acquire lock file
lock_file = open(lock_file_path, 'w')
⋮----
# Try to acquire exclusive lock (non-blocking)
⋮----
# Successfully acquired lock, proceed with processing
⋮----
task = self.connector.prompt_executor.active_prompts[prompt_context.id]
⋮----
# Lock is held by another process, skip processing
⋮----
# Explicitly close the lock file to release the lock
⋮----
prompt = self.connector.file_watcher.process_changes()
⋮----
changed_files = ", ".join(sorted(self.connector.file_watcher.changed_files))
group = {
prompt_context = PromptContext(str(uuid.uuid4()), group)
⋮----
def clone_coder(connector, coder, prompt_context=None, messages=None, files=None, **kwargs)
⋮----
coder = Coder.create(**kwargs)
⋮----
# Set messages from the provided data
⋮----
# Set files from the provided data
⋮----
file_path = file['path']
⋮----
file_path = os.path.join(connector.task_dir, file_path)
⋮----
def create_base_coder(connector)
⋮----
coder = cli_main(return_coder=True)
⋮----
def create_io(connector, coder, prompt_context=None)
⋮----
io = ConnectorInputOutput(
⋮----
class Connector
⋮----
def __init__(self, base_dir, task_id, task_dir, watch_files=False, server_url="http://localhost:24337", reasoning_effort=None, thinking_tokens=None, confirm_before_edit=False)
⋮----
# Create initial coder for setup and non-prompt operations
⋮----
# Initialize prompt executor
⋮----
ignores = []
⋮----
def monkey_patch_coder_functions(self, coder, prompt_context=None)
⋮----
# self here is the Connector instance
# coder is the Coder instance
⋮----
original_lint_edited = coder.lint_edited
def _patched_lint_edited(coder_instance, fnames)
⋮----
# Add loading message before linting
⋮----
# Call the original Coder.lint_edited logic
result = original_lint_edited(fnames)
# Finish the loading message after linting
⋮----
# Replace the original lint_edited method with the patched version
⋮----
# Monkey patch prepare_to_edit to add confirmation if enabled
original_prepare_to_edit = coder.prepare_to_edit
def _patched_prepare_to_edit(coder_instance, edits)
⋮----
# Replace the original prepare_to_edit method with the patched version
⋮----
original_cmd_test = coder.commands.cmd_test
def _patched_cmd_test(coder_commands_instance, args)
⋮----
# coder_commands_instance is the Commands instance (coder.commands)
⋮----
result = original_cmd_test(args)
⋮----
# Replace the original run_test method with the patched version
⋮----
# Initialize command_outputs list if it doesn't exist
⋮----
original_run_shell_commands = coder.run_shell_commands
def _patched_run_shell_commands(coder_instance)
⋮----
# Call the original run_shell_commands method
result = original_run_shell_commands()
# Store the result in command_outputs list
⋮----
# Replace the original run_shell_commands method with the patched version
⋮----
def monkey_patch_repo_functions(self, repo, prompt_context=None)
⋮----
# patch repo.get_commit_message to send a loading message while generating commit message
original_get_commit_message = repo.get_commit_message
⋮----
def _patched_get_commit_message(repo_instance, diffs, context, user_language=None)
⋮----
result = original_get_commit_message(diffs, context, user_language)
⋮----
def register_events(self)
⋮----
@self.sio.event
    async def connect()
⋮----
@self.sio.event
    async def message(data)
⋮----
@self.sio.event
    async def disconnect()
⋮----
@self.sio.event
    async def connect_error(error)
⋮----
async def on_connect(self)
⋮----
"""Handle connection event."""
# Guard against duplicate initialization (can happen if event fires and we also call explicitly)
⋮----
def _tokenize_files_sync(self, root, rel_fnames, addable_rel_fnames, encoding, abs_read_only_fnames)
⋮----
"""Synchronous helper function for file tokenization."""
⋮----
auto_completer = AutoCompleter(
⋮----
# Return tokenized words
⋮----
async def on_message(self, data)
⋮----
async def on_disconnect(self)
⋮----
"""Handle disconnection event."""
⋮----
# Shutdown prompt executor
⋮----
async def connect(self)
⋮----
"""Connect to the server with retry logic."""
max_retries = 10
base_delay = 0.5  # Initial delay in seconds
max_delay = 5.0   # Maximum delay in seconds
⋮----
# Reset initialized flag before each connection attempt
# This ensures on_connect runs properly after reconnection
⋮----
# Add a small delay before the first connection attempt to give
# the server time to be fully ready. This helps avoid the
# "One or more namespaces failed to connect" error.
⋮----
# Connect with explicit timeout. The default is 5 seconds which can
# be too short if the server is still initializing.
connector_token = os.getenv("AIDER_DESK_CONNECTOR_TOKEN")
auth = {"connectorToken": connector_token} if connector_token else None
⋮----
# Explicitly call on_connect after successful connection
# This ensures init message is sent even if the event handler doesn't fire
# (which can happen on reconnection after namespace errors)
⋮----
return  # Connection successful
⋮----
# Last attempt failed, re-raise the exception
⋮----
# Calculate delay with exponential backoff
delay = min(base_delay * (2 ** attempt), max_delay)
⋮----
# Disconnect any partial connection before retrying
⋮----
async def wait(self)
⋮----
"""Wait for events."""
⋮----
async def start(self)
⋮----
async def send_action(self, action, with_delay = True)
⋮----
async def send_log_message(self, level, message, finished=False, prompt_context=None)
⋮----
payload = {
⋮----
async def process_message(self, message)
⋮----
"""Process incoming message and return response"""
⋮----
action = message.get('action')
⋮----
prompt = message.get('prompt')
mode = message.get('mode')
architect_model = message.get('architectModel')
prompt_context_data = message.get('promptContext')
messages = message.get('messages', [])
files = message.get('files', [])
options = message.get('options', {})
⋮----
# Log the options with default values
auto_approve = options.get('autoApprove', False)
deny_commands = options.get('denyCommands', False)
⋮----
prompt_context = PromptContext(prompt_context_data.get('id'), prompt_context_data.get('group'), auto_approve, deny_commands)
⋮----
confirmation_result = message.get('answer')
⋮----
main_model = message.get('mainModel')
weak_model = message.get('weakModel')
edit_format = message.get('editFormat')
environment_variables = message.get('environmentVariables')
⋮----
model = models.Model(main_model, weak_model=weak_model)
⋮----
edit_format = "diff"
⋮----
command = message.get('command')
⋮----
# Interrupt all active prompts
⋮----
edits = message.get('edits')
⋮----
edit_tuples = [(edit['path'], edit['original'], edit['updated']) for edit in edits]
⋮----
main_model = models.Model(self.coder.main_model.name, weak_model=self.coder.main_model.weak_model.name)
⋮----
models_info = message.get('modelsInfo')
⋮----
messages = message.get('messages')
files = message.get('files')
⋮----
async def update_environment_variables(self, environment_variables)
⋮----
"""Update environment variables for the Aider process"""
⋮----
tls_changed = any(
⋮----
# Update the environment variables in the current process
⋮----
async def run_command(self, command, messages, files)
⋮----
command_coder = clone_coder(self, self.coder, messages=messages, files=files)
⋮----
repo_map = command_coder.repo_map.get_repo_map(set(), command_coder.get_all_abs_files()) if command_coder.repo_map else None
⋮----
parts = command.split()
valid_values = ['high', 'medium', 'low', 'minimal', 'none']
⋮----
# Safely remove 'reasoning_effort' if it exists
⋮----
# for /reset and /drop, we only need to send the initial context files
⋮----
# run the command
⋮----
# reset flags
⋮----
async def send_autocompletion(self, files)
⋮----
# Use all files from files parameter and convert to relative paths
rel_fnames = []
⋮----
file_path = f['path']
# Convert to relative path if it's an absolute path
⋮----
relative_path = os.path.relpath(file_path, self.task_dir)
⋮----
relative_path = os.path.relpath(file_path, self.base_dir)
⋮----
relative_path = file_path
⋮----
# Run tokenization in a separate thread
⋮----
# Cancel any previous tokenization task if it's still running
⋮----
async def tokenize_and_send()
⋮----
tokenized_words = await asyncio.to_thread(
⋮----
# Task was cancelled, do nothing.
⋮----
# else: The initial message with just filenames is sufficient if too many files
⋮----
async def send_repo_map(self)
⋮----
repo_map = self.coder.repo_map.get_repo_map(set(), self.coder.get_all_abs_files())
⋮----
# Remove the prefix before sending
prefix = self.coder.gpt_prompts.repo_content_prefix
⋮----
repo_map = repo_map[len(prefix):]
⋮----
def get_context_files(self, coder=None)
⋮----
coder = self.coder
⋮----
def get_rel_fname(fname)
⋮----
editable_files = [get_rel_fname(fname) for fname in coder.abs_fnames]
read_only_files = [get_rel_fname(fname) for fname in coder.abs_read_only_fnames]
⋮----
async def send_add_context_message(self, role, content)
⋮----
async def send_add_context_files(self, coder=None)
⋮----
context_files = self.get_context_files(coder)
⋮----
async def send_update_context_files(self, coder=None)
⋮----
async def send_current_models(self)
⋮----
error = None
info = self.coder.main_model.info
⋮----
error = (
⋮----
async def send_tokens_info(self, messages, files)
⋮----
cost_per_token = self.coder.main_model.info.get("input_cost_per_token") or 0
info = {
⋮----
# system messages
main_sys = self.coder.fmt_system_prompt(self.coder.gpt_prompts.main_system)
⋮----
msgs = [
tokens = self.coder.main_model.token_count(msgs)
⋮----
# Convert messages to the format expected by token_count
msgs = [dict(role=msg['role'], content=msg['content']) for msg in messages]
⋮----
tokens = 0
⋮----
# Convert context_files to absolute file paths like coder does
abs_fnames = set()
abs_read_only_fnames = set()
⋮----
file_path = os.path.join(self.task_dir, file_path)
⋮----
all_abs_files = self.coder.get_all_abs_files()
other_files = set(all_abs_files) - abs_fnames
⋮----
repo_content = self.coder.repo_map.get_repo_map(abs_fnames, other_files)
⋮----
tokens = self.coder.main_model.token_count(repo_content)
⋮----
fence = "`" * 3
⋮----
# Process the provided context files
⋮----
# Skip directories
⋮----
relative_fname = self.coder.get_rel_fname(file_path)
content = self.coder.io.read_text(file_path)
⋮----
tokens = self.coder.main_model.token_count_for_image(file_path)
⋮----
# approximate
content = f"{relative_fname}\n{fence}\n" + content + "{fence}\n"
tokens = self.coder.main_model.token_count(content)
⋮----
async def handle_models_info_update(self, models_info)
⋮----
"""Handle models info update event"""
⋮----
def main(argv=None)
⋮----
argv = sys.argv[1:]
⋮----
# Parse command line arguments
parser = argparse.ArgumentParser(description="AiderDesk Connector")
⋮----
args, _ = parser.parse_known_args(argv) # Use parse_known_args to ignore unknown args
⋮----
# Get environment variables
server_url = os.getenv("CONNECTOR_SERVER_URL", "http://localhost:24337")
base_dir = os.getenv("BASE_DIR", os.getcwd())
task_id = os.getenv("TASK_ID", "default")
task_dir = os.getenv("TASK_DIR", os.getcwd())
confirm_before_edit = os.getenv("CONNECTOR_CONFIRM_BEFORE_EDIT", "0") == "1"
⋮----
# Telemetry
⋮----
# Create connector instance
connector = Connector(
⋮----
# Start the connector
⋮----
def setup_telemetry()
⋮----
langfuse_public_key = os.getenv("LANGFUSE_PUBLIC_KEY")
langfuse_secret_key = os.getenv("LANGFUSE_SECRET_KEY")
langfuse_host = os.getenv("LANGFUSE_HOST", "https://cloud.langfuse.com")
⋮----
posthog_api_key = os.getenv("POSTHOG_API_KEY")
⋮----
# Set OpenRouter site and app name

================
File: scripts/after-pack.js
================
const restore = (dir, rel) =>
⋮----
// Moves everything packaged-pruned in scripts/before-pack.js back into node_modules,
// so the dev environment and other platform builds keep all platform binaries.

================
File: scripts/before-pack.js
================
const nodePlatform = (platformName)
⋮----
const pruneToBackup = (nodeModulesDir, relPaths, key) =>
⋮----
const pruneOnnxruntimeNode = (nodeModulesDir, nodePlatformName, nodeArch, key) =>
⋮----
// CUDA and TensorRT execution providers are loaded dynamically and are not used (embeddings run on CPU); they add ~344MB on linux/x64.
⋮----
const pruneLanceDb = (nodeModulesDir, nodePlatformName, nodeArch, key) =>
⋮----
// lancedb ships glibc (gnu) and musl builds; our Linux targets (AppImage/deb/rpm) use glibc, so only keep the gnu flavor
⋮----
// only prune when we found a matching subpackage for the current platform/arch, otherwise keep everything to be safe
⋮----
// Moves platform-mismatched native binaries away from node_modules for the duration of packaging,
// so they are not included in the bundle; restored again in scripts/after-pack.js.

================
File: scripts/download-probe.mjs
================
async function downloadAndExtractProbeForPlatform(target)
⋮----
// Ensure the specific platform directory exists
⋮----
// Check if probe already exists for this platform
⋮----
strip: 0 // Do not strip any components from the file path initially
⋮----
// The probe executable might be inside a directory like probe-v0.6.0-rc128-x86_64-unknown-linux-gnu/probe-binary
⋮----
// Clean up the extracted directory
⋮----
// If not found in extractedDir, check the top level (for older releases or different structures)
⋮----
zip.extractAllTo(extractPath, true); // Overwrite existing files
⋮----
// Clean up the extracted directory
⋮----
// Do not exit on error for one platform, try others.
⋮----
// Clean up the temporary archive file
⋮----
async function downloadAllProbes()
⋮----
// Ensure the base resources directory exists

================
File: scripts/test-proxy-server.mjs
================
const timestamp = ()
⋮----
/**
 * Simple HTTP forward proxy for testing AiderDesk's proxy settings.
 * Logs every HTTP request and HTTPS CONNECT that passes through.
 */

================
File: scripts/validate-locales.mjs
================


================
File: src/main/agent/middlewares/extract-reasoning-middleware.ts
================
import type { LanguageModelV4StreamPart } from '@ai-sdk/provider';
import type { LanguageModelMiddleware } from 'ai';
⋮----
import logger from '@/logger';
⋮----
/**
 * Extract an XML-tagged reasoning section from the generated text and exposes it
 * as a `reasoning` property on the result.
 *
 * @param tagName - The name of the XML tag to extract reasoning from.
 * @param separator - The separator to use between reasoning and text sections.
 */
⋮----
// check for additional character at the end, e.g. `
// if not found, continue with the next chunk
⋮----
// if there is a backtick before the closing tag and after, publish the buffer, as this is not ending
⋮----
// this should not happen
⋮----
// set and publish the rest of the buffer as non reasoning
⋮----
/**
 * Returns the index of the start of the searchedText in the text, or null if it
 * is not found.
 */
⋮----
// Return null immediately if searchedText is empty.
⋮----
// Check if the searchedText exists as a direct substring of text.
⋮----
// Otherwise, look for the largest suffix of "text" that matches
// a prefix of "searchedText". We go from the end of text inward.

================
File: src/main/agent/tools/aider.ts
================
import fs from 'fs/promises';
import path from 'path';
⋮----
import { tool } from 'ai';
import { z } from 'zod';
import { v4 as uuidv4 } from 'uuid';
import {
  TOOL_GROUP_NAME_SEPARATOR,
  AIDER_TOOL_GROUP_NAME as TOOL_GROUP_NAME,
  AIDER_TOOL_GET_CONTEXT_FILES as TOOL_GET_CONTEXT_FILES,
  AIDER_TOOL_ADD_CONTEXT_FILES as TOOL_ADD_CONTEXT_FILES,
  AIDER_TOOL_DROP_CONTEXT_FILES as TOOL_DROP_CONTEXT_FILES,
  AIDER_TOOL_RUN_PROMPT as TOOL_RUN_PROMPT,
  AIDER_TOOL_DESCRIPTIONS,
} from '@common/tools';
import { AgentProfile, AutonomyMode, PromptContext, ToolApprovalState } from '@common/types';
⋮----
import { ApprovalManager } from './approval-manager';
⋮----
import type { ToolSet } from 'ai';
⋮----
import { coerceBoolean } from '@/agent/utils';
import { Task } from '@/task';
⋮----
export const createAiderToolset = (task: Task, profile: AgentProfile, promptContext?: PromptContext): ToolSet =>
⋮----
// This condition is now met if the file initially existed or was just created
⋮----
// Only add to results if it wasn't handled by the creation logic above
⋮----
// Ask the question and wait for the answer
⋮----
// Notify that we are still processing after aider finishes
⋮----
// Filter out tools that are set to Never in toolApprovals

================
File: src/main/agent/tools/approval-manager.ts
================
import { AgentProfile, AutonomyMode, QuestionData, ToolApprovalState } from '@common/types';
⋮----
import { Task } from '@/task';
⋮----
const isAutoApprove = (task: Task): boolean =>
⋮----
export class ApprovalManager
⋮----
constructor(
⋮----
async handleToolApproval(
    toolName: string,
    input: Record<string, unknown> | undefined,
    key: string,
    text: string,
    subject?: string,
): Promise<[boolean, string | undefined]>
⋮----
async handleApproval(key: string, text: string, subject?: string): Promise<[boolean, string | undefined]>
⋮----
return [true, undefined]; // Auto-approve
⋮----
return [true, undefined]; // Pre-approved
⋮----
return [true, undefined]; // Approved and remember for this run
⋮----
return [true, undefined]; // Approved for this instance
⋮----
return [false, userInput]; // Not approved

================
File: src/main/agent/tools/helpers.ts
================
import { tool } from 'ai';
import { z } from 'zod';
import {
  TOOL_GROUP_NAME_SEPARATOR,
  HELPERS_TOOL_GROUP_NAME as TOOL_GROUP_NAME,
  HELPERS_TOOL_NO_SUCH_TOOL as TOOL_NO_SUCH_TOOL,
  HELPERS_TOOL_INVALID_TOOL_ARGUMENTS as TOOL_INVALID_TOOL_ARGUMENTS,
} from '@common/tools';
⋮----
import type { ToolSet } from 'ai';
⋮----
export const createHelpersToolset = (): ToolSet =>
⋮----
// This tool's result is primarily for the LLM's internal reasoning,
// informing it that its previous attempt failed because the tool was invalid.
⋮----
// This tool informs the LLM about the argument validation error.

================
File: src/main/agent/tools/memory.ts
================
import { tool, type ToolSet } from 'ai';
import { z } from 'zod';
import { getAgentMemoryScopeId } from '@common/agent';
import { AgentProfile, MemoryEntryType, PromptContext, ToolApprovalState } from '@common/types';
import {
  MEMORY_TOOL_DELETE as TOOL_DELETE,
  MEMORY_TOOL_DESCRIPTIONS,
  MEMORY_TOOL_LIST as TOOL_LIST,
  MEMORY_TOOL_RETRIEVE as TOOL_RETRIEVE,
  MEMORY_TOOL_STORE as TOOL_STORE,
  MEMORY_TOOL_UPDATE as TOOL_UPDATE,
  MEMORY_TOOL_GROUP_NAME as TOOL_GROUP_NAME,
  TOOL_GROUP_NAME_SEPARATOR,
} from '@common/tools';
⋮----
import { ApprovalManager } from './approval-manager';
⋮----
import { MemoryManager } from '@/memory/memory-manager';
import { Task } from '@/task';
import logger from '@/logger';
⋮----
export const createMemoryToolset = (task: Task, profile: AgentProfile, memoryManager: MemoryManager, promptContext?: PromptContext): ToolSet =>
⋮----
// Get the memory to show its content in the approval question
⋮----
// Filter by memory scope and type if specified
⋮----
// Get the memory to show its content in the approval question
⋮----
// Filter out tools that are set to Never in toolApprovals

================
File: src/main/agent/tools/power.ts
================
import fs from 'fs/promises';
import path from 'path';
import { spawn } from 'child_process';
import { ChildProcess } from 'node:child_process';
⋮----
import treeKill from 'tree-kill';
import { tool, type ToolSet } from 'ai';
import { z } from 'zod';
import { globIterate } from 'glob';
import { AgentProfile, BashToolSettings, FileWriteMode, PromptContext, ToolApprovalState } from '@common/types';
import {
  POWER_TOOL_BASH as TOOL_BASH,
  POWER_TOOL_DESCRIPTIONS,
  POWER_TOOL_FETCH as TOOL_FETCH,
  POWER_TOOL_FILE_EDIT as TOOL_FILE_EDIT,
  POWER_TOOL_FILE_READ as TOOL_FILE_READ,
  POWER_TOOL_FILE_WRITE as TOOL_FILE_WRITE,
  POWER_TOOL_GLOB as TOOL_GLOB,
  POWER_TOOL_GREP as TOOL_GREP,
  POWER_TOOL_GROUP_NAME as TOOL_GROUP_NAME,
  POWER_TOOL_SEMANTIC_SEARCH as TOOL_SEMANTIC_SEARCH,
  TOOL_GROUP_NAME_SEPARATOR,
} from '@common/tools';
import { isURL } from '@common/utils';
⋮----
import { ApprovalManager } from './approval-manager';
⋮----
import { search } from '@/utils/probe';
import { Task } from '@/task';
import logger from '@/logger';
import { ensureRipgrepBinary, filterIgnoredFiles, scrapeWeb } from '@/utils';
import { isAbortError, isFileNotFoundError } from '@/utils/errors';
import { getShellCommandArgs, getShellInitCommand, getShellPath } from '@/utils/shell';
import { BoundedOutputAccumulator, expandTilde, readFileContent, truncateToolResult, coerceBoolean } from '@/agent/utils';
import { RIPGREP_BINARY_PATH } from '@/constants';
⋮----
/**
 * File lock map to prevent race conditions when multiple edits target the same file.
 * Each file path maps to a promise representing the ongoing operation.
 * New operations on the same file must wait for the previous one to complete.
 */
⋮----
/**
 * Acquires a lock for a file and executes the operation.
 * If another operation is in progress for the same file, it waits for it to complete.
 */
const withFileLock = <T>(filePath: string, operation: () => Promise<T>): Promise<T> =>
⋮----
export const createPowerToolset = (task: Task, profile: AgentProfile, promptContext?: PromptContext, abortSignal?: AbortSignal): ToolSet =>
⋮----
// Sanitize escape characters from searchTerm and replacementText
const sanitize = (str: string) =>
⋮----
// Check if string contains single escaped backslashes (like \n, \t, etc.)
⋮----
// Only sanitize if no single escaped backslashes are found
⋮----
// Remove leading backslash
⋮----
// Remove escaped newlines, quotes, tabs, etc. only when they have double backslashes
⋮----
// Normalize Windows line endings to Unix for consistent matching
⋮----
// Use replacer function to avoid special replacement patterns ($&, $`, '$', $$)
⋮----
absolute: false, // Keep paths relative to cwd for easier processing
⋮----
// Convert to absolute paths for filtering, then back to relative
⋮----
// Ensure paths are relative to task.getTaskDir()
⋮----
// rg --no-heading -n output format:
//   "path:linenum:content" for match lines
//   "path-linenum-content" for context lines (before/after)
//   "--" on its own line as a context break separator
// Context lines before a match are "before-context", after a match are "after-context".
// When matches are far apart in the same file, rg inserts "--" between non-overlapping groups.
⋮----
const processLine = (rawLine: string): boolean =>
⋮----
const cleanup = () =>
⋮----
const finish = () =>
⋮----
abortListener = () =>
⋮----
// Process may have already exited
⋮----
// Process may have already exited
⋮----
// Group results by file path
⋮----
false, // not finished yet
⋮----
// Pattern validation
⋮----
// Check denied patterns first
⋮----
// Check allowed patterns - if matches, skip approval and execute directly
⋮----
const killProcess = (pid: number) =>
⋮----
// Remove abort listener to prevent memory leaks
⋮----
const finishOutput = async (accumulator: BoundedOutputAccumulator): Promise<string> =>
⋮----
const resolveWithResult = async () =>
⋮----
const sendStreamingUpdate = () =>
⋮----
false, // not finished yet
⋮----
// Use the standard resolution method with proper type
⋮----
exitCode = 130; // Standard cancel exit code (128 + SIGINT=2)
⋮----
// Listen for abort signal
⋮----
stdio: ['ignore', 'pipe', 'pipe'], // Explicitly pipe stdout and stderr to capture output from piped commands
⋮----
// Set timeout
⋮----
// Send throttled bounded streaming update
⋮----
// Send throttled bounded streaming update
⋮----
exitCode = 124; // Timeout exit code
⋮----
// Use parameter maxTokens if provided, otherwise use the default
⋮----
// Check if it's a dependency path (format: language:rest)
⋮----
// If path is relative (including "." and "./"), resolve it relative to task.getTaskDir()
⋮----
// List of supported languages by the probe binary
⋮----
// Only include language parameter if it's supported
⋮----
timeout: 5 * 60, // 5 minutes
⋮----
// Filter out tools that are set to Never in toolApprovals

================
File: src/main/agent/tools/skills.ts
================
import { tool, type ToolSet } from 'ai';
import { z } from 'zod';
import { SKILLS_TOOL_ACTIVATE_SKILL, SKILLS_TOOL_GROUP_NAME, TOOL_GROUP_NAME_SEPARATOR } from '@common/tools';
import { AgentProfile, PromptContext, SkillDefinition as Skill, ToolApprovalState } from '@common/types';
⋮----
import { ApprovalManager } from './approval-manager';
⋮----
import { Task } from '@/task';
⋮----
const getActivateSkillDescription = (skills: Skill[]): string =>
⋮----
export const createSkillsToolset = async (task: Task, profile: AgentProfile, promptContext?: PromptContext): Promise<ToolSet> =>
⋮----
const generateActivateSkillDescription = async (): Promise<string> =>

================
File: src/main/agent/tools/subagents.ts
================
import { tool, type ToolSet } from 'ai';
import { z } from 'zod';
import { AgentProfile, ContextMessage, InvocationMode, SettingsData } from '@common/types';
import { getSubagentId } from '@common/agent';
import { SUBAGENTS_TOOL_GROUP_NAME, SUBAGENTS_TOOL_RUN_TASK, TOOL_GROUP_NAME_SEPARATOR } from '@common/tools';
⋮----
import { AgentProfileManager } from '@/agent/agent-profile-manager';
import { findEnabledSubagent, getEnabledSubagents, runSubagentTask } from '@/agent/subagent';
import { Task } from '@/task';
⋮----
export const createSubagentsToolset = async (
  _settings: SettingsData,
  task: Task,
  agentProfileManager: AgentProfileManager,
  mainAgentProfile: AgentProfile,
  abortSignal?: AbortSignal,
  contextMessages: ContextMessage[] = [],
  currentMessages: ContextMessage[] = [],
): Promise<ToolSet> =>
⋮----
const generateSubagentsRunTaskDescription = (): string =>

================
File: src/main/agent/tools/tasks.ts
================
import path from 'path';
⋮----
import { type Tool, tool, type ToolSet } from 'ai';
import { z } from 'zod';
import {
  TASKS_TOOL_CREATE_TASK,
  TASKS_TOOL_DELETE_TASK,
  TASKS_TOOL_DESCRIPTIONS,
  TASKS_TOOL_GET_TASK,
  TASKS_TOOL_GET_TASK_MESSAGE,
  TASKS_TOOL_GROUP_NAME,
  TASKS_TOOL_LIST_TASKS,
  TASKS_TOOL_RUN_PROMPT,
  TASKS_TOOL_SEARCH_PARENT_TASK,
  TASKS_TOOL_SEARCH_TASK,
  TOOL_GROUP_NAME_SEPARATOR,
} from '@common/tools';
import {
  AgentProfile,
  AutonomyMode,
  ContextMessage,
  DefaultTaskState,
  PromptContext,
  SettingsData,
  TaskData,
  TaskExecutionMode,
  ToolApprovalState,
} from '@common/types';
import { fileExists, isUuid } from '@common/utils';
⋮----
import { ApprovalManager } from './approval-manager';
⋮----
import { AIDER_DESK_TASKS_DIR } from '@/constants';
import { search } from '@/utils/probe';
import logger from '@/logger';
import { isAbortError } from '@/utils/errors';
import { deriveDirName } from '@/utils';
import { Task } from '@/task';
⋮----
const stripProviderOptions = (content: ContextMessage['content']): ContextMessage['content']
⋮----
export const createTasksToolset = (settings: SettingsData, task: Task, profile: AgentProfile, promptContext?: PromptContext): ToolSet =>
⋮----
// Find subtasks of this task
⋮----
// Handle negative indexing
⋮----
actualIndex = contextMessages.length + messageIndex; // e.g., -1 becomes length-1
⋮----
// createNewTask returns TaskData, not Task instance
// We need to get the actual Task instance to call methods on it
⋮----
// Prevent deleting the current task
⋮----
/**
 * Creates the search parent task tool for subtasks.
 * This tool is not included in the default toolset and should be added
 * directly in agent.ts's getAvailableTools method when task is a subtask.
 */
export const createSearchParentTaskTool = (task: Task, promptContext?: PromptContext): Tool =>
⋮----
// Always use parent task ID
⋮----
// No approval required for parent task search

================
File: src/main/agent/tools/todo.ts
================
import { tool, type ToolSet } from 'ai';
import { z } from 'zod';
import {
  TODO_TOOL_CLEAR_ITEMS,
  TODO_TOOL_DESCRIPTIONS,
  TODO_TOOL_GET_ITEMS,
  TODO_TOOL_GROUP_NAME,
  TODO_TOOL_SET_ITEMS,
  TODO_TOOL_UPDATE_ITEM_COMPLETION,
  TOOL_GROUP_NAME_SEPARATOR,
} from '@common/tools';
import { AgentProfile, PromptContext, ToolApprovalState } from '@common/types';
⋮----
import { ApprovalManager } from './approval-manager';
⋮----
import { coerceBoolean } from '@/agent/utils';
import { Task } from '@/task';
⋮----
export const createTodoToolset = (task: Task, profile: AgentProfile, promptContext?: PromptContext): ToolSet =>

================
File: src/main/agent/agent-profile-manager.ts
================
import { FSWatcher, watch } from 'chokidar';
import { debounce } from 'lodash';
import { DEFAULT_AGENT_PROFILE, DEFAULT_AGENT_PROFILES } from '@common/agent';
import { fileExists } from '@common/utils';
import { v4 as uuidv4 } from 'uuid';
⋮----
import type { AgentProfile, SettingsData } from '@common/types';
import type { Store } from '@/store';
⋮----
import { Project } from '@/project';
import { ExtensionManager, ExtensionsChangeListener } from '@/extensions/extension-manager';
import { AIDER_DESK_AGENTS_DIR, AIDER_DESK_HOME_DIR, AIDER_DESK_RULES_DIR } from '@/constants';
import logger from '@/logger';
import { EventManager } from '@/events';
import { deriveDirName } from '@/utils';
import { shouldUsePolling } from '@/utils/file-watch';
⋮----
// Helper methods for directory management
const getGlobalAgentsDir = (): string
const getProjectAgentsDir = (projectDir: string): string
const getAgentsDirForProfile = (profile: AgentProfile): string
⋮----
// Helper methods for rule file discovery
const getAgentRulesDir = (agentsDir: string, agentDirName: string): string
⋮----
const getRuleFilesForAgent = async (agentDirName: string, agentsDir: string): Promise<string[]> =>
⋮----
// Rules directory doesn't exist or can't be read
⋮----
const getAllRuleFilesForProfile = async (profile: AgentProfile, dirName: string): Promise<string[]> =>
⋮----
// Get rule files from the profile's own directory
⋮----
// If this is a project-level profile, also check for global profile with same ID
⋮----
interface AgentProfileContext {
  dirName: string;
  order: number;
  agentProfile: AgentProfile;
}
⋮----
export class AgentProfileManager
⋮----
// Single profiles map - the core simplification
⋮----
// Directory watching (one watcher per directory)
private directoryWatchers: Map<string, FSWatcher> = new Map(); // agentsDir → watcher
⋮----
// Listener for extension changes
⋮----
constructor(
    private readonly eventManager: EventManager,
    private readonly extensionManager: ExtensionManager,
    private readonly store: Store,
)
⋮----
public async init(): Promise<void>
⋮----
public async initializeForProject(projectDir: string): Promise<void>
⋮----
// Load project-specific profiles
⋮----
// Setup file watcher for project directory
⋮----
public removeProject(projectDir: string): void
⋮----
// Remove project profiles from the main profiles map
⋮----
// Clean up directory watcher
⋮----
public sendAgentProfilesUpdated(): void
⋮----
private async initializeProfiles(ensureDefaults = true): Promise<void>
⋮----
// Ensure default profiles exist in global directory
⋮----
// Load global profiles
⋮----
private async ensureDefaultProfiles(globalAgentsDir: string): Promise<void>
⋮----
// Only create default profiles if the directory is empty
⋮----
// Config file doesn't exist, create it
⋮----
private async setupGlobalFileWatcher(): Promise<void>
⋮----
// Clean up existing global watcher
⋮----
// Watch global agents directory
⋮----
private async setupWatcherForDirectory(agentsDir: string): Promise<void>
⋮----
// Create directory if it doesn't exist
⋮----
// Close existing watcher for this directory if any
⋮----
// Also watch rules subdirectories within each agent directory
⋮----
const reloadFunction = ()
⋮----
// Set up rules watcher event handlers
⋮----
private async reloadProfiles(agentsDir: string): Promise<void>
⋮----
// Clear existing profiles from this directory
⋮----
// Reload profiles from directory
⋮----
// Notify listeners
⋮----
private async getExistingDirNames(agentsDir: string): Promise<Set<string>>
⋮----
// Directory doesn't exist or can't be read
⋮----
private async isAgentsDirectoryEmpty(agentsDir: string): Promise<boolean>
⋮----
// Check if there are any directories (profiles)
⋮----
return false; // Found at least one profile directory
⋮----
return true; // No profile directories found
⋮----
// Directory doesn't exist or can't be read, treat as empty
⋮----
private async loadProfilesFromDirectory(agentsDir: string): Promise<void>
⋮----
// Load order first
⋮----
order: 0, // Will be set by order file
⋮----
// If no config.json exists, check if this is a project-level directory
// and if there's a corresponding global profile, merge project-level rule files
⋮----
// Apply order to profiles
⋮----
// If no order file exists, create one based on current order
⋮----
// Add profiles to the main profiles map
⋮----
private sanitizeAgentProfile(loadedProfile: AgentProfile, agentDirName: string): AgentProfile
⋮----
private async loadProfileFile(filePath: string, dirName: string): Promise<AgentProfile | null>
⋮----
// Sanitize profile with defaults
⋮----
// Discover and load rule files for this profile
⋮----
private async saveProfileToFile(profile: AgentProfile, filePath: string): Promise<void>
⋮----
// Create a copy of the profile without ruleFiles since they are dynamically discovered
⋮----
private async loadOrderFile(agentsDir: string): Promise<Map<string, number>>
⋮----
private async saveOrderFile(agentsDir: string, order: Map<string, number>): Promise<void>
⋮----
private applyOrderToProfiles(profilesMap: Map<string, AgentProfileContext>, order: Map<string, number>): Map<string, AgentProfileContext>
⋮----
// If both have order, use it
⋮----
// If only one has order, the one with order comes first
⋮----
// If neither has order, maintain original order (by name as fallback)
⋮----
private async createDefaultOrderFromProfiles(profilesMap: Map<string, AgentProfileContext>, agentsDir: string): Promise<void>
⋮----
public async createProfile(profile: AgentProfile, projectDir?: string): Promise<void>
⋮----
// Get existing directory names for collision handling
⋮----
// Derive unique directory name
⋮----
// Create directory and save config.json
⋮----
// Reload profiles to update the cache
⋮----
public async updateProfile(profile: AgentProfile): Promise<void>
⋮----
public async deleteProfile(profileId: string): Promise<void>
⋮----
// Reload profiles to update the cache
⋮----
public getProfile(profileId: string): AgentProfile | undefined
⋮----
// Check extension agents first (they can override file-based profiles)
⋮----
// Fall back to file-based profiles
⋮----
public resolveAgentProfile(id: string): AgentProfile | null
⋮----
// 1. Try matching by profile id (covers both file-based and extension agents)
⋮----
// 2. Try matching by profile name (case-insensitive)
⋮----
// 3. Try matching by dirName (folder name) for file-based profiles
⋮----
// 4. Try matching by dirName using slugified input
⋮----
// 5. Try matching by derived name from profile name (slugified + filenamified)
⋮----
private getOrderedProfiles(profileContexts: AgentProfileContext[]): AgentProfile[]
⋮----
// First, prioritize project profiles over global profiles
⋮----
// Then sort by order within the same scope
⋮----
private getGlobalProfiles(): AgentProfile[]
⋮----
public getAllProfiles(): AgentProfile[]
⋮----
// Get extension agents first (they can override file-based profiles)
⋮----
// Then get file-based profiles
⋮----
// Extension agents come first for override capability
⋮----
public getProjectProfiles(project: Project, includeGlobal = true, includeExtension = true): AgentProfile[]
⋮----
// Extension agents come first for override capability
⋮----
public async updateAgentProfilesOrder(agentProfiles: AgentProfile[]): Promise<void>
⋮----
// Group profiles by their projectDir (undefined/null = global)
⋮----
// Process each group separately
⋮----
// Create order map for this group
⋮----
// Determine the agents directory for this group
⋮----
// Save the order file for this group
⋮----
// Update the in-memory profiles for this group
⋮----
// Notify listeners
⋮----
private async mergeProjectRuleFilesIntoGlobalProfile(agentDirName: string, projectAgentsDir: string): Promise<void>
⋮----
// Check if there are project-level rule files
⋮----
return; // No project-level rule files to merge
⋮----
// Find the corresponding global profile by directory name
⋮----
return; // No global profile found with matching directory name
⋮----
// Create a new profile object with merged rule files
⋮----
// Update the profile context
⋮----
async dispose(): Promise<void>
⋮----
// Remove extension change listener
⋮----
// Clean up all directory watchers
⋮----
// Clear profiles
⋮----
async settingsChanged(oldSettings: SettingsData, newSettings: SettingsData): Promise<void>
⋮----
getDefaultAgentProfileId()

================
File: src/main/agent/agent.ts
================
import fs from 'fs/promises';
import path from 'path';
⋮----
import { v4 as uuidv4 } from 'uuid';
import {
  AgentProfile,
  ContextAssistantMessage,
  ContextCompactionType,
  ContextFile,
  ContextMessage,
  ContextToolMessage,
  ContextUserMessage,
  DefaultTaskState,
  Mode,
  ModelCallSettings,
  PromptContext,
  ProviderProfile,
  ToolApprovalState,
  UsageReportData,
} from '@common/types';
import {
  APICallError,
  type FilePart,
  type FinishReason,
  generateText,
  InvalidToolInputError,
  type ModelMessage,
  NoSuchToolError,
  Output,
  smoothStream,
  type StepResult,
  streamText,
  type TelemetryOptions,
  type ToolExecutionOptions,
  type ToolSet,
  type TypedToolResult,
  wrapLanguageModel,
} from 'ai';
import { extractProviderModel, extractServerNameToolName, extractTextContent } from '@common/utils';
// @ts-expect-error istextorbinary is not typed properly
import { isBinary } from 'istextorbinary';
import { fileTypeFromBuffer } from 'file-type';
import {
  HELPERS_TOOL_GROUP_NAME,
  POWER_TOOL_FILE_READ,
  POWER_TOOL_GROUP_NAME,
  TASKS_TOOL_GROUP_NAME,
  TASKS_TOOL_SEARCH_PARENT_TASK,
  TOOL_GROUP_NAME_SEPARATOR,
} from '@common/tools';
⋮----
import { createPowerToolset } from './tools/power';
import { createTodoToolset } from './tools/todo';
import { createSearchParentTaskTool, createTasksToolset } from './tools/tasks';
import { createAiderToolset } from './tools/aider';
import { createHelpersToolset } from './tools/helpers';
import { createMemoryToolset } from './tools/memory';
import { createSkillsToolset } from './tools/skills';
import { McpManager } from './mcp-manager';
import { ApprovalManager } from './tools/approval-manager';
import {
  estimateMessageTokens,
  extractPromptContextFromToolResult,
  findLastUserMessage,
  isNetworkError,
  readFileContent,
  safeStringifyToolOutput,
} from './utils';
import { extractReasoningMiddleware } from './middlewares/extract-reasoning-middleware';
import { CompactionLevel, generateCompactedSummary, getReloadableMessages, getSubagentOldResultIds, smartCompactMessages } from './compaction';
⋮----
import type { ToolsetToolEntry } from '@common/extensions';
import type { TextPart } from '@ai-sdk/provider-utils';
import type { z } from 'zod';
import type { LanguageModelV4 } from '@ai-sdk/provider';
⋮----
import { MemoryManager } from '@/memory/memory-manager';
import { PromptsManager } from '@/prompts';
import { AIDER_DESK_PROJECT_RULES_DIR } from '@/constants';
import { Task } from '@/task';
import { Store } from '@/store';
import logger from '@/logger';
import { optimizeMessages } from '@/agent/optimizer';
import { ModelManager } from '@/models/model-manager';
import { TelemetryManager } from '@/telemetry/telemetry-manager';
import { ResponseMessage } from '@/messages';
import { createSubagentsToolset } from '@/agent/tools/subagents';
import { AgentProfileManager } from '@/agent/agent-profile-manager';
import { McpConfigManager } from '@/agent/mcp-config-manager';
import { ExtensionManager } from '@/extensions/extension-manager';
⋮----
export class Agent
⋮----
constructor(
⋮----
private getTelemetrySettings(): TelemetryOptions
⋮----
// PostHog specific metadata
⋮----
private async getFilesContentForPrompt(files: ContextFile[], task: Task): Promise<
⋮----
// If binary, try to detect if it's an image using image-type and return base64
⋮----
// Read file as text
⋮----
// Add line numbers to content
⋮----
// Process the results and separate text files from images
⋮----
// Add to imageParts array
⋮----
// Add to textFileContents array
⋮----
private async getContextFilesMessages(task: Task, profile: AgentProfile, contextFiles: ContextFile[]): Promise<ModelMessage[]>
⋮----
// Filter out rule files as they are already included in the system prompt
⋮----
// Check if the file is within the rules directory
⋮----
// Separate readonly and editable files
⋮----
// Process readonly files first
⋮----
// Process editable files
⋮----
private async getWorkingFilesMessages(task: Task, contextFiles: ContextFile[]): Promise<ModelMessage[]>
⋮----
// Separate image files from non-image files
⋮----
// Add file list for non-image files
⋮----
// Add images as separate messages
⋮----
private async getAvailableTools(
    task: Task,
    mode: Mode,
    profile: AgentProfile,
    provider: ProviderProfile,
    model: string,
    messages?: ContextMessage[],
    resultMessages?: ContextMessage[],
    abortSignal?: AbortSignal,
    promptContext?: PromptContext,
): Promise<ToolSet>
⋮----
// MCP tools
⋮----
// Add search parent task tool for subtasks
⋮----
// Add helper tools
⋮----
// Add provider-specific tools
⋮----
// Add extension tools
⋮----
// Let extensions modify the complete toolset (built-in, MCP, and extension tools)
⋮----
private wrapToolsWithHooks(task: Task, profile: AgentProfile, toolSet: ToolSet, abortSignal?: AbortSignal, promptContext?: PromptContext): ToolSet
⋮----
// Use modified input from extension if provided
⋮----
async runAgent(
    task: Task,
    profile: AgentProfile,
    prompt: string | null,
    mode: Mode = 'agent',
    promptContext?: PromptContext,
    initialContextMessages?: ContextMessage[],
    initialContextFiles?: ContextFile[],
    systemPrompt?: string,
    includeInContext = true,
    abortSignal?: AbortSignal,
    images?: string[],
    skillsToActivate?: string[],
): Promise<ContextMessage[]>
⋮----
// Register an abort controller for the duration of the onAgentStarted dispatch so that
// agent runs driven by extensions (which return blocked: true) are visible via isRunning().
// This way Task.isPromptRunning() reports busy while such a run is in progress and
// prompts sent during it get queued instead of starting a concurrent run.
⋮----
// Reset the injected dispatch signal so the agent loop below creates its own abort
// controller as usual, but keep a signal explicitly set by an extension.
⋮----
// Activate skills after the user message is persisted to context so the
// agent sees: [history -> user message -> skill activation] and doesn't
// re-activate them itself.
⋮----
// Store resolved provider for use in retry logic
⋮----
// add user message and skill activation messages (skills right after the
// user message so the agent sees them already active and doesn't
// re-activate them itself)
⋮----
// Normalize messages for provider-specific requirements
⋮----
// repairToolCall function that attempts to repair tool calls
const repairToolCall = async (
⋮----
// If the tool doesn't exist, return a call to the helper tool
// to inform the LLM about the missing tool.
⋮----
// If the arguments are invalid, return a call to the helper tool
// to inform the LLM about the argument error.
⋮----
toolInput: JSON.stringify(error.toolInput), // Pass the problematic input
error: error.message, // Pass the validation error message
⋮----
// Attempt generic repair for other types of errors
⋮----
// Ensure args are stringified for the AI SDK tool call format
⋮----
: null; // Return null if the LLM couldn't repair the call
⋮----
// Get the model to use its temperature and max output tokens settings
⋮----
const getBaseModelCallParams = async () =>
⋮----
const getOptimizedMessages = async () =>
⋮----
const onStepEnd = async (stepResult: StepResult<typeof toolSet>) =>
⋮----
// Reset retry count when we get a response
⋮----
// The AI SDK swallows errors thrown in onStepEnd callbacks, so
// capture them here to stop the loop instead of silently re-sending
// the same prompt on the next iteration
⋮----
// @ts-expect-error checking keys in error
⋮----
// no-op
⋮----
// try again
⋮----
// Persist new messages incrementally after each iteration
⋮----
// Check for 'stop' with no usable output (model generated tokens, but no messages or reasoning-only message)
⋮----
// Check for 'stop' with trailing tool message
⋮----
// Dump the last 2 message roles to diagnose why the trailing tool message check may not match
⋮----
// if response messages have been modified by other means (e.g. hooks), we need to continue when the last message is a user message
⋮----
// Clean up abort controller only if we created it
⋮----
private stepShouldContinue(stepMessages: ContextMessage[]): boolean
⋮----
// A single assistant message with only reasoning parts and no text or tool calls is not a usable output
⋮----
private filterResultMessages(resultMessages: ContextMessage[])
⋮----
// All content parts are helpers — drop the message entirely
⋮----
// No helpers content — keep as-is
⋮----
// Some helpers content — keep with non-helpers parts only
⋮----
// All parts are helpers tool calls — drop the message entirely
⋮----
// Keep with non-helpers parts only
⋮----
// User and other roles — pass through unchanged
⋮----
private async getContextFilesAsToolCallMessages(task: Task, profile: AgentProfile, contextFiles: ContextFile[]): Promise<ModelMessage[]>
⋮----
// Check if power file_read tool is available (not 'Never')
⋮----
// Filter out rule files as they are already included in the system prompt
⋮----
// Separate readonly and editable files
⋮----
// If tool call format is not available, fall back to the old user message format
⋮----
// Use tool call format
const createToolCallAndResult = async (files: ContextFile[], readOnlyFile: boolean): Promise<ModelMessage[]> =>
⋮----
// Read all files and categorize them
⋮----
// If binary, try to detect if it's an image
⋮----
// Process non-binary files: each file gets its own assistant message with tool-call and tool-result
⋮----
// Process image files: keep current behavior
⋮----
// Process read-only files
⋮----
// Process editable files
⋮----
private async prepareMessages(task: Task, profile: AgentProfile, contextMessages: ContextMessage[], contextFiles: ContextFile[]): Promise<ModelMessage[]>
⋮----
// Add repo map if enabled
⋮----
// Add context files with content or just list of working files
⋮----
// Add message history before context files
⋮----
// Add message history after working files
⋮----
async generateText(
    modelId: string,
    systemPrompt: string,
    prompt: string,
    projectDir: string,
    messages: ContextMessage[] = [],
    abortable = true,
    abortSignal?: AbortSignal,
    sessionId?: string,
): Promise<string | undefined>
⋮----
async generateObject<T>(
    modelId: string,
    systemPrompt: string,
    prompt: string,
    schema: z.ZodType<T>,
    projectDir: string,
    messages: ContextMessage[] = [],
    abortable = true,
    abortSignal?: AbortSignal,
    sessionId?: string,
): Promise<T | undefined>
⋮----
async estimateTokens(task: Task, profile: AgentProfile): Promise<number>
⋮----
// Format tools for the prompt
⋮----
inputSchema: tool.inputSchema ? tool.inputSchema : '', // Get Zod schema description
⋮----
// Add tool definitions and system prompt to the beginning
⋮----
interrupt()
⋮----
isRunning()
⋮----
private removeUnfinishedStreamingMessages(task: Task, messageIds: Set<string>)
⋮----
private async processStep<TOOLS extends ToolSet>(
    currentResponseId: string,
    { content, reasoningText, text, toolCalls, toolResults, finishReason, rawFinishReason, usage, providerMetadata, response }: StepResult<TOOLS>,
    task: Task,
    provider: ProviderProfile,
    model: string,
    promptContext?: PromptContext,
    abortSignal?: AbortSignal,
): Promise<ContextMessage[]>
⋮----
reasoningText: reasoningText?.substring(0, 100), // Log truncated reasoning
text: text?.substring(0, 100), // Log truncated text
⋮----
const processToolResult = (toolResult: TypedToolResult<TOOLS>, isLast: boolean) =>
⋮----
// Update the existing tool message with the result
// Only attach usage report to the last tool message when there is no assistant message
⋮----
// Process successful tool results *after* sending text/reasoning and handling errors
⋮----
// @ts-expect-error the id is there
⋮----
private async compactMessagesIfNeeded(
    task: Task,
    profile: AgentProfile,
    provider: ProviderProfile,
    model: string,
    userRequestMessage: ContextUserMessage,
    contextMessages: ContextMessage[],
    contextFiles: ContextFile[],
    messages: ModelMessage[],
    resultMessages: ContextMessage[],
    promptContext?: PromptContext,
    abortSignal?: AbortSignal,
)
⋮----
// subagent cannot use Handoff, so we fallback to Smart compaction
⋮----
// The usage report covers only up to (and including) the last model call. Tool
// results appended after it are not in that report, yet they are part of the next
// prompt — so a single large result (e.g. parallel file reads) can silently push
// the request past the model's context limit. Add an estimate for the trailing
// messages so the threshold reflects the real next-prompt size.
⋮----
// percentage disabled → auto-compact off (consistent whether or not the context size is known)
⋮----
// model context size unknown — fall back to the absolute token threshold
⋮----
// reload messages after compacting
⋮----
// Perform handoff

================
File: src/main/agent/compaction.ts
================
import { v4 as uuidv4 } from 'uuid';
⋮----
import type { AgentProfile, ContextMessage, PromptContext } from '@common/types';
import type { Task } from '@/task';
⋮----
export const extractSummary = (content: string): string =>
⋮----
export const getSubagentOldResultIds = (resultMessages: ContextMessage[], userRequestMessageId: string): string[]
⋮----
export const getReloadableMessages = (messages: ContextMessage[]): ContextMessage[]
⋮----
export const buildCompactSummaryMessages = (
⋮----
export const generateCompactedSummary = async (
  userMessage: ContextMessage,
  allMessages: ContextMessage[],
  profile: AgentProfile,
  task: Task,
  promptContext?: PromptContext,
  abortSignal?: AbortSignal,
  getCompactConversationPrompt: (task: Task, customInstructions?: string) => Promise<string> = async () => '',
  generateText: (
    modelId: string,
    systemPrompt: string,
    prompt: string,
    projectDir: string,
    messages: ContextMessage[],
    abortable: boolean,
    abortSignal?: AbortSignal,
    sessionId?: string,
  ) => Promise<string | undefined> = async () => undefined,
): Promise<ContextMessage[]> =>

================
File: src/main/agent/index.ts
================


================
File: src/main/agent/mcp-config-manager.ts
================
import { FSWatcher, watch } from 'chokidar';
import { debounce } from 'lodash';
import { fileExists } from '@common/utils';
⋮----
import type { McpServerConfig, SettingsData } from '@common/types';
import type { Store } from '@/store';
⋮----
import { AIDER_DESK_DIR, AIDER_DESK_HOME_DIR, AIDER_DESK_MCP_SERVERS_FILE } from '@/constants';
import logger from '@/logger';
import { EventManager } from '@/events';
import { shouldUsePolling } from '@/utils/file-watch';
⋮----
const getGlobalMcpServersPath = (): string
const getProjectMcpServersPath = (projectDir: string): string
⋮----
interface McpServersFileContent {
  mcpServers: Record<string, McpServerConfig>;
}
⋮----
export class McpConfigManager
⋮----
private fileWatchers: Map<string, FSWatcher> = new Map(); // filePath -> watcher
⋮----
constructor(
⋮----
public async init(): Promise<void>
⋮----
public async initializeForProject(projectDir: string): Promise<void>
⋮----
public removeProject(projectDir: string): void
⋮----
public getGlobalServers(): Record<string, McpServerConfig>
⋮----
public getProjectServers(projectDir: string): Record<string, McpServerConfig>
⋮----
public getMergedServers(projectDir?: string | null): Record<string, McpServerConfig>
⋮----
public getState():
⋮----
public sendMcpServersUpdated(): void
⋮----
public async addServer(projectDir: string | undefined, name: string, config: McpServerConfig): Promise<void>
⋮----
public async updateServer(projectDir: string | undefined, oldName: string, name: string, config: McpServerConfig): Promise<void>
⋮----
public async removeServer(projectDir: string | undefined, name: string): Promise<void>
⋮----
public async replaceServers(projectDir: string | undefined, servers: Record<string, McpServerConfig>): Promise<void>
⋮----
private async saveServers(projectDir: string | undefined, servers: Record<string, McpServerConfig>): Promise<void>
⋮----
private async ensureFileExists(filePath: string): Promise<void>
⋮----
private async loadServersFromFile(filePath: string): Promise<Record<string, McpServerConfig>>
⋮----
private async writeServersToFile(filePath: string, servers: Record<string, McpServerConfig>): Promise<void>
⋮----
private async setupWatcherForFile(filePath: string): Promise<void>
⋮----
const reloadFunction = ()
⋮----
private async reloadFile(filePath: string): Promise<void>
⋮----
async settingsChanged(oldSettings: SettingsData, newSettings: SettingsData): Promise<void>
⋮----
async dispose(): Promise<void>

================
File: src/main/agent/mcp-manager.ts
================
import path from 'path';
import fs from 'fs/promises';
⋮----
import { v4 as uuidv4 } from 'uuid';
import { createMCPClient, UnauthorizedError as AiSdkUnauthorizedError, type CallToolResult, type ListToolsResult, type MCPClient } from '@ai-sdk/mcp';
import { type JSONValue } from '@ai-sdk/provider';
import { UnauthorizedError as McpSdkUnauthorizedError } from '@modelcontextprotocol/sdk/client/auth.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
import { type Tool, type ToolExecutionOptions, type ToolSet } from 'ai';
import {
  AgentProfile,
  McpOAuthStatus,
  McpServerConfig,
  McpTool,
  McpToolInputSchema,
  PromptContext,
  ToolApprovalState,
  type McpOAuthStatusData,
} from '@common/types';
import { LlmProviderName } from '@common/agent';
import { delay } from '@common/utils';
import { TOOL_GROUP_NAME_SEPARATOR } from '@common/tools';
⋮----
import { ApprovalManager } from './tools/approval-manager';
import { McpOAuthManager } from './mcp-oauth-manager';
import { truncateToolResult } from './utils';
⋮----
import logger from '@/logger';
import { AIDER_DESK_CACHE_DIR } from '@/constants';
import { translate } from '@/utils/i18n';
import { Task } from '@/task';
⋮----
export interface McpToolsCacheEntry {
  tools: McpTool[];
  cachedAt: number;
}
⋮----
export interface McpToolsCache {
  version: number;
  servers: Record<string, McpToolsCacheEntry>;
}
⋮----
export class McpAuthenticationRequiredError extends Error
⋮----
constructor(serverName: string, language?: string)
⋮----
interface McpConnector {
  client: MCPClient;
  serverName: string;
  tools: McpTool[];
  toolDefinitions: ListToolsResult;
  serverConfig: McpServerConfig;
}
⋮----
type McpClientTransport = Parameters<typeof createMCPClient>[0]['transport'];
⋮----
export class McpManager
⋮----
constructor(
⋮----
async init()
⋮----
async createToolset(
    task: Task,
    profile: AgentProfile,
    providerName: LlmProviderName,
    mcpServers: Record<string, McpServerConfig>,
    approvalManager: ApprovalManager,
    promptContext?: PromptContext,
): Promise<ToolSet>
⋮----
private wrapMcpTool(
    serverName: string,
    toolName: string,
    task: Task,
    profile: AgentProfile,
    toolDef: Tool,
    approvalManager: ApprovalManager,
    lastToolCallTimeRef: { value: number },
    promptContext?: PromptContext,
): Tool
⋮----
const execute = async (args: unknown, options: ToolExecutionOptions<unknown>) =>
⋮----
private describeToolResponse(response: CallToolResult): Record<string, unknown>
⋮----
private createMcpErrorResult(message: string): CallToolResult
⋮----
private isMcpCallToolResult(value: unknown): value is CallToolResult
⋮----
private stripUnsupportedSchemaKeywords(schema: Record<string, unknown>): Record<string, unknown>
⋮----
const processObject = (obj: Record<string, unknown>): Record<string, unknown> =>
⋮----
private normalizeToolInputSchema(inputSchema: ListToolsResult['tools'][number]['inputSchema']): McpToolInputSchema
⋮----
private fixInputSchema(provider: LlmProviderName, inputSchema: McpToolInputSchema): McpToolInputSchema
⋮----
private async initMcpConnectors(
    mcpServers: Record<string, McpServerConfig>,
    projectDir: string | null,
    taskDir: string | null,
    forceReload = false,
    enabledServers?: string[],
): Promise<McpConnector[]>
⋮----
// Ensure index is within bounds for serverNames, though it should be if Object.values and Object.keys maintain order
⋮----
private async initMcpConnector(
    projectDir: string | null,
    taskDir: string | null,
    serverName: string,
    config: McpServerConfig,
    forceReload = false,
    initId?: string,
    scope: string = 'global',
): Promise<McpConnector>
⋮----
async close(): Promise<void>
⋮----
private interpolateServerConfig(serverConfig: McpServerConfig, projectDir: string | null, taskDir: string | null): McpServerConfig
⋮----
const interpolateValue = (value: string): string =>
⋮----
private hasInterpolation(value: string, pattern: string): boolean
⋮----
private configHasInterpolation(serverConfig: McpServerConfig, pattern: string): boolean
⋮----
private calculateServerScope(serverConfig: McpServerConfig, projectDir: string | null, taskDir: string | null, serverName: string): string
⋮----
// Has ${projectDir} only: scope = ${projectDir || 'global'}
⋮----
// Has both ${projectDir} and ${taskDir}: scope = ${projectDir}:${taskDir}
⋮----
private createClient(transport: McpClientTransport): Promise<MCPClient>
⋮----
private async createMcpConnector(serverName: string, config: McpServerConfig, projectDir: string | null, taskDir: string | null): Promise<McpConnector>
⋮----
// If command is 'docker', ensure '--init' is present after 'run'
// so the container properly handles SIGINT and SIGTERM
⋮----
// Find the index of 'run'. This handles both 'docker run' and 'docker container run'.
⋮----
// Verify it's likely the actual 'run' subcommand
// e.g., 'run' is the first arg, or it follows 'container'
⋮----
async getMcpServerTools(serverName: string, config?: McpServerConfig): Promise<McpTool[] | null>
⋮----
async getOAuthStatus(config: McpServerConfig): Promise<McpOAuthStatusData>
⋮----
async startOAuth(serverName: string, config: McpServerConfig): Promise<string>
⋮----
async completeOAuth(code: string, state: string): Promise<void>
⋮----
async cancelOAuth(state: string): Promise<void>
⋮----
async disconnectOAuth(config: McpServerConfig): Promise<void>
⋮----
private async invalidateConnectorsForUrl(serverUrl: string): Promise<void>
⋮----
private compareServerConfig(config: McpServerConfig, otherConfig: McpServerConfig)
⋮----
private async loadToolsCache(): Promise<void>
⋮----
async saveToolsCache(): Promise<void>
⋮----
getCachedTools(serverName: string): McpTool[] | null
⋮----
updateToolsCache(serverName: string, tools: McpTool[]): void
⋮----
private getPoolKey(scope: string, serverName: string): string
⋮----
private getPooledConnector(scope: string, serverName: string): Promise<McpConnector> | undefined
⋮----
private setPooledConnector(scope: string, serverName: string, connector: Promise<McpConnector>, config: McpServerConfig): void
⋮----
private async closeAllPooledConnectors(): Promise<void>
⋮----
async reloadAllServers(mcpServers: Record<string, McpServerConfig>, force: boolean): Promise<void>
⋮----
async reloadSingleServer(serverName: string, config: McpServerConfig): Promise<McpTool[]>

================
File: src/main/agent/mcp-oauth-manager.ts
================
import fs from 'fs/promises';
import path from 'path';
import { randomBytes } from 'crypto';
⋮----
import {
  auth,
  type OAuthAuthorizationServerInformation,
  type OAuthClientInformation,
  type OAuthClientMetadata,
  type OAuthClientProvider,
  type OAuthTokens,
} from '@ai-sdk/mcp';
import { McpOAuthStatus, type McpOAuthStatusData } from '@common/types';
⋮----
import type {
  OAuthClientProvider as McpSdkOAuthClientProvider,
  OAuthDiscoveryState as McpSdkOAuthDiscoveryState,
} from '@modelcontextprotocol/sdk/client/auth.js';
⋮----
import { AIDER_DESK_DATA_DIR, MCP_OAUTH_CALLBACK_PATH, SERVER_PORT } from '@/constants';
import { isElectron } from '@/app';
import logger from '@/logger';
⋮----
type McpOAuthRecord = {
  serverUrl: string;
  oauthRequired: boolean;
  tokens?: OAuthTokens;
  clientInformation?: OAuthClientInformation;
  authorizationServerInformation?: OAuthAuthorizationServerInformation;
  discoveryState?: McpSdkOAuthDiscoveryState;
  codeVerifier?: string;
  state?: string;
  authorizationUrl?: string;
  authorizationStartedAt?: number;
};
⋮----
type PlainStoreData = {
  version: number;
  records: Record<string, McpOAuthRecord>;
};
⋮----
type EncryptedStoreData = {
  version: number;
  encrypted: string;
};
⋮----
type StoreData = PlainStoreData | EncryptedStoreData;
⋮----
const normalizeServerUrl = (serverUrl: string): string =>
⋮----
const isLoopbackHostname = (hostname: string): boolean
⋮----
export class McpOAuthManager
⋮----
constructor(private readonly storeFilePath = MCP_OAUTH_STORE_FILE)
⋮----
init(): Promise<void>
⋮----
async getProvider(serverUrl: string): Promise<OAuthClientProvider>
⋮----
async getMcpSdkProvider(serverUrl: string): Promise<McpSdkOAuthClientProvider>
⋮----
createValidatedFetch(serverUrl: string): typeof fetch
⋮----
async getStatus(serverUrl: string): Promise<McpOAuthStatusData>
⋮----
async startAuthorization(serverUrl: string): Promise<string | null>
⋮----
async completeAuthorization(code: string, state: string): Promise<string>
⋮----
async cancelAuthorization(state: string): Promise<void>
⋮----
async disconnect(serverUrl: string): Promise<void>
⋮----
async clearPendingAuthorization(serverUrl: string): Promise<void>
⋮----
private async createAndSaveState(serverUrl: string): Promise<string>
⋮----
private validateOAuthUrl(serverUrl: string | URL, candidateUrl: string | URL): void
⋮----
private get redirectUrl(): string
⋮----
private ensureRecord(serverUrl: string): McpOAuthRecord
⋮----
private async load(): Promise<void>
⋮----
private save(): Promise<void>
⋮----
private async getSafeStorage()

================
File: src/main/agent/optimizer.ts
================
import { type AgentProfile, AutonomyMode, InvocationMode, ToolApprovalState } from '@common/types';
import { getSubagentId, isSubagentEnabled } from '@common/agent';
import { cloneDeep } from 'lodash';
import { type FilePart, type ModelMessage, type TextPart, type ToolContent, type ToolResultPart, type UserModelMessage } from 'ai';
import {
  AIDER_TOOL_GROUP_NAME,
  AIDER_TOOL_RUN_PROMPT,
  MEMORY_TOOL_GROUP_NAME,
  MEMORY_TOOL_RETRIEVE,
  MEMORY_TOOL_STORE,
  SUBAGENTS_TOOL_GROUP_NAME,
  SUBAGENTS_TOOL_RUN_TASK,
  TODO_TOOL_GET_ITEMS,
  TODO_TOOL_GROUP_NAME,
  TOOL_GROUP_NAME_SEPARATOR,
} from '@common/tools';
import { extractTextContent } from '@common/utils';
⋮----
import { convertMcpResultToModelOutput } from './utils';
⋮----
import logger from '@/logger';
import { type CacheControl } from '@/models';
import { Task } from '@/task';
import { ExtensionManager } from '@/extensions';
⋮----
/**
 * Optimizes the messages before sending them to the LLM. This should reduce the token count and improve the performance.
 */
export const optimizeMessages = async (
  messages: ModelMessage[],
  cacheControl?: CacheControl,
  task: Task | null = null,
  profile: AgentProfile | null = null,
  projectProfiles: AgentProfile[] = [],
  userRequestMessageIndex: number = -1,
  extensionManager?: ExtensionManager,
): Promise<ModelMessage[]> =>
⋮----
const addImportantReminders = async (
  task: Task,
  profile: AgentProfile,
  projectProfiles: AgentProfile[],
  userRequestMessageIndex: number,
  messages: ModelMessage[],
  extensionManager?: ExtensionManager,
): Promise<ModelMessage[]> =>
⋮----
// Add reminder about automatic subagents
⋮----
// Add reminder about worktree mode
⋮----
// Dispatch event to extensions to allow modification of reminders
⋮----
/**
 * For run_prompt tool, which returns `responses` array, we should replace this array with empty array.
 */
const optimizeAiderMessages = (messages: ModelMessage[]): ModelMessage[] =>
⋮----
// @ts-expect-error result is expected to have responses
⋮----
// @ts-expect-error result is expected to have promptContext
⋮----
// ignore
⋮----
/**
 * For subagent tool, which now returns an array of messages, we should replace this array with only the last message for LLM processing.
 */
const optimizeSubagentMessages = (messages: ModelMessage[]): ModelMessage[] =>
⋮----
// @ts-expect-error result is expected to have messages
⋮----
// ignore
⋮----
/**
 * Converts tool results containing image data (from MCP tools that return
 * content with image parts) into a user message with image parts, and
 * replaces the tool result output with text-only content.
 *
 * This is necessary because the OpenAI API (and OpenAI-compatible providers)
 * only supports string content in tool results — images cannot be sent
 * inline. By moving images to a user message, all providers can handle
 * them via their native image support (e.g. OpenAI `image_url`, Anthropic
 * `image` with base64, Google `inlineData`).
 */
export const convertImageToolResults = (messages: ModelMessage[]): ModelMessage[] =>
⋮----
// Handle content-type outputs — extract file/image parts into user messages
// since not all providers support file parts in tool results natively
⋮----
// If images were extracted, add a user message with the image parts
⋮----
/**
 * Some models (Gemini Flash) are trying to call the same tool multiple times in a row.
 * This function detects this pattern (assistant tool call -> tool result -> same assistant tool call)
 * and modifies the result of the second tool call to return an error, preventing a loop.
 */
const removeDuplicateToolCalls = (messages: ModelMessage[]): ModelMessage[] =>
⋮----
// Need at least 4 messages for the pattern: assistant, tool, assistant, tool
⋮----
const newMessages = [...messages]; // Create a mutable copy
⋮----
// Iterate up to the point where the pattern can start
⋮----
// Check for the pattern: assistant(call) -> tool(result) -> assistant(call) -> tool(result)
⋮----
// Ensure both assistant messages contain tool calls
⋮----
// Compare the tool calls
⋮----
// This is a duplicate call. Modify the fourth message (the result of the duplicate call).
⋮----
// Match the tool result part to the duplicate tool call id
⋮----
// Replace the old tool message with the modified one
⋮----
// To prevent overlapping checks and unnecessary work, we can advance the index.
// Since we've processed a block of 4, we can safely jump ahead.

================
File: src/main/agent/smart-compaction.ts
================
import { ContextMessage, ContextToolMessage, ContextAssistantMessage, TextPart, ToolResultPart, ToolResultOutput } from '@common/types';
import {
  POWER_TOOL_GROUP_NAME,
  POWER_TOOL_FILE_EDIT,
  POWER_TOOL_FILE_READ,
  POWER_TOOL_FILE_WRITE,
  POWER_TOOL_GLOB,
  POWER_TOOL_GREP,
  POWER_TOOL_SEMANTIC_SEARCH,
  POWER_TOOL_BASH,
  POWER_TOOL_FETCH,
} from '@common/tools';
import { extractServerNameToolName } from '@common/utils';
import { v4 as uuidv4 } from 'uuid';
⋮----
import { truncateToolResult } from '@/agent/utils';
import logger from '@/logger';
⋮----
export enum CompactionLevel {
  One = 1,
  Two = 2,
  Three = 3,
  Four = 4,
  Five = 5,
  Max = CompactionLevel.Five,
}
⋮----
export type SmartCompactionPass =
  | 'erroredTools'
  | 'fileEdits'
  | 'staleFileReads'
  | 'fileReads'
  | 'searches'
  | 'semanticSearches'
  | 'bash'
  | 'fetch'
  | 'otherTools'
  | 'verboseToolCalls'
  | 'reasoning';
⋮----
export type SmartCompactionPassSelection = Partial<Record<SmartCompactionPass, boolean>>;
⋮----
type ToolInfo = {
  messageIndex: number;
  toolCallId: string;
  toolName: string;
  serverName: string;
  input: unknown;
  output: ToolResultOutput | undefined;
};
⋮----
type AssistantToolCallInfo = {
  messageId: string;
  messageIndex: number;
  partIndex: number;
  toolCallId: string;
  toolName: string;
  input: unknown;
};
⋮----
const extractFilePath = (args: unknown): string | undefined =>
⋮----
const getToolOutputText = (output: ToolResultOutput | undefined): string =>
⋮----
const isErrorResult = (output: ToolResultOutput | undefined): boolean =>
⋮----
const isNoOpResult = (output: ToolResultOutput | undefined): boolean =>
⋮----
const isPowerTool = (serverName: string): boolean =>
⋮----
const getToolInfoFromToolMessage = (message: ContextToolMessage): ToolInfo[] =>
⋮----
const findAssistantToolCall = (messages: ContextMessage[], toolCallId: string): AssistantToolCallInfo | undefined =>
⋮----
const cloneMessages = (messages: ContextMessage[]): ContextMessage[] =>
⋮----
const removeToolCallFromAssistant = (messages: ContextMessage[], toolCallId: string): void =>
⋮----
const removeToolResult = (messages: ContextMessage[], toolCallId: string): void =>
⋮----
const getProtectedStartIndex = (messages: ContextMessage[], protectedMessageCount: number): number =>
⋮----
const mergeConsecutiveAssistantMessages = (messages: ContextMessage[]): ContextMessage[] =>
⋮----
export const truncateNonPowerToolResults = async (
  messages: ContextMessage[],
  protectedMessageCount = 10,
  compactionLevel = CompactionLevel.One,
): Promise<ContextMessage[]> =>
⋮----
export const smartCompactMessages = async (
  messages: ContextMessage[],
  protectedMessageCount = 10,
  compactionLevel = CompactionLevel.One,
  passes?: SmartCompactionPassSelection,
): Promise<ContextMessage[]> =>
⋮----
const isPassEnabled = (pass: SmartCompactionPass): boolean
⋮----
// explicitly selected passes (manual compaction) respect the safe window and run on any level;
// automatic compaction keeps the extended protection window and level gates
⋮----
export const removeErroredTools = (messages: ContextMessage[], protectedMessageCount = 10): ContextMessage[] =>
⋮----
export const collapseFileEdits = (messages: ContextMessage[], protectedMessageCount = 10): ContextMessage[] =>
⋮----
export const removeStaleFileReads = (messages: ContextMessage[], protectedMessageCount = 10): ContextMessage[] =>
⋮----
// Check for synthetic <file-edited> messages from collapseFileEdits
⋮----
export const compactFileReads = (messages: ContextMessage[], protectedMessageCount = 10, compactionLevel = CompactionLevel.One): ContextMessage[] =>
⋮----
export const removeObsoleteSearches = (messages: ContextMessage[], protectedMessageCount = 10, compactionLevel = CompactionLevel.One): ContextMessage[] =>
⋮----
// Check for synthetic <file-edited> messages from collapseFileEdits
⋮----
export const compactSemanticSearches = (messages: ContextMessage[], protectedMessageCount = 10, compactionLevel = CompactionLevel.One): ContextMessage[] =>
⋮----
export const compactBashOutputs = (messages: ContextMessage[], protectedMessageCount = 10, compactionLevel = CompactionLevel.One): ContextMessage[] =>
⋮----
// Not JSON, leave as-is
⋮----
export const removeVerboseToolCalls = (messages: ContextMessage[], protectedMessageCount = 10, compactionLevel = CompactionLevel.One): ContextMessage[] =>
⋮----
export const removeReasoningFromAssistant = (
  messages: ContextMessage[],
  protectedMessageCount = 10,
  compactionLevel = CompactionLevel.One,
): ContextMessage[] =>
⋮----
export const redactFetchOutputs = (messages: ContextMessage[], protectedMessageCount = 10): ContextMessage[] =>

================
File: src/main/agent/subagent.ts
================
import { AgentProfile, ContextMemoryMode, ContextMessage, PromptContext } from '@common/types';
import { DEFAULT_AGENT_PROFILE, getSubagentId, isSubagentEnabled } from '@common/agent';
import { SUBAGENTS_TOOL_GROUP_NAME, SUBAGENTS_TOOL_RUN_TASK } from '@common/tools';
import { extractServerNameToolName } from '@common/utils';
import { v4 as uuidv4 } from 'uuid';
⋮----
import { Task } from '@/task';
import logger from '@/logger';
⋮----
export type SubagentGroupName =
  | string
  | {
      key: string;
      params: Record<string, string>;
    };
⋮----
export type RunSubagentTaskParams = {
  task: Task;
  targetSubagent: AgentProfile;
  prompt: string;
  description?: string;
  abortSignal?: AbortSignal;
  contextMessages?: ContextMessage[];
  currentMessages?: ContextMessage[];
  onStarted?: (promptContext: PromptContext) => void | Promise<void>;
};
⋮----
export type RunSubagentTaskResult =
  | {
      status: 'success';
      messages: ContextMessage[];
      promptContext: PromptContext;
    }
  | {
      status: 'cancelled';
      messages: ContextMessage[];
      promptContext: PromptContext;
    }
  | {
      status: 'error';
      error: string;
      promptContext: PromptContext;
    };
⋮----
export const getEnabledSubagents = (profiles: AgentProfile[], mainAgentProfile: AgentProfile): AgentProfile[] =>
⋮----
export const findEnabledSubagent = (
  enabledSubagents: AgentProfile[],
  subagentId: string,
  options: { matchByName?: boolean } = {},
): AgentProfile | undefined =>
⋮----
export const buildRestrictedSubagentProfile = (targetSubagent: AgentProfile): AgentProfile =>
⋮----
export const createSubagentPromptContext = (targetSubagent: AgentProfile, description?: string): PromptContext =>
⋮----
export const getRunningGroupName = (targetSubagent: AgentProfile, description?: string): SubagentGroupName =>
⋮----
export const getCancelledGroupName = (targetSubagent: AgentProfile, description?: string): SubagentGroupName =>
⋮----
export const getCompletedGroupName = (targetSubagent: AgentProfile, description?: string): SubagentGroupName =>
⋮----
const getSubagentContextMessages = (
  subagentId: string,
  contextMemory: ContextMemoryMode,
  contextMessages: ContextMessage[],
  currentMessages: ContextMessage[],
): ContextMessage[] =>
⋮----
const findMessagesForToolCallId = (toolCallId: string) =>
⋮----
// @ts-expect-error part.output.value is expected to be in the output
⋮----
// @ts-expect-error subagentId is expected to be in the input
⋮----
// @ts-expect-error prompt is expected to be in the input
⋮----
export const runSubagentTask = async ({
  task,
  targetSubagent,
  prompt,
  description,
  abortSignal,
  contextMessages = [],
  currentMessages = [],
  onStarted,
}: RunSubagentTaskParams): Promise<RunSubagentTaskResult> =>

================
File: src/main/agent/utils.ts
================
import os, { tmpdir } from 'os';
import fs from 'fs/promises';
import path from 'path';
import { createReadStream, createWriteStream } from 'fs';
import readline from 'readline';
import { StringDecoder } from 'string_decoder';
⋮----
// @ts-expect-error istextorbinary is not typed properly
import { isBinary } from 'istextorbinary';
import { encode } from 'gpt-tokenizer/model/gpt-4o';
import { z } from 'zod';
import { ContextMessage, ContextUserMessage, JSONValue, MessageRole, PromptContext, ToolResultOutput } from '@common/types';
⋮----
/**
 * Zod schema that coerces string values to booleans before validation.
 * Handles the case where LLMs send boolean parameters as strings ("true"/"false").
 */
⋮----
/**
 * Extracts PromptContext from a tool result if available.
 * @param toolResult - The tool result object to extract PromptContext from
 * @returns PromptContext if found, undefined otherwise
 */
export const extractPromptContextFromToolResult = (toolResult: unknown): PromptContext | undefined =>
⋮----
export const findLastUserMessage = (messages: ContextMessage[]): ContextUserMessage | undefined =>
⋮----
/**
 * Expands a tilde (~) at the beginning of a path to the user's home directory.
 * @param filePath - The file path to expand
 * @returns The expanded path with ~ replaced by the home directory
 */
export const expandTilde = (filePath: string): string =>
⋮----
/**
 * Accumulates process output with a constant memory footprint.
 *
 * Keeps the first/last slices of the output in memory and spills the full
 * content to a temporary file once it exceeds the in-memory threshold, so
 * commands producing gigabytes of output cannot exhaust the V8 heap.
 */
export class BoundedOutputAccumulator
⋮----
append(chunk: Buffer | string): void
⋮----
private appendText(text: string): void
⋮----
private startSpill(initialContent: string): void
⋮----
private writeToSpill(text: string): void
⋮----
didSpill(): boolean
⋮----
getSpillFilePath(): string | null
⋮----
getTotalChars(): number
⋮----
getPreview(maxChars: number): string
⋮----
async finish(): Promise<string>
⋮----
private composeResult(): string
⋮----
async dispose(): Promise<void>
⋮----
const done = (): void =>
⋮----
// file may not exist
⋮----
/**
 * Serializes a value to JSON with a hard character budget. When the natural
 * serialization exceeds the budget, returns a valid-JSON envelope containing
 * a preview instead of the oversized payload.
 */
export const stringifyWithBudget = (value: unknown, maxChars = TOOL_RESULT_MAX_JSON_CHARS):
⋮----
export const safeJsonStringify = (value: unknown, maxChars = TOOL_RESULT_MAX_JSON_CHARS): string =>
⋮----
/**
 * Checks if a content array (e.g. MCP tool result parts) contains any non-text parts
 * (e.g. media, image, file). These parts carry binary data (base64) that must NOT be
 * truncated — truncating them corrupts the data and causes the model to receive a
 * broken text fragment instead of a proper multimodal attachment.
 */
export const contentArrayHasNonTextParts = (content: unknown): boolean =>
⋮----
/**
 * Checks if a value contains non-text parts (e.g. media, image, file) in its content array.
 * Handles multiple formats: direct arrays, { content: [...] }, and { type: 'content', value: [...] }.
 */
const hasNonTextContentParts = (value: unknown): boolean =>
⋮----
// { content: [...] } (MCP / extension tool result format)
⋮----
// { type: 'content', value: [...] } (wrapped ToolResultOutput)
⋮----
/**
 * Safely serializes a tool result output to JSON for storage in context/IPC.
 * When the output contains non-text parts (media, image, file), the full payload
 * is preserved via JSON.stringify to avoid corrupting binary data. For text-only
 * outputs, falls back to stringifyWithBudget for OOM protection.
 */
export const safeStringifyToolOutput = (value: unknown, maxChars = TOOL_RESULT_MAX_JSON_CHARS): string =>
⋮----
/**
 * Reads a file and returns its content with optional line numbering and line range.
 * Files larger than LARGE_FILE_READ_THRESHOLD_BYTES are streamed instead of loaded
 * into memory in full, keeping the heap footprint bounded.
 * @param absolutePath - The absolute path to the file
 * @param withLines - Whether to return the file content with line numbers in format "lineNumber|content"
 * @param lineOffset - The starting line number (0-based) to begin reading from
 * @param lineLimit - The maximum number of lines to read
 * @param sizeLimit - Maximum file size in KB; files exceeding this are truncated. Defaults to 5% of lineLimit
 * @returns The file content as a string, formatted according to the parameters
 * @throws Error if the file is binary or cannot be read
 */
⋮----
export const readFileContent = async (
  absolutePath: string,
  withLines = false,
  lineOffset = 0,
  lineLimit = 1000,
  sizeLimit = Math.max(50, 0.05 * lineLimit),
): Promise<string> =>
⋮----
// stat failed, fall through to regular readFile path
⋮----
const readLargeFileContent = async (absolutePath: string, withLines: boolean, lineOffset: number, lineLimit: number, sizeLimit: number): Promise<string> =>
⋮----
const finish = ()
⋮----
export const truncateToolResult = async (
  content: string,
  maxLines = 1000,
  maxSizeKB = 50,
  maxTokens = 50000,
  saveToFile = true,
  truncationSuffix?: string,
): Promise<string> =>
⋮----
// skip the expensive tokenizer pass when a cheaper limit is already exceeded
⋮----
const getSuffix = () =>
⋮----
export const isNetworkError = (error: unknown): boolean =>
⋮----
/**
 * Converts MCP tool result output to AI SDK's ToolResultOutput format,
 * extracting image parts into `content`-type output with `file` parts.
 * The AI SDK handles per-provider conversion (e.g. OpenAI `input_image`,
 * Anthropic `image` with base64, Google `inlineData`).
 *
 * Handles MCP content parts of type `image`, `image-data`, and `media`,
 * with `data` or `image` fields, and optional `mimeType`/`mediaType`.
 */
export const convertMcpResultToModelOutput = (output: unknown): ToolResultOutput =>
⋮----
type CountableMessage = { role: string; content: unknown };
⋮----
const extractToolResultText = (output: unknown): string =>
⋮----
export const estimateMessageTokens = (messages: CountableMessage[]): number =>

================
File: src/main/connector/connector-auth.ts
================
import { randomBytes } from 'crypto';

================
File: src/main/connector/connector-manager.ts
================
import { Server as HttpServer } from 'http';
⋮----
import { ModelsData, QuestionData, TokensInfoData } from '@common/types';
import { Server, Socket } from 'socket.io';
⋮----
import logger from '@/logger';
import {
  isAddFileMessage,
  isAskQuestionMessage,
  isDropFileMessage,
  isInitMessage,
  isPromptFinishedMessage,
  isResponseMessage,
  isSetModelsMessage,
  isTokensInfoMessage,
  isUpdateAutocompletionMessage,
  isUpdateContextFilesMessage,
  isUpdateRepoMapMessage,
  isUseCommandOutputMessage,
  LogMessage,
  Message,
  isAddMessageMessage,
  isSubscribeEventsMessage,
  isUnsubscribeEventsMessage,
  isWriteToTerminalMessage,
  isReadonlySubscribeEventsMessage,
  isReadonlyUnsubscribeEventsMessage,
} from '@/messages';
import { Connector } from '@/connector/connector';
import { ProjectManager } from '@/project';
import { EventManager } from '@/events';
import { EventsHandler } from '@/events-handler';
import { Store } from '@/store';
import { createCorsOriginValidator } from '@/server/cors';
import { READONLY_MODE } from '@/constants';
import { CONNECTOR_TOKEN } from '@/connector/connector-auth';
⋮----
export class ConnectorManager
⋮----
constructor(
    httpServer: HttpServer,
    private readonly projectManager: ProjectManager,
    private readonly eventManager: EventManager,
    private readonly store: Store,
    private readonly eventsHandler: EventsHandler,
)
⋮----
private get isReadonlyMode(): boolean
⋮----
public init(httpServer: HttpServer): void
⋮----
pingTimeout: 600_000, // 10 minutes
maxHttpBufferSize: 1e8, // Increase payload size to 100 MB
⋮----
// Log when Socket.IO server is ready to accept connections
// The 'connection' event fires after the namespace handshake is complete
⋮----
public async close()
⋮----
// Check if there's an existing connector with the same baseDir and taskId
// This can happen on reconnection when the old connector wasn't properly cleaned up
⋮----
// Handle the case where taskId is not available, e.g., log an error or apply to all tasks

================
File: src/main/connector/connector.ts
================
import path from 'path';
⋮----
import { AiderRunOptions, ContextFile, EditFormat, FileEdit, MessageRole, Mode, ModelInfo, PromptContext } from '@common/types';
import { Socket } from 'socket.io';
⋮----
import logger from '@/logger';
import {
  AddFileMessage,
  AddMessageMessage,
  AnswerQuestionMessage,
  ApplyEditsMessage,
  CompactConversationMessage,
  DropFileMessage,
  InterruptResponseMessage,
  Message,
  MessageAction,
  PromptMessage,
  RequestContextInfoMessage,
  RunCommandMessage,
  SetModelsMessage,
  UpdateEnvVarsMessage,
  UpdateModelsInfoMessage,
} from '@/messages';
⋮----
export class Connector
⋮----
constructor(
⋮----
public sendPromptMessage(
    prompt: string,
    promptContext: PromptContext,
    mode: Mode | null = null,
    architectModel: string | null = null,
    messages: { role: MessageRole; content: string }[] = [],
    files: ContextFile[] = [],
    options?: AiderRunOptions,
): void
⋮----
public sendSetModelsMessage(mainModel: string, weakModel: string | null, editFormat: EditFormat, environmentVariables?: Record<string, string>): void
⋮----
public sendRunCommandMessage(command: string, messages:
⋮----
public sendAddMessageMessage(role: MessageRole = MessageRole.User, content: string, acknowledge = true)
⋮----
public sendInterruptResponseMessage()
⋮----
public sendApplyEditsMessage(edits: FileEdit[])
⋮----
public sendCompactConversationMessage(customInstructions?: string)
⋮----
public sendUpdateEnvVarsMessage(environmentVariables: Record<string, unknown>)
⋮----
public sendRequestTokensInfoMessage(messages:
⋮----
public sendUpdateModelsInfoMessage(modelsInfo: Record<string, ModelInfo>)

================
File: src/main/connector/index.ts
================


================
File: src/main/custom-commands/custom-command-manager.ts
================
import fs from 'fs/promises';
import path from 'path';
import { exec } from 'child_process';
import { promisify } from 'util';
⋮----
import { watch, FSWatcher } from 'chokidar';
import { loadFront } from 'yaml-front-matter';
import debounce from 'lodash/debounce';
⋮----
import type { AutonomyMode, Command, CommandArgument, CommandsData, CustomCommand, SettingsData } from '@common/types';
import type { EventManager } from '@/events';
import type { ExtensionManager, ExtensionsChangeListener } from '@/extensions';
import type { Store } from '@/store';
⋮----
import { AIDER_DESK_COMMANDS_DIR, AIDER_DESK_HOME_DIR } from '@/constants';
import logger from '@/logger';
import { Project } from '@/project';
import { shouldUsePolling } from '@/utils/file-watch';
⋮----
// Constants for shell command formatting
⋮----
const SHELL_COMMAND_TIMEOUT = 30000; // 30 seconds
⋮----
export class ShellCommandError extends Error
⋮----
constructor(
    message: string,
    public readonly command: string,
    public readonly stderr: string,
    public readonly exitCode?: number,
)
⋮----
interface ExecError extends Error {
  stdout?: string;
  stderr?: string;
  code?: number;
}
⋮----
export class CustomCommandManager
⋮----
public async start(): Promise<void>
⋮----
private async initializeCommands(): Promise<void>
⋮----
// Load project-specific commands (these will overwrite global ones with same name)
⋮----
private async setupFileWatchers(): Promise<void>
⋮----
// Clean up existing watchers
⋮----
// Watch global commands directory
⋮----
// Watch project-specific commands directory
⋮----
private async setupWatcherForDirectory(commandsDir: string): Promise<void>
⋮----
// Create directory if it doesn't exist
⋮----
private async reloadCommands(): Promise<void>
⋮----
public sendCommandsUpdated(): void
⋮----
private async loadCommandsFromDir(commandsDir: string, commands: Map<string, CustomCommand>): Promise<void>
⋮----
private async loadCommandsRecursively(dir: string, commands: Map<string, CustomCommand>, baseDir: string, prefix = ''): Promise<void>
⋮----
private async loadCommandFile(filePath: string, name: string, commands: Map<string, CustomCommand>): Promise<void>
⋮----
// Optionally: send error to chat window via IPC or callback
⋮----
getCommand(name: string): CustomCommand | undefined
⋮----
getAllCommands(): CommandsData
⋮----
// Get extension commands first
⋮----
// Get custom commands from files
⋮----
async processCommandTemplate(command: CustomCommand, args: string[], cwd?: string): Promise<string>
⋮----
private substituteArguments(template: string, args: string[], commandArgs: CommandArgument[]): string
⋮----
// First, substitute {{ARGUMENTS}} placeholder with all arguments joined by spaces
⋮----
// Then, substitute provided arguments
⋮----
// Handle any remaining placeholders that weren't substituted
⋮----
// Replace unreplaced placeholders with empty strings for optional arguments
⋮----
private findUnreplacedPlaceholders(template: string, argsLength: number): number[]
⋮----
private async executeShellCommands(prompt: string, cwd?: string): Promise<string>
⋮----
private async processShellCommandLine(line: string, cwd?: string): Promise<string | string[]>
⋮----
// Handle edge case: line with just '!' or '! ' (space after)
⋮----
maxBuffer: 10 * 1024 * 1024, // 10 MB
⋮----
private extractErrorDetails(error: unknown):
⋮----
private isExecError(error: unknown): error is ExecError
⋮----
private formatShellOutput(command: string, output: string): string[]
⋮----
dispose(): void
⋮----
async settingsChanged(oldSettings: SettingsData, newSettings: SettingsData): Promise<void>

================
File: src/main/custom-commands/index.ts
================


================
File: src/main/data-manager/data-manager.ts
================
import { DatabaseSync } from 'node:sqlite';
⋮----
import { UsageReportData, UsageDataRow } from '@common/types';
⋮----
import logger from '@/logger';
import { DB_FILE_PATH } from '@/constants';
⋮----
/**
 * Manages the application's database connection and structure.
 * This class is responsible for initializing the database, creating necessary tables,
 * and providing a method to close the connection.
 */
export class DataManager
⋮----
/**
   * Constructs a new DataManager instance, opening a database connection
   * and ensuring the necessary 'messages' table exists.
   */
constructor()
⋮----
// Initialize the database connection.
// The verbose option logs all SQL statements to the console, which is useful for debugging.
⋮----
public init()
⋮----
/**
   * Applies pending schema migrations for existing databases.
   * Each migration must be idempotent, as this runs on every startup.
   */
private runMigrations(): void
⋮----
/**
   * Saves a message to the database.
   * @param id The unique identifier for the message.
   * @param type The type of the message ('tool' or 'assistant').
   * @param project The project's base directory.
   * @param taskId The task's identifier.
   * @param model The model used for the message.
   * @param usageReport The usage report data.
   * @param content The content of the message.
   */
public saveMessage(
    id: string,
    type: 'tool' | 'assistant',
    project: string,
    taskId: string,
    model: string,
    usageReport: UsageReportData | undefined,
    content: unknown,
): void
⋮----
/**
   * Closes the database connection.
   * It's important to call this method when the application is shutting down
   * to ensure that all data is saved correctly and resources are released.
   */
public close(): void
⋮----
public queryUsageData(from: Date, to: Date): UsageDataRow[]
⋮----
public getExtensionState(extensionId: string, key: string): unknown
⋮----
public setExtensionState(extensionId: string, key: string, value: unknown): void

================
File: src/main/data-manager/index.ts
================


================
File: src/main/events/event-manager.ts
================
import { Socket } from 'socket.io';
import {
  ContextFile,
  InputHistoryData,
  ProviderProfile,
  LogData,
  SystemLogData,
  ModelsData,
  NotificationData,
  QuestionData,
  QuestionAnsweredData,
  ResponseChunkData,
  ResponseCompletedData,
  CommandOutputData,
  TerminalData,
  TerminalExitData,
  ToolData,
  ToolInputChunkData,
  TokensInfoData,
  UserMessageData,
  MessageRemovedData,
  VersionsInfo,
  AutocompletionData,
  ProviderModelsData,
  ProvidersUpdatedData,
  SettingsData,
  TaskData,
  ClearTaskData,
  ProjectSettings,
  AgentProfile,
  AgentProfilesUpdatedData,
  McpServersData,
  TaskGitStatus,
  TaskGitStatusUpdatedData,
  TaskCreatedData,
  UpdatedFile,
  UpdatedFilesUpdatedData,
  QueuedPromptData,
  QueuedPromptsUpdatedData,
  SkillDefinition,
  SkillsUpdatedData,
  CommandsData,
  ExtensionUIRefreshData,
  ExtensionsUpdatedData,
  ModalOverlayUrlData,
  InputPromptData,
  ContextInfoData,
} from '@common/types';
⋮----
import type { WindowManager } from '@/window-manager';
⋮----
import logger from '@/logger';
⋮----
export interface EventsConnectorConfig {
  eventTypes?: string[];
  baseDirs?: string[];
  readonly?: boolean;
}
⋮----
export interface EventsConnector extends EventsConnectorConfig {
  socket: Socket;
}
⋮----
type EventListener = (event: { type: string; data: unknown }) => void;
⋮----
export class EventManager
⋮----
constructor(private readonly windowManager?: WindowManager)
⋮----
// Project lifecycle events
sendProjectStarted(baseDir: string): void
⋮----
sendClearTask(baseDir: string, taskId: string, clearMessages: boolean, clearFiles: boolean): void
⋮----
sendContextInfoUpdated(data: ContextInfoData): void
⋮----
// File management events
sendFileAdded(baseDir: string, taskId: string, file: ContextFile): void
⋮----
sendContextFilesUpdated(baseDir: string, taskId: string, files: ContextFile[]): void
⋮----
sendUpdatedFilesUpdated(baseDir: string, taskId: string, taskDir: string, files: UpdatedFile[]): void
⋮----
sendSkillsUpdated(baseDir: string, taskId: string, skills: SkillDefinition[]): void
⋮----
// Response events
sendResponseChunk(data: ResponseChunkData): void
⋮----
sendResponseCompleted(data: ResponseCompletedData): void
⋮----
// Question events
sendInputPrompt(data: InputPromptData): void
⋮----
sendAskQuestion(questionData: QuestionData): void
⋮----
sendQuestionAnswered(baseDir: string, taskId: string, question: QuestionData, answer: string, userInput?: string): void
⋮----
// Autocompletion events
sendUpdateAutocompletion(baseDir: string, taskId: string, words?: string[], allFiles?: string[]): void
⋮----
// Queue events
sendQueuedPromptsUpdated(baseDir: string, taskId: string, queuedPrompts: QueuedPromptData[]): void
⋮----
// Aider models events
sendUpdateAiderModels(_baseDir: string, _taskId: string, modelsData: ModelsData): void
⋮----
// Command events
sendCommandOutput(baseDir: string, taskId: string, command: string, output: string): void
⋮----
// Log events
sendLog(data: LogData): void
⋮----
// System log events (application-wide logs)
sendSystemLog(data: SystemLogData): void
⋮----
// Tool events
sendTool(data: ToolData): void
⋮----
sendToolInputChunk(data: ToolInputChunkData): void
⋮----
// User message events
sendUserMessage(data: UserMessageData): void
⋮----
// Tokens info events
sendUpdateTokensInfo(tokensInfo: TokensInfoData): void
⋮----
// Input history events
sendInputHistoryUpdated(baseDir: string, taskId: string, inputHistory: string[]): void
⋮----
// Commands events
sendCommandsUpdated(data: CommandsData): void
⋮----
sendCustomCommandError(baseDir: string, taskId: string, error: string): void
⋮----
sendTaskGitStatusUpdated(baseDir: string, taskId: string, status: TaskGitStatus | null): void
⋮----
// Terminal events
sendTerminalData(data: TerminalData): void
⋮----
sendTerminalExit(data: TerminalExitData): void
⋮----
// Versions events
sendVersionsInfoUpdated(versionsInfo: VersionsInfo): void
⋮----
sendSettingsUpdated(settings: SettingsData): void
⋮----
// Provider events
sendProvidersUpdated(providers: ProviderProfile[]): void
⋮----
sendProviderModelsUpdated(data: ProviderModelsData): void
⋮----
sendProjectSettingsUpdated(baseDir: string, settings: ProjectSettings): void
⋮----
// Agent profile events
sendAgentProfilesUpdated(profiles: AgentProfile[]): void
⋮----
// MCP servers events
sendMcpServersUpdated(data: McpServersData): void
⋮----
// Extension events
sendExtensionsUpdated(data: ExtensionsUpdatedData =
⋮----
// Task lifecycle events
sendTaskCreated(task: TaskData, activate?: boolean): void
⋮----
sendTaskInitialized(task: TaskData): void
⋮----
sendTaskUpdated(task: TaskData): void
⋮----
sendTaskStarted(task: TaskData): void
⋮----
sendTaskCompleted(task: TaskData): void
⋮----
sendTaskCancelled(task: TaskData): void
⋮----
sendTaskDeleted(task: TaskData): void
⋮----
sendTaskMessageRemoved(baseDir: string, taskId: string, messageIds: string[]): void
⋮----
sendNotification(baseDir: string, title: string, body: string): void
⋮----
subscribe(socket: Socket, config: EventsConnectorConfig): void
⋮----
unsubscribe(socket: Socket, log = true): void
⋮----
private sendToWindows(eventType: string, data: unknown): void
⋮----
// Send event to all open windows
⋮----
// Sending to a disposed/crashed render frame can throw synchronously; skip it
// without breaking delivery to other windows or the code that triggered the event
⋮----
private broadcastToEventConnectors(eventType: string, data: unknown, log = true): void
⋮----
// Filter by event types if specified
⋮----
// Filter by base directories if specified
⋮----
// Remove disconnected sockets
⋮----
// Notify SSE listeners
⋮----
// ignore broken listeners
⋮----
// Buffer events for late-arriving SSE subscribers
⋮----
// Extension UI events
sendExtensionUIRefresh(options:
⋮----
// Modal overlay URL events
sendModalOverlayUrl(url: string): void
⋮----
// Aider connector status events (Python install + per-task connector lifecycle)
sendAiderConnectorStatus(status: import('@/python-dependencies-installer').PythonInstallStatus, baseDir?: string, taskId?: string): void
⋮----
// SSE event subscription for CLI streaming
subscribeSSE(listenerId: string, listener: EventListener, replayBuffer = false): void
⋮----
// Replay buffered events to late-arriving subscribers
⋮----
// ignore broken listeners
⋮----
unsubscribeSSE(listenerId: string, listener: EventListener): void

================
File: src/main/events/index.ts
================


================
File: src/main/extensions/__fixtures__/invalid-syntax.ts
================


================
File: src/main/extensions/__fixtures__/prohibited-import.ts
================
import { app } from 'electron';

================
File: src/main/extensions/__fixtures__/valid-extension.ts
================


================
File: src/main/extensions/disposable-store.ts
================
import logger from '@/logger';
⋮----
type CleanupFn = () => void | Promise<void>;
⋮----
export class DisposableStore
⋮----
constructor(private readonly extensionName: string)
⋮----
addExtensionDisposable(cleanup: CleanupFn): void
⋮----
addProjectDisposable(projectDir: string, cleanup: CleanupFn): void
⋮----
async disposeExtension(): Promise<void>
⋮----
async disposeProject(projectDir: string): Promise<void>
⋮----
hasProjectDisposables(projectDir: string): boolean
⋮----
private async disposeAllProjects(): Promise<void>

================
File: src/main/extensions/extension-context.ts
================
import { ProjectContextImpl } from './project-context';
import { TaskContextImpl } from './task-context';
⋮----
import type { DisposableStore } from './disposable-store';
import type { ElectronApp, ExtensionContext, MemoryContext, ProjectContext, TaskContext } from '@common/extensions';
import type { McpServerConfig, Model, ProviderProfile, SettingsData } from '@common/types';
import type { McpConfigManager } from '@/agent/mcp-config-manager';
import type { EventManager } from '@/events';
import type { MemoryManager } from '@/memory/memory-manager';
import type { ModelManager } from '@/models';
import type { Project } from '@/project';
import type { Store } from '@/store';
import type { Task } from '@/task';
⋮----
import logger from '@/logger';
import { truncateToolResult } from '@/agent/utils';
import { openUrl as openUrlUtil } from '@/utils/open-url';
⋮----
export class ExtensionContextImpl implements ExtensionContext
⋮----
constructor(
    private readonly extensionId: string,
    private readonly extensionName: string,
    disposableStore: DisposableStore,
    private readonly store?: Store,
    private readonly modelManager?: ModelManager,
    private readonly eventManager?: EventManager,
    private readonly memoryManager?: MemoryManager,
    private readonly project?: Project,
    private readonly task?: Task,
    private readonly mcpConfigManager?: McpConfigManager,
)
⋮----
addDisposable(setup: () => (() => void | Promise<void>) | void): void
⋮----
log(message: string, type: 'info' | 'error' | 'warn' | 'debug' = 'info'): void
⋮----
getProjectDir(): string
⋮----
getOpenProjectDirs(): string[]
⋮----
getTaskContext(): TaskContext | null
⋮----
getProjectContext(): ProjectContext
⋮----
async getModelConfigs(): Promise<Model[]>
⋮----
getProviders(): ProviderProfile[]
⋮----
async getMcpServers(projectDir?: string): Promise<Record<string, McpServerConfig>>
⋮----
async getSetting(key: string): Promise<unknown>
⋮----
async updateSettings(updates: Partial<SettingsData>): Promise<void>
⋮----
triggerUIDataRefresh(componentId?: string, taskId?: string): void
⋮----
triggerUIComponentsReload(): void
⋮----
async openUrl(url: string, target: 'external' | 'window' | 'modal-overlay' = 'window'): Promise<void>
⋮----
async openPath(path: string): Promise<boolean>
⋮----
async getElectronApp(): Promise<ElectronApp | null>
⋮----
getMemoryContext(): MemoryContext
⋮----
async truncateToolResult(
    content: string,
    maxLines?: number,
    maxSizeKB?: number,
    maxTokens?: number,
    saveToFile?: boolean,
    truncationSuffix?: string,
): Promise<string>

================
File: src/main/extensions/extension-fetcher.ts
================
import fs from 'fs/promises';
import path from 'path';
⋮----
import { AvailableExtension } from '@common/types';
import { ExtensionMetadata } from '@common/extensions';
⋮----
import logger from '@/logger';
import { execWithShellPath } from '@/utils/shell';
import { EXTENSIONS_REPOS_CACHE_DIR } from '@/constants';
⋮----
interface CacheEntry {
  timestamp: number;
  extensions: AvailableExtension[];
}
⋮----
const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
⋮----
export class ExtensionFetcher
⋮----
async getAvailableExtensions(repositories: string[], forceRefresh = false): Promise<AvailableExtension[]>
⋮----
private async getAvailableExtensionsInternal(repositories: string[], forceRefresh: boolean): Promise<AvailableExtension[]>
⋮----
async fetchExtensionsFromRepo(repoUrl: string, forceRefresh: boolean): Promise<AvailableExtension[]>
⋮----
private getRepoDirName(repoUrl: string): string
⋮----
// fall through
⋮----
async ensureRepoCloned(repoUrl: string): Promise<string>
⋮----
private getCloneUrl(webUrl: string): string | null
⋮----
private getExtensionSubPath(repoUrl: string): string | null
⋮----
getExtensionsPath(repoUrl: string, clonedRepoPath: string): string
⋮----
private async scanForExtensions(extensionsPath: string, repoUrl: string): Promise<AvailableExtension[]>
⋮----
// Check if the repo root itself is a folder-based extension
⋮----
// README.md doesn't exist or can't be read, that's fine
⋮----
// Try to read README.md for folder-based extensions
⋮----
// README.md doesn't exist or can't be read, that's fine
⋮----
private async mergeInstallCounts(extensions: AvailableExtension[], extensionsPath: string, repoDir: string): Promise<void>
⋮----
// Registry file missing or invalid at this location, try the next candidate
⋮----
private getRepoName(repoUrl: string): string
⋮----
// fall through
⋮----
private async fileExists(filePath: string): Promise<boolean>
⋮----
private async extractMetadataFromLocalFile(filePath: string): Promise<ExtensionMetadata | null>
⋮----
private async getStaticMetadata(filePath: string): Promise<ExtensionMetadata | null>
⋮----
const visitor = (node: ts.Node) =>
⋮----
private findVariableDeclaration(sourceFile: ts.SourceFile, variableName: string): ts.ObjectLiteralExpression | null
⋮----
private collectEnumValues(sourceFile: ts.SourceFile): Record<string, unknown>
⋮----
private parseObjectLiteral(node: ts.ObjectLiteralExpression, enumValues: Record<string, unknown>): Record<string, unknown>
⋮----
private parsePropertyValue(node: ts.Node, enumValues: Record<string, unknown>): unknown
⋮----
private convertToGitHubRawUrl(webUrl: string): string | null
⋮----
// Match URL with specific path: github.com/{owner}/{repo}/tree/{branch}/{path}
⋮----
// Match repository root URL: github.com/{owner}/{repo}
⋮----
getRawUrl(webUrl: string): string | null

================
File: src/main/extensions/extension-library-loader.ts
================
import path from 'path';
import fs from 'fs/promises';
⋮----
import { AIDER_DESK_CACHE_DIR } from '@/constants';
import logger from '@/logger';
import { fetchWithTimeout } from '@/utils';
⋮----
const sanitizeLibraryName = (name: string): string =>
⋮----
export class ExtensionLibraryLoader
⋮----
constructor()
⋮----
private getCachePath(librarySpec: string): string
⋮----
private getMetaPath(librarySpec: string): string
⋮----
async loadLibrary(librarySpec: string): Promise<string>
⋮----
// cache miss - fetch below

================
File: src/main/extensions/extension-loader.ts
================
import path from 'path';
⋮----
import { createJiti, type JitiOptions } from 'jiti';
import { Extension, ExtensionMetadata } from '@common/extensions';
⋮----
import logger from '@/logger';
⋮----
// Define the interface for the loaded module
interface ExtensionModule {
  default: {
    new (): Extension;
    metadata?: ExtensionMetadata;
  };
}
⋮----
// Base jiti options shared across all instances
⋮----
export class ExtensionLoader
⋮----
/**
   * Get or create a jiti instance for a specific extension directory.
   * This ensures module resolution happens relative to the extension's directory.
   */
private getJitiForExtension(filePath: string): ReturnType<typeof createJiti>
⋮----
// Check if we already have a jiti instance for this directory
⋮----
// Create a new jiti instance with the extension directory as base
⋮----
private deriveExtensionName(filePath: string): string
⋮----
// If the file is index.ts or index.js, use the parent folder name
⋮----
// Otherwise, use the filename without extension
⋮----
async loadExtension(filePath: string): Promise<
⋮----
// Use extension-specific jiti instance for proper module resolution
⋮----
// Use jiti to load the file
// jiti handles TypeScript compilation on the fly
⋮----
// Check if default export exists
⋮----
// Check if it's a class/constructor
⋮----
// Check for static metadata, or generate from filename
⋮----
// Instantiate the extension

================
File: src/main/extensions/extension-manager.ts
================
import os from 'os';
import fs from 'fs/promises';
import path from 'path';
⋮----
import { FSWatcher, watch } from 'chokidar';
import debounce from 'lodash/debounce';
import { z } from 'zod';
import {
  AvailableExtension,
  ExtensionConfigComponent,
  ExtensionOperationResult,
  ExtensionToolInfo,
  SettingsData,
  SkillDefinition,
  ToolApprovalState,
} from '@common/types';
import { TOOL_GROUP_NAME_SEPARATOR } from '@common/tools';
import { AIDER_DESK_EXTENSIONS_REPO_URL, ProviderDefinition, Tool, UIComponentDefinition } from '@common/extensions';
import { DEFAULT_AGENT_PROFILE } from '@common/agent';
⋮----
import { ExtensionLoader } from './extension-loader';
import { ExtensionRegistry, LoadedExtension } from './extension-registry';
import { ExtensionContextImpl } from './extension-context';
import { DisposableStore } from './disposable-store';
import { ExtensionFetcher } from './extension-fetcher';
import { ExtensionLibraryLoader } from './extension-library-loader';
⋮----
import type {
  AfterCommitEvent,
  AgentFinishedEvent,
  AgentStartedEvent,
  AgentStepFinishedEvent,
  AgentStepStartedEvent,
  AiderPromptFinishedEvent,
  AiderPromptStartedEvent,
  BeforeCommitEvent,
  CommandDefinition,
  CommandExecutedEvent,
  CustomCommandExecutedEvent,
  Extension,
  ExtensionContext,
  FilesAddedEvent,
  FilesDroppedEvent,
  HandleApprovalEvent,
  ImportantRemindersEvent,
  InterruptedEvent,
  ModeDefinition,
  OptimizeMessagesEvent,
  ProjectStartedEvent,
  ProjectStoppedEvent,
  PromptFinishedEvent,
  PromptStartedEvent,
  PromptTemplateEvent,
  QuestionAnsweredEvent,
  QuestionAskedEvent,
  ResponseChunkEvent,
  ResponseCompletedEvent,
  RuleFilesRetrievedEvent,
  SubagentFinishedEvent,
  SubagentStartedEvent,
  TaskClosedEvent,
  TaskCreatedEvent,
  TaskDeletedEvent,
  TaskInitializedEvent,
  TaskPreparedEvent,
  TaskUpdatedEvent,
  ToolApprovalEvent,
  ToolCalledEvent,
  ToolDefinition,
  ToolFinishedEvent,
  ToolsetCreatedEvent,
} from '@common/extensions';
import type { AgentProfile } from '@common/types';
import type { Store } from '@/store';
import type { McpConfigManager } from '@/agent/mcp-config-manager';
import type { ModelManager } from '@/models';
import type { EventManager } from '@/events';
import type { MemoryManager } from '@/memory/memory-manager';
import type { TelemetryManager } from '@/telemetry';
import type { ToolExecutionOptions, ToolSet } from 'ai';
⋮----
import { shouldUsePolling } from '@/utils/file-watch';
import logger from '@/logger';
import { AIDER_DESK_EXTENSIONS_DIR, AIDER_DESK_GLOBAL_EXTENSIONS_DIR } from '@/constants';
import { ApprovalManager } from '@/agent/tools/approval-manager';
import { contentArrayHasNonTextParts, stringifyWithBudget, truncateToolResult } from '@/agent/utils';
import { Project } from '@/project';
import { Task } from '@/task';
⋮----
export type ExtensionsChangeListener = (extensions: LoadedExtension[]) => void;
⋮----
export interface RegisteredTool {
  extensionId: string;
  extensionName: string;
  tool: ToolDefinition;
}
⋮----
export interface RegisteredCommand {
  extensionId: string;
  extensionName: string;
  command: CommandDefinition;
}
⋮----
export interface RegisteredAgent {
  extensionId: string;
  extensionName: string;
  agent: AgentProfile;
}
⋮----
export interface RegisteredMode {
  extensionId: string;
  extensionName: string;
  mode: ModeDefinition;
}
⋮----
export interface RegisteredUIComponent {
  extensionId: string;
  extensionName: string;
  component: UIComponentDefinition;
}
⋮----
export interface RegisteredProvider {
  extensionId: string;
  extensionName: string;
  provider: ProviderDefinition;
}
⋮----
/**
 * Mapping of extension event names to their event payload types
 */
export type ExtensionEventMap = {
  onProjectStarted: ProjectStartedEvent;
  onProjectStopped: ProjectStoppedEvent;
  onTaskCreated: TaskCreatedEvent;
  onTaskPrepared: TaskPreparedEvent;
  onTaskInitialized: TaskInitializedEvent;
  onTaskClosed: TaskClosedEvent;
  onTaskUpdated: TaskUpdatedEvent;
  onTaskDeleted: TaskDeletedEvent;
  onPromptStarted: PromptStartedEvent;
  onPromptFinished: PromptFinishedEvent;
  onPromptTemplate: PromptTemplateEvent;
  onAgentStarted: AgentStartedEvent;
  onAgentFinished: AgentFinishedEvent;
  onAgentStepStarted: AgentStepStartedEvent;
  onAgentStepFinished: AgentStepFinishedEvent;
  onOptimizeMessages: OptimizeMessagesEvent;
  onImportantReminders: ImportantRemindersEvent;
  onAiderPromptStarted: AiderPromptStartedEvent;
  onAiderPromptFinished: AiderPromptFinishedEvent;
  onToolApproval: ToolApprovalEvent;
  onToolCalled: ToolCalledEvent;
  onToolFinished: ToolFinishedEvent;
  onToolsetCreated: ToolsetCreatedEvent;
  onFilesAdded: FilesAddedEvent;
  onFilesDropped: FilesDroppedEvent;
  onRuleFilesRetrieved: RuleFilesRetrievedEvent;
  onResponseChunk: ResponseChunkEvent;
  onResponseCompleted: ResponseCompletedEvent;
  onHandleApproval: HandleApprovalEvent;
  onSubagentStarted: SubagentStartedEvent;
  onSubagentFinished: SubagentFinishedEvent;
  onQuestionAsked: QuestionAskedEvent;
  onQuestionAnswered: QuestionAnsweredEvent;
  onCommandExecuted: CommandExecutedEvent;
  onCustomCommandExecuted: CustomCommandExecutedEvent;
  onBeforeCommit: BeforeCommitEvent;
  onAfterCommit: AfterCommitEvent;
  onInterrupted: InterruptedEvent;
};
⋮----
const hasNonTextContentParts = (value: unknown): boolean =>
⋮----
const boundExtensionToolResult = async (result: unknown): Promise<unknown> =>
⋮----
export class ExtensionManager
⋮----
constructor(
    private readonly store: Store,
    private readonly modelManager: ModelManager,
    private readonly eventManager: EventManager,
    private readonly telemetryManager: TelemetryManager,
    private readonly memoryManager: MemoryManager,
    private readonly mcpConfigManager?: McpConfigManager,
    private readonly registry: ExtensionRegistry = new ExtensionRegistry(),
)
⋮----
private isExtensionDisabled(filePath: string): boolean
⋮----
/**
   * Filter extensions based on disabled list from settings
   * @param extensions - All loaded extensions
   * @returns Filtered extensions excluding disabled ones
   */
private filterEnabledExtensions(extensions: LoadedExtension[]): LoadedExtension[]
⋮----
/**
   * Handle settings changes. Detects when extensions are enabled/disabled
   * and triggers lifecycle events accordingly.
   */
async settingsChanged(oldSettings: SettingsData, newSettings: SettingsData): Promise<void>
⋮----
// Check for disabled extensions changes
⋮----
// newlyEnabled: was disabled before, now enabled (removed from disabled list)
// newlyDisabled: was enabled before, now disabled (added to disabled list)
⋮----
// Unload newly disabled extensions: disposables first, then onUnload
⋮----
// Re-initialize newly enabled extensions
⋮----
// Check if any changed extensions have UI components
⋮----
// Check for repository changes
⋮----
// Re-create watchers if fileWatchMode changed
⋮----
addListener(listener: ExtensionsChangeListener): void
⋮----
removeListener(listener: ExtensionsChangeListener): void
⋮----
async init(): Promise<void>
⋮----
// Preload available extensions from repositories in background
⋮----
// Mark as initialized even on failure so dispatchEvent doesn't block forever waiting for init
⋮----
/**
   * Preload available extensions from configured repositories.
   * This warms the fetcher cache so Settings page loads instantly.
   */
private async preloadAvailableExtensions(): Promise<void>
⋮----
private async initializeExtension(loaded: LoadedExtension, project?: Project): Promise<boolean>
⋮----
/**
   * Delivers onProjectStarted to a freshly (re)initialized extension for projects that are
   * already running — otherwise lifecycle events dispatched before this extension was
   * loaded or reloaded would be silently lost.
   */
private async dispatchDelayedProjectStarted(loaded: LoadedExtension): Promise<void>
⋮----
// A project-bound extension initialized during Project.start() is not tracked as started yet;
// its onProjectStarted arrives via the regular dispatchEvent call right after.
⋮----
/**
   * Loads, registers, and initializes an extension from a file path.
   * Combines the common pattern of load → register → initialize.
   */
private async loadAndInitializeExtension(filePath: string, project?: Project): Promise<
⋮----
/**
   * Unified method to load all extensions from a directory.
   * Discovers, loads, initializes, and collects tools/commands for all extensions in the directory.
   * Used for both global initialization and project-specific loading.
   *
   * @param dir - The directory to load extensions from
   * @param project - Optional project instance (for project-specific extensions)
   * @returns Object with loadedCount, initializedCount, and errors
   */
private async loadExtensionsForDir(dir: string, project?: Project): Promise<void>
⋮----
/**
   * Diff-based reload: only unload/load extensions that were added, removed, or changed,
   * rather than reloading all extensions in the directory.
   */
private async reloadChangedExtensions(dir: string, project?: Project): Promise<void>
⋮----
// Unload removed extensions
⋮----
// Load added extensions
⋮----
// Reload changed extensions (file exists before and after, but mtime differs)
⋮----
getExtensions(projectDir?: string): LoadedExtension[]
⋮----
isInitialized(): boolean
⋮----
async waitForInit(): Promise<void>
⋮----
/**
   * @deprecated Migration helper: converts old name-based disabled list to filePath-based.
   * Will be removed in a future version.
   */
private migrateDisabledExtensions(): void
⋮----
private captureExtensionsTelemetry()
⋮----
async dispose(): Promise<void>
⋮----
async unloadExtension(filePath: string): Promise<void>
⋮----
async reloadExtension(filePath: string, project?: Project): Promise<boolean>
⋮----
private getOrCreateDisposableStore(loaded: LoadedExtension): DisposableStore
⋮----
private findLoadedById(extensionId: string, projectDir?: string): LoadedExtension | undefined
⋮----
private createContext(extensionId: string, extensionName: string, project?: Project, task?: Task): ExtensionContextImpl
⋮----
private findExtensionByPath(filePath: string): LoadedExtension | undefined
⋮----
private async discoverExtensionsFromDir(extensionsDir: string): Promise<string[]>
⋮----
private async scanDirectory(dir: string): Promise<string[]>
⋮----
private async fileExists(filePath: string): Promise<boolean>
⋮----
private async setupWatcherForDir(dir: string, onChange: () => Promise<void>): Promise<FSWatcher | null>
⋮----
const isRelevantFile = (filePath: string): boolean =>
⋮----
private async startHotReloadWatcher(): Promise<void>
⋮----
private async reloadGlobalExtensions(): Promise<void>
⋮----
private async unloadExtensionsForDir(dir: string): Promise<void>
⋮----
private async stopGlobalWatcher(): Promise<void>
⋮----
async reloadProjectExtensions(project: Project): Promise<void>
⋮----
async stopProjectWatcher(projectDir: string): Promise<void>
⋮----
private async restartWatchers(): Promise<void>
⋮----
validateToolDefinition(tool: ToolDefinition):
⋮----
getToolsInfo(projectDir?: string): ExtensionToolInfo[]
⋮----
getTools(task: Task, mode: string, profile: AgentProfile): RegisteredTool[]
⋮----
/**
   * Creates a Vercel AI SDK compatible ToolSet from registered extension tools.
   * Each tool is wrapped with validation, error handling, and ExtensionContext creation.
   *
   * @param task - The current Task instance
   * @param mode - The current mode
   * @param profile - The agent profile with tool approval settings
   * @param allTools - The complete set of available tools
   * @param abortSignal - Optional AbortSignal for cancellation support
   * @returns A ToolSet containing all approved extension tools
   */
createExtensionToolset(
    task: Task,
    mode: string,
    profile: AgentProfile,
    allTools: ToolSet,
    approvalManager: ApprovalManager,
    abortSignal?: AbortSignal,
): ToolSet
⋮----
// Tool approval filtering is handled in getTools()
⋮----
// --- Tool Approval Logic ---
⋮----
// --- End Tool Approval Logic ---
⋮----
// Error isolation - log and return error message
⋮----
validateCommandDefinition(command: CommandDefinition):
⋮----
getCommands(project: Project): RegisteredCommand[]
⋮----
getAgents(project?: Project): RegisteredAgent[]
⋮----
getAgentById(agentId: string, project?: Project): RegisteredAgent | undefined
⋮----
getModes(project?: Project): RegisteredMode[]
⋮----
validateModeDefinition(mode: ModeDefinition):
⋮----
getProviders(project?: Project): RegisteredProvider[]
⋮----
getSkills(project: Project, task: Task): SkillDefinition[]
⋮----
getUIComponents(project?: Project, task?: Task): RegisteredUIComponent[]
⋮----
// Skip uninitialized extensions
⋮----
getUIComponentsLibraries(project?: Project): Map<string, Record<string, string>>
⋮----
async loadExtensionLibrary(librarySpec: string): Promise<string>
⋮----
/**
   * Check if an extension provides a settings config component.
   * Uses the dedicated getConfigComponent() method on the Extension interface.
   */
extensionHasConfig(loadedExt: LoadedExtension): boolean
⋮----
async getUIExtensionData(extensionId: string, componentId: string, project?: Project, task?: Task): Promise<unknown>
⋮----
async executeUIExtensionAction(extensionId: string, componentId: string, action: string, args: unknown[], project?: Project, task?: Task): Promise<unknown>
⋮----
/**
   * Get the config component JSX for a specific extension.
   * Uses the dedicated getConfigComponent() method on the Extension interface.
   */
getExtensionConfigComponent(extensionId: string, project?: Project): ExtensionConfigComponent | null
⋮----
/**
   * Get the current configuration data for an extension's settings.
   * Delegates to the extension's getConfigData() method.
   */
async getExtensionConfig(extensionId: string, project?: Project): Promise<unknown>
⋮----
/**
   * Save configuration data for an extension's settings.
   * Delegates to the extension's saveConfigData() method.
   */
async saveExtensionConfig(extensionId: string, configData: unknown, project?: Project): Promise<unknown>
⋮----
validateUIComponentDefinition(component: UIComponentDefinition):
⋮----
async updateAgentProfile(profile: AgentProfile): Promise<AgentProfile | null>
⋮----
async executeCommand(commandName: string, args: string[], project: Project, task?: Task, images?: string[]): Promise<void>
⋮----
/**
   * Fetch available extensions from a repository URL
   * @param repositories - Array of repository URLs
   * @param forceRefresh - Force refresh cache and refetch
   * @param fetchOnly - Bypass all caching (promise and repo cache) and fetch directly via git clone
   * @returns Array of available extensions with metadata
   */
async getAvailableExtensions(repositories: string[], forceRefresh = false, fetchOnly = false): Promise<AvailableExtension[]>
⋮----
/**
   * Install an extension from a repository
   * @param extensionId - Extension identifier
   * @param repositoryUrl - Repository URL where the extension is located
   * @param project - Optional project for project-level install
   * @returns ExtensionOperationResult indicating success or failure with error details
   */
async installExtension(extensionId: string, repositoryUrl: string, project?: Project): Promise<ExtensionOperationResult>
⋮----
// For repo-root extensions (entire repo is the extension), use extensionsPath directly
// For subdirectory extensions, join with the folder name
⋮----
// Use temp dir for npm install to avoid triggering the watcher with
// hundreds of node_modules file writes
⋮----
// Remove .git directory if present (from cloned repos)
⋮----
// Copy to final location — node_modules is ignored by the watcher
⋮----
/**
   * Install npm dependencies for a folder extension
   */
private async installDependencies(extensionPath: string): Promise<void>
⋮----
/**
   * Uninstall an extension
   * @param extensionId - Extension identifier (filePath)
   * @param projectDir - Optional project directory for project-level uninstall
   * @returns true if uninstallation succeeded
   */
async uninstallExtension(extensionId: string, projectDir?: string): Promise<boolean>
⋮----
// Unload (dispose + onUnload) before removing files
⋮----
/**
   * Update an extension from a repository, overwriting existing files
   * @param extensionId - Extension identifier (filePath of installed extension)
   * @param repositoryUrl - Repository URL where the extension is located
   * @param project - Optional project for project-level extension
   * @returns ExtensionOperationResult indicating success or failure with error details
   */
async updateExtension(extensionId: string, repositoryUrl: string, project?: Project): Promise<ExtensionOperationResult>
⋮----
// Unload only the extension being updated (not all extensions in the directory)
⋮----
// Clean up old extension if the type changed (file → folder or folder → file)
⋮----
// Previous version was a single file, new version is a folder — remove old file
⋮----
// Previous version was a folder, new version is a single file — remove old folder
⋮----
// For repo-root extensions (entire repo is the extension), use extensionsPath directly
⋮----
// Use temp dir for npm install to avoid triggering the watcher
⋮----
// Remove .git directory if present (from cloned repos)
⋮----
// Copy on top of existing directory — merges files, overwriting source
// files while preserving user-created files like config.json
⋮----
// Determine the entry file path
⋮----
// Load only the updated extension
⋮----
/**
   * Dispatches an event to all loaded and initialized extensions.
   * Events can be modified or blocked by extensions.
   *
   * @param eventName - The name of the event to dispatch
   * @param event - The event payload
   * @param project - The current Project instance
   * @param task - Optional Task instance (not available for all events)
   * @returns The potentially modified event (blocked status is part of the event)
   */
async dispatchEvent<K extends keyof ExtensionEventMap>(
    eventName: K,
    event: ExtensionEventMap[K],
    project: Project,
    task?: Task,
): Promise<ExtensionEventMap[K]>
⋮----
// Wait for initialization so events are never delivered to a partially loaded extension set
⋮----
// Get all extensions (global + project-specific)
⋮----
// Early exit if no extensions
⋮----
// Sort extensions: global first, then project-specific
⋮----
// Skip uninitialized extensions
⋮----
// Get the handler method dynamically
⋮----
// Skip if extension doesn't handle this event
⋮----
// For onProjectStopped: dispose project-level disposables BEFORE calling the handler
// (disposers first, then manual cleanup — consistent with onUnload pattern)
⋮----
// Create ExtensionContext for this extension
⋮----
// Call the extension handler
// Using type assertion to handle dynamic dispatch across different event types
⋮----
// Process result if extension returned modifications
⋮----
// Merge partial event modifications
⋮----
// Check for blocking
⋮----
// Continue to next extension - errors don't stop the chain
⋮----
/**
   * Sorts extensions for event dispatch: global extensions first, then project-specific
   * @param extensions - All loaded extensions
   * @param projectDir - Current project directory
   * @returns Sorted array of extensions
   */
private sortExtensionsForDispatch(extensions: LoadedExtension[], projectDir: string): LoadedExtension[]

================
File: src/main/extensions/extension-registry.ts
================
import path from 'path';
import { promises as fs } from 'fs';
⋮----
import { Extension, ExtensionMetadata } from '@common/extensions';
⋮----
import type { Project } from '@/project';
import type { DisposableStore } from './disposable-store';
⋮----
import logger from '@/logger';
⋮----
export interface LoadedExtension {
  id: string;
  instance: Extension;
  metadata: ExtensionMetadata;
  filePath: string;
  initialized: boolean;
  projectDir?: string;
  readmeContent?: string;
  project?: Project;
  disposableStore?: DisposableStore;
}
⋮----
export class ExtensionRegistry
⋮----
private deriveExtensionId(filePath: string): string
⋮----
// If the file is index.ts or index.js, use the parent folder name
⋮----
// Otherwise, use the filename without extension
⋮----
async register(extension: Extension, metadata: ExtensionMetadata, filePath: string, projectDir?: string)
⋮----
// Try to load README.md for folder-based extensions
⋮----
// README.md doesn't exist or can't be read, that's fine
⋮----
setInitialized(filePath: string, initialized: boolean): void
⋮----
getExtensions(projectDir?: string): LoadedExtension[]
⋮----
getExtension(filePath: string): LoadedExtension | undefined
⋮----
unregister(filePath: string): boolean
⋮----
clear()

================
File: src/main/extensions/index.ts
================


================
File: src/main/extensions/project-context.ts
================
import { TaskContextImpl } from './task-context';
⋮----
import type { DisposableStore } from './disposable-store';
import type { ProjectContext, TaskContext } from '@common/extensions';
import type { AgentProfile, CommandsData, CreateTaskParams, ProjectSettings, TaskData } from '@common/types';
import type { Project } from '@/project';
⋮----
export class ProjectContextImpl implements ProjectContext
⋮----
constructor(
⋮----
addDisposable(setup: () => (() => void | Promise<void>) | void): void
⋮----
get baseDir(): string
⋮----
async createTask(params: CreateTaskParams): Promise<TaskData>
⋮----
getTask(taskId: string): TaskContext | null
⋮----
async getTasks(): Promise<TaskData[]>
⋮----
async reloadTasks(): Promise<TaskData[]>
⋮----
getMostRecentTask(): TaskContext | null
⋮----
async forkTask(taskId: string, messageId: string): Promise<TaskData>
⋮----
async duplicateTask(taskId: string): Promise<TaskData>
⋮----
async deleteTask(taskId: string): Promise<void>
⋮----
getAgentProfiles(): AgentProfile[]
⋮----
getCommands(): CommandsData
⋮----
getProjectSettings(): ProjectSettings
⋮----
async getInputHistory(): Promise<string[]>

================
File: src/main/extensions/task-context.ts
================
import {
  AgentProfile,
  ContextFile,
  ContextMessage,
  Mode,
  PromptContext,
  QueuedPromptData,
  ResponseCompletedData,
  SwitchToLocalOptions,
  SwitchToWorktreeOptions,
  TaskData,
  TodoItem,
  UpdatedFile,
  UsageReportData,
  UncommittedFiles,
} from '@common/types';
⋮----
import type { QuestionOptions, SmartCompactionOptions, SmartCompactionResult, TaskContext, ResponseMessage } from '@common/extensions';
import type { z } from 'zod';
import type { Task } from '@/task';
⋮----
export class TaskContextImpl implements TaskContext
⋮----
constructor(private readonly task: Task)
⋮----
get data(): TaskData
⋮----
// Context Files (Read + Safe Write)
⋮----
async getContextFiles(): Promise<ContextFile[]>
⋮----
async addFile(path: string, readOnly = false): Promise<void>
⋮----
async addFiles(...files: ContextFile[]): Promise<void>
⋮----
async dropFile(path: string): Promise<void>
⋮----
// Messages (Read + Safe Write)
⋮----
async getContextMessages(): Promise<ContextMessage[]>
⋮----
async addContextMessage(message: ContextMessage, updateContextInfo = false): Promise<void>
⋮----
async removeMessage(messageId: string): Promise<void>
⋮----
async removeLastMessage(): Promise<void>
⋮----
async loadContextMessages(messages: ContextMessage[]): Promise<void>
⋮----
async redoUserPrompt(messageId: string, mode?: string, updatedPrompt?: string): Promise<void>
⋮----
async redoLastUserPrompt(mode?: string, updatedPrompt?: string): Promise<void>
⋮----
async removeMessagesUpTo(messageId: string): Promise<void>
⋮----
addUserMessage(id: string, content: string, promptContext?: PromptContext)
⋮----
addToolMessage(
    id: string,
    serverName: string,
    toolName: string,
    input?: unknown,
    response?: string,
    usageReport?: UsageReportData,
    promptContext?: PromptContext,
    saveToDb = true,
    finished = !!response,
)
⋮----
async addResponseMessage(message: ResponseMessage, saveToDb = true)
⋮----
// Files & Repo (Read-only)
⋮----
getTaskDir(): string
⋮----
async getAddableFiles(searchRegex?: string): Promise<string[]>
⋮----
async getAllFiles(useGit = true): Promise<string[]>
⋮----
async getUpdatedFiles(): Promise<UpdatedFile[]>
⋮----
async getUpdatedFileDiff(filePath: string, commitHash?: string): Promise<string>
⋮----
async getUpdatedFileContents(filePath: string, commitHash?: string): Promise<
⋮----
getRepoMap(): string
⋮----
async addToGit(path: string): Promise<void>
⋮----
// Todos (Read + Safe Write)
⋮----
async getTodos(): Promise<TodoItem[]>
⋮----
async addTodo(name: string): Promise<TodoItem[]>
⋮----
async updateTodo(name: string, updates: Partial<TodoItem>): Promise<TodoItem[]>
⋮----
async deleteTodo(name: string): Promise<TodoItem[]>
⋮----
async clearAllTodos(): Promise<TodoItem[]>
⋮----
async setTodos(items: TodoItem[], initialUserPrompt = ''): Promise<void>
⋮----
// Execution
⋮----
async runPrompt(prompt: string, mode?: string, images?: string[]): Promise<void>
⋮----
async runPromptInAgent(
    profile: AgentProfile,
    mode: string,
    prompt: string | null,
    promptContext?: PromptContext,
    contextMessages?: ContextMessage[],
    contextFiles?: ContextFile[],
    systemPrompt?: string,
    waitForCurrentAgentToFinish = true,
    sendNotification = true,
    skillsToActivate?: string[],
): Promise<ResponseCompletedData[]>
⋮----
async runCustomCommand(name: string, args: string[] = [], mode?: string): Promise<void>
⋮----
async runSubagent(agentProfile: AgentProfile, prompt: string): Promise<void>
⋮----
async runCommand(command: string): Promise<void>
⋮----
async interruptResponse(): Promise<void>
⋮----
async generateText(modelId: string, systemPrompt: string, prompt: string): Promise<string | undefined>
⋮----
async generateObject<T>(modelId: string, systemPrompt: string, prompt: string, schema: z.ZodType<T>): Promise<T | undefined>
⋮----
// User Interaction
⋮----
async askQuestion(text: string, options?: QuestionOptions): Promise<string>
⋮----
async answerQuestion(answer: string, userInput?: string): Promise<boolean>
⋮----
addLogMessage(level: 'info' | 'error' | 'warning', message?: string): void
⋮----
addLoadingMessage(message?: string, finished?: boolean): void
⋮----
// Task Management
⋮----
async getTaskAgentProfile(): Promise<AgentProfile | null>
⋮----
async updateTask(updates: Partial<TaskData>): Promise<TaskData>
⋮----
async handoffConversation(focus?: string, execute = false): Promise<void>
⋮----
// Context Management
⋮----
async clearContext(): Promise<void>
⋮----
async resetContext(): Promise<void>
⋮----
async compactConversation(instructions?: string): Promise<void>
⋮----
async smartCompact(options?: SmartCompactionOptions): Promise<SmartCompactionResult>
⋮----
async getEstimatedTokens(): Promise<number>
⋮----
async generateContextMarkdown(): Promise<string | null>
⋮----
isInitialized(): boolean
⋮----
async updateAutocompletionWords(words?: string[]): Promise<void>
⋮----
// Queued Prompts
⋮----
getQueuedPrompts(): QueuedPromptData[]
⋮----
async sendQueuedPromptNow(promptId: string): Promise<void>
⋮----
removeQueuedPrompt(promptId: string): void
⋮----
reorderQueuedPrompts(prompts: QueuedPromptData[]): void
⋮----
editQueuedPrompt(promptId: string, newText: string): void
⋮----
// Working Mode & Worktrees
⋮----
async switchToWorktreeWorkingMode(options?: SwitchToWorktreeOptions): Promise<void>
⋮----
async switchToLocalWorkingMode(options?: SwitchToLocalOptions): Promise<void>
⋮----
async getLocalUncommittedFiles(): Promise<UncommittedFiles>
⋮----
async applyUncommittedChanges(): Promise<void>
⋮----
async mergeWorktreeToWorktree(targetWorktreeDir: string, includeUncommitted?: boolean): Promise<void>
⋮----
async resumeTask(): Promise<void>

================
File: src/main/git/git-askpass-manager.ts
================
import { createServer, Server, IncomingMessage, ServerResponse } from 'http';
import { AddressInfo } from 'net';
⋮----
import { randomUUID } from 'crypto';
⋮----
import { InputPromptData } from '@common/types';
⋮----
import { EventManager } from '@/events';
import { AIDER_DESK_DATA_DIR } from '@/constants';
import logger from '@/logger';
⋮----
interface PendingRequest {
  resolve: (answer: string) => void;
  reject: (err: Error) => void;
  timer: NodeJS.Timeout;
  prompt: string;
}
⋮----
export class GitAskpassManager
⋮----
constructor(
    private readonly eventManager?: EventManager,
    customAskpassDir?: string,
)
⋮----
async init(): Promise<void>
⋮----
// Export askpass environment variables to process.env so all git commands inherit them
⋮----
private writeAskpassFiles(): void
⋮----
private async startServer(): Promise<void>
⋮----
private handleHttpRequest(req: IncomingMessage, res: ServerResponse): void
⋮----
// Check if there is a cached answer for this prompt
⋮----
private promptUser(prompt: string): Promise<string>
⋮----
// Check if there is already another request active for this exact prompt text
⋮----
private buildPromptData(id: string, prompt: string): InputPromptData
⋮----
respond(id: string, value: string | null, rememberSession?: boolean): boolean
⋮----
getEnv(): NodeJS.ProcessEnv
⋮----
clearSessionCache(): void
⋮----
async close(): Promise<void>

================
File: src/main/git/git-manager.ts
================
import os from 'os';
import path, { join } from 'path';
import fs, { mkdir, rm, lstat, symlink } from 'fs/promises';
import { existsSync, lstatSync } from 'fs';
⋮----
import {
  BranchInfo,
  ConflictResolutionFileContext,
  GitSyncCommits,
  MergeState,
  RebaseState,
  UpdatedFile,
  UpdatedFilesGroupMode,
  WorkingMode,
  Worktree,
  GitAheadCommits,
  UncommittedFiles,
} from '@common/types';
// @ts-expect-error istextorbinary library does not provide TypeScript definitions
import { isBinary } from 'istextorbinary';
import { simpleGit } from 'simple-git';
⋮----
import type { GitAskpassManager } from '@/git/git-askpass-manager';
⋮----
import { execWithShellPath, withLock } from '@/utils';
import { AIDER_DESK_TASKS_DIR } from '@/constants';
import logger from '@/logger';
import { isFileNotFoundError } from '@/utils/errors';
⋮----
export class GitError extends Error
⋮----
getErrorDetails()
⋮----
// Interface for raw commit data
interface RawCommitData {
  hash: string;
  message: string;
  date: string | Date;
  author?: string;
  additions?: number;
  deletions?: number;
  filesChanged?: number;
}
⋮----
const isAbortError = (error: unknown): boolean =>
⋮----
export class GitManager
⋮----
constructor(private readonly gitAskpassManager?: GitAskpassManager)
⋮----
private getWorktreePath(projectPath: string, taskId: string): string
⋮----
private async isBranchCheckedOutInOtherWorktree(projectPath: string, branch: string, worktreePath: string): Promise<boolean>
⋮----
private async initializeWorktree(projectPath: string, taskId: string): Promise<void>
⋮----
async createWorktree(projectPath: string, taskId: string, branch?: string, baseBranch = 'HEAD'): Promise<Worktree>
⋮----
// 1. Initialize repository if necessary
⋮----
// Initialize git repository
⋮----
// 2. Clean up stale worktree registrations (e.g. the directory was deleted out-of-band)
⋮----
// 3. Clean up any existing worktree before recreating its directory
⋮----
// 4. Ensure the repository has at least one commit
⋮----
// Repository has no commits yet, create initial commit
⋮----
// Ignore add errors (no files to add)
⋮----
// 5. Logic for creating the worktree
⋮----
const baseRef = baseBranch; // Will be 'HEAD' if not provided
⋮----
// A. Branch name provided: Check if branch exists or create it
⋮----
// Check if it's an actual branch (not just a commit SHA)
⋮----
// Branch doesn't exist, it will be created from baseRef
⋮----
// A1. Use existing branch
⋮----
// git refuses to check out a branch that is already used by another worktree;
// fall back to a unique sibling branch created from the original branch's tip
⋮----
// Get the base commit (the tip of the existing branch)
⋮----
// A2. Create new branch from baseRef (which defaults to 'HEAD')
⋮----
// Verify that the base branch exists if specified and is a branch name
⋮----
// Capture the base commit before creating the worktree
⋮----
// Create the new branch and checkout the worktree
⋮----
newBranchName = branch; // Use the newly created branch name
⋮----
// B. No branch name provided: Create detached worktree from current HEAD (or specified baseRef)
⋮----
// Note: git worktree add <path> defaults to current HEAD
⋮----
// Capture the base commit (the commit it's detached at)
⋮----
// Use the baseRef for the branch name, or resolve the current branch if baseRef is 'HEAD'
⋮----
// Resolve the base branch: if baseRef is 'HEAD', resolve to the current branch name
⋮----
async renameBranch(projectPath: string, oldBranch: string, newBranch: string): Promise<string>
⋮----
private async findUniqueBranchName(projectPath: string, baseName: string): Promise<string>
⋮----
async createSymlinks(projectPath: string, worktreePath: string, folderNames: string[]): Promise<void>
⋮----
private async existsAndIsDirectory(path: string): Promise<boolean>
⋮----
private async isFolderTrackedByGit(projectPath: string, folderName: string): Promise<boolean>
⋮----
async removeWorktree(projectDir: string, worktree: Worktree, force = false): Promise<void>
⋮----
// Clean up the branch if it's a task branch (starts with "task-" or looks like generated from task name)
⋮----
// Branch might not exist or be protected, don't fail the removal
⋮----
// If the worktree is not found, that's okay - it might have been manually deleted
⋮----
// For other errors, still throw
⋮----
async listWorktrees(projectDir: string): Promise<Worktree[]>
⋮----
async listBranches(projectPath: string, includeRemote = false): Promise<BranchInfo[]>
⋮----
// Get all local branches
⋮----
// Get all worktrees to identify which branches have worktrees
⋮----
// Get upstream tracking info (upstream name and ahead/behind counts) in one call
⋮----
// Remove leading *, +, and spaces. The + indicates uncommitted changes
⋮----
// Unborn HEAD (repository with no commits yet): `git branch` outputs nothing,
// so synthesize the current branch to keep it visible in the UI
⋮----
// Skip remote-name refs (e.g. "origin"), detached HEAD pointers (e.g. "origin/HEAD") and branches that exist locally
⋮----
// Sort branches: current first, then other worktree branches, then locals, then remotes; alphabetical within groups
⋮----
// Within each group, sort alphabetically
⋮----
async createBranch(projectPath: string, name: string, startPoint?: string, checkout = true): Promise<void>
⋮----
async checkoutBranch(projectPath: string, branch: string, createTracking = false, takeOver = false): Promise<void>
⋮----
private async detachBranchFromOtherWorktrees(projectPath: string, branch: string): Promise<void>
⋮----
// The first worktree is always the main working tree. Detaching it would leave the main repository
// in a detached HEAD state, breaking project-level git operations, so it must never be detached.
⋮----
const isMainWorktree = (w: Worktree)
⋮----
async deleteBranch(projectPath: string, branch: string, force = false): Promise<void>
⋮----
async mergeIntoCurrent(projectPath: string, branch: string): Promise<
⋮----
async rebaseOnto(projectPath: string, branch: string): Promise<
⋮----
private quoteArg(arg: string): string
⋮----
private wrapGitError(error: unknown, commands: string[], workingDirectory?: string): GitError
⋮----
private async getConflictedFiles(path: string): Promise<string[]>
⋮----
async getProjectMainBranch(projectPath: string): Promise<string>
⋮----
// ONLY check the current branch in the project root directory
⋮----
// Throw error if we're in detached HEAD state
⋮----
// Deprecated: Use getProjectMainBranch instead
async detectMainBranch(projectPath: string): Promise<string>
⋮----
// Deprecated: Use getProjectMainBranch instead
async getEffectiveMainBranch(project:
⋮----
async isGitRepository(projectPath: string): Promise<boolean>
⋮----
async initRepository(projectPath: string): Promise<void>
⋮----
// Fallback for older git versions without `-b` support
⋮----
private async ensureGitignoreEntry(projectPath: string): Promise<void>
⋮----
private async ensureGitRepository(projectPath: string, action: string): Promise<boolean>
⋮----
async getCommitHash(worktreePath: string, ref: string): Promise<string | undefined>
⋮----
async getHeadCommit(worktreePath: string): Promise<string | undefined>
⋮----
/**
   * Check if a commit is an ancestor of another commit (or HEAD).
   * Returns true if `ancestorCommit` is reachable from `descendantRef`.
   */
async isCommitAncestorOf(worktreePath: string, ancestorCommit: string, descendantRef = 'HEAD'): Promise<boolean>
⋮----
// git merge-base --is-ancestor returns exit code 0 if ancestor, 1 if not
⋮----
/**
   * Get the "onto" commit from an active rebase.
   * Returns undefined if no rebase is in progress or the file doesn't exist.
   */
async getRebaseOntoCommit(worktreePath: string): Promise<string | undefined>
⋮----
// Try rebase-merge first (interactive rebase)
⋮----
// rebase-merge/onto doesn't exist, try rebase-apply
⋮----
// Try rebase-apply (non-interactive rebase)
⋮----
// rebase-apply/onto doesn't exist either
⋮----
/**
   * Resolve a commit to the branch name that points to it.
   * Returns the first branch found, or undefined if no branch points to that exact commit.
   */
async getBranchPointingAtCommit(projectPath: string, commit: string): Promise<string | undefined>
⋮----
async getBranchesContainingCommit(projectPath: string, commit: string): Promise<string[]>
⋮----
async hasChangesToRebase(worktreePath: string, mainBranch: string): Promise<boolean>
⋮----
// Check if main branch has commits that the current branch doesn't have
// Use cross-platform approach
⋮----
// Error checking, assume no changes
⋮----
async checkForRebaseConflicts(
    worktreePath: string,
    mainBranch: string,
): Promise<
⋮----
// First check if there are any changes to rebase
⋮----
// Get the merge base
⋮----
// Try a dry-run merge to detect conflicts
// We use merge-tree to check for conflicts without modifying the working tree
⋮----
// Parse merge-tree output for conflicts
⋮----
// Get list of files that would conflict
⋮----
// Find files modified in both branches
⋮----
// Get commit info for better error reporting
⋮----
// If merge-tree is not available (older git), fall back to checking modified files
⋮----
// Get files changed in both branches
⋮----
// Find files modified in both branches (potential conflicts)
⋮----
// Get commit info
⋮----
// On error, return unknown status
⋮----
async rebaseMainIntoWorktree(
    worktreePath: string,
    mainBranch: string,
    baseCommit?: string,
    symlinkFolders: string[] = [],
): Promise<
⋮----
// 0. Guard against starting a rebase while one is already in progress
⋮----
// 1. Check for uncommitted changes and create temporary commit if needed
⋮----
// Stage all changes
⋮----
// Unstage symlink folders to prevent them from being tracked by git
// (they are worktree infrastructure artifacts, not real project files)
⋮----
// Create temporary commit with unique timestamp
⋮----
// 2. Validate baseCommit if provided - ensure it's an ancestor of HEAD
// If baseCommit is stale (e.g., user rebased externally), fall back to simple rebase
⋮----
// 3. Rebase the current worktree branch onto target branch
// When baseCommit is provided and valid, use --onto to only replay commits made after baseCommit
// This avoids carrying over base branch commits that aren't in the target branch
⋮----
// 4. If rebase succeeds AND we had uncommitted changes, reset to uncommitted state
⋮----
// Create detailed error with git command output
⋮----
async abortRebase(worktreePath: string): Promise<void>
⋮----
// Check if we're in the middle of a rebase
⋮----
// Abort the rebase
⋮----
// Always try to reset temporary commit if it exists
⋮----
async continueRebase(worktreePath: string): Promise<
⋮----
// Capture the "onto" info before continuing (it will be gone after rebase completes)
⋮----
// Always try to reset temporary commit if it exists after successful continue
⋮----
private async squashAndMergeWorktreeToMain(
    projectPath: string,
    worktreePath: string,
    mainBranch: string,
    commitMessage: string,
    baseCommit?: string,
    checkoutless = false,
): Promise<void>
⋮----
// SAFETY CHECK 0: A rebase left in progress (e.g. awaiting conflict resolution in the UI)
// would block our own rebase and must not be aborted by this operation
⋮----
// Get current branch name in worktree (for logging purposes)
⋮----
// Check if there are any changes to merge (before rebase)
⋮----
// Validate baseCommit if provided - ensure it's an ancestor of HEAD
// If baseCommit is stale (e.g., user rebased externally), fall back to simple rebase
⋮----
// SAFETY CHECK 1: Rebase worktree onto main FIRST before squashing
// Use --onto with baseCommit to only replay worktree-specific commits
⋮----
// Only abort a rebase that this operation started - never a pre-existing rebase
// the user may still be resolving (e.g. started concurrently while we passed the guard)
⋮----
// Ignore abort errors
⋮----
// Get the HEAD commit hash from worktree AFTER rebase (but before squashing)
// This preserves the worktree's commit history
⋮----
// Create the squashed commit via plumbing and update the target branch ref directly
// without checking it out (the project directory is on a different branch, and git
// guarantees the target branch cannot be checked out in any other worktree)
⋮----
// No-op check: nothing to squash if the trees are identical
⋮----
// Switch to main branch in the main repository
⋮----
// SQUASH MERGE: Use git merge --squash to create a squashed staging of all changes
// This keeps the worktree branch history intact while creating a single commit in main
⋮----
// Check if there are staged changes to commit
⋮----
// Commit the squashed changes with the provided message
⋮----
// Preserve specific errors (e.g. rebase-in-progress guard, rebase conflict details)
⋮----
// Create detailed error with git command output
⋮----
// Prioritize actual error messages over lastOutput (which may contain unrelated data like commit counts)
⋮----
private async mergeWorktreeToMain(projectPath: string, worktreePath: string, mainBranch: string, checkoutless = false): Promise<void>
⋮----
// SAFETY CHECK 0: A rebase left in progress (e.g. awaiting conflict resolution in the UI)
// would block our own rebase and must not be aborted by this operation
⋮----
// Get current branch name in worktree (for logging purposes)
⋮----
// Check if there are any changes to merge
⋮----
// SAFETY CHECK 1: Rebase worktree onto main FIRST (resolves conflicts in worktree, not main)
⋮----
// Only abort a rebase that this operation started - never a pre-existing rebase
// the user may still be resolving (e.g. started concurrently while we passed the guard)
⋮----
// Ignore abort errors
⋮----
// Get the HEAD commit hash from worktree AFTER rebase
⋮----
// Update the target branch ref directly without checking it out:
// the project directory is on a different branch, and git guarantees the target
// branch cannot be checked out in any other worktree
⋮----
// Switch to main branch in the main repository
⋮----
// SAFETY CHECK 2: Use --ff-only merge to prevent history rewriting
// This will fail if local main has diverged from the worktree branch
⋮----
// Preserve specific errors (e.g. rebase-in-progress guard, rebase conflict details)
⋮----
// Create detailed error with git command output
⋮----
// Prioritize actual error messages over lastOutput (which may contain unrelated data like commit counts)
⋮----
generateRebaseCommands(mainBranch: string): string[]
⋮----
generateSquashCommands(mainBranch: string, branchName: string): string[]
⋮----
generateMergeCommands(mainBranch: string, branchName: string): string[]
⋮----
async gitPull(worktreePath: string, rebase?: boolean): Promise<
⋮----
async gitPush(worktreePath: string, force?: boolean, setUpstream?: boolean): Promise<
⋮----
private async getDefaultRemote(repoPath: string): Promise<string>
⋮----
async updateBranch(repoPath: string, branchName: string): Promise<
⋮----
// ignore
⋮----
async getLastCommits(worktreePath: string, count: number = 20, includeStats: boolean = true): Promise<RawCommitData[]>
⋮----
async getChangesDiff(projectPath: string, worktreePath: string, targetBranch?: string): Promise<string>
⋮----
// Check if there are any commits in worktree that are not in main
⋮----
// No commits in worktree, return empty string
⋮----
// Get the first commit hash in the worktree branch (oldest commit not in main)
⋮----
// Get full diff from the commit before the oldest worktree commit to HEAD
⋮----
async getOriginBranch(worktreePath: string, branch: string): Promise<string | null>
⋮----
async getTaskWorktree(projectPath: string, taskId: string): Promise<Worktree | null>
⋮----
// Find worktree that matches our task's worktree path
⋮----
// A prunable worktree has a missing or invalid directory (stale registration), treat it as absent
⋮----
// If no worktree exists for this task, return null
⋮----
/**
   * Check if there are uncommitted changes in the given path
   */
async hasUncommittedChanges(path: string): Promise<boolean>
⋮----
/**
   * Stash uncommitted changes with a unique identifier
   * Returns the stash identifier or null if no changes to stash
   */
async stashUncommittedChanges(stashId: string, path: string, message: string, symlinkFolders: string[] = []): Promise<string | null>
⋮----
// Find which symlink folders have untracked files
⋮----
// Get untracked files to check which symlink folders have content
⋮----
// Check if folder has untracked files
⋮----
// Build stash command with exclude patterns for folders with untracked files
⋮----
/**
   * Apply a stash by its identifier
   */
async applyStash(path: string, stashId: string): Promise<void>
⋮----
// Find the stash entry that matches our identifier
⋮----
// Extract stash reference (e.g., "stash@{0}")
⋮----
// A previous partial apply may have restored this stash's untracked files to disk,
// which blocks re-applying the stash. Remove identical copies and retry once.
⋮----
/**
   * Remove untracked files from the stash's untracked-files commit that already exist on disk
   * with identical content (left behind by a partial stash apply), so the stash can be applied again.
   * Tracked files and files with different content are never removed.
   */
private async removeIdenticalStashUntrackedFiles(dir: string, stashRef: string): Promise<void>
⋮----
// Never remove files that are tracked in the index
⋮----
// not in the index
⋮----
/**
   * Drop a stash by its identifier
   */
async dropStash(path: string, stashId: string): Promise<void>
⋮----
// Find the stash entry that matches our identifier
⋮----
// Extract stash reference (e.g., "stash@{0}")
⋮----
// Don't throw - dropping stash is not critical
⋮----
/**
   * Merge worktree to main branch with uncommitted changes support
   * Returns MergeState for potential revert
   */
async mergeWorktreeToMainWithUncommitted(
    projectPath: string,
    taskId: string,
    worktreePath: string,
    squash: boolean,
    commitMessage?: string,
    targetBranch?: string,
    symlinkFolders: string[] = [],
    baseCommit?: string,
): Promise<MergeState>
⋮----
// If the project directory is on a different branch, update the target branch ref directly
// without checking it out (the working tree is left untouched)
⋮----
// 1. Track initial state
⋮----
// 2. Stash uncommitted changes in worktree
⋮----
// 3. Stash uncommitted changes in main branch if any (not needed for checkoutless merge,
// since the project directory working tree is never touched)
⋮----
// 4. Perform the merge operation (existing methods handle the actual merge)
⋮----
// 5. Apply worktree stash to both branches (keeping changes uncommitted)
// For checkoutless merge the project directory is on a different branch, so the
// worktree's uncommitted changes are only restored into the worktree
⋮----
// Drop the worktree stash after successful application
⋮----
// 6. Restore main branch's original uncommitted changes if any
// DO NOT drop this stash - we need it for potential revert
⋮----
// Return merge state for potential revert
⋮----
// Recovery: revert main repo to pre-merge state before restoring stashes
// Only when the merge step actually ran - otherwise the main working tree may still
// contain un-stashed changes that a hard reset would destroy
⋮----
// Checkoutless merge never touched the project working tree - only move the ref back
⋮----
// Abort any in-progress merge (in case merge itself failed)
⋮----
// Ignore - no merge in progress
⋮----
// Hard reset main to undo merge commit and clean conflict markers from failed stash apply
⋮----
// Restore worktree stash
⋮----
// Restore main's original uncommitted changes on the clean working tree
⋮----
/**
   * Check if worktree has uncommitted changes or unmerged commits
   * Returns information about unsaved work in the worktree
   */
async checkWorktreeForUnmergedWork(
    projectPath: string,
    worktreePath: string,
    targetBranch?: string,
    symlinkFolders: string[] = [],
): Promise<
⋮----
// 1. Check for uncommitted changes
⋮----
// Filter out files that belong to symlink folders (worktree infrastructure artifacts)
⋮----
// 2. Get the target branch name
⋮----
// 3. Check for commits in worktree that are not in target branch
⋮----
// If we can't get commits, log but don't fail
⋮----
// On error, return safe defaults (assume there might be work)
⋮----
async getAheadCommits(worktreePath: string, targetBranch: string): Promise<GitAheadCommits>
⋮----
/**
   * Get outgoing and incoming commits relative to a target branch, or relative to the
   * upstream of the current branch when no target branch is provided.
   * Fetches the remote first (throttled per repository) so un-fetched remote commits are counted too.
   */
async getSyncCommits(repoPath: string, targetBranch?: string): Promise<GitSyncCommits>
⋮----
const parseCommits = (stdout: string)
⋮----
private async fetchRemote(repoPath: string): Promise<void>
⋮----
async getUncommittedFiles(worktreePath: string): Promise<UncommittedFiles>
⋮----
private parseGitStatusEntries(stdout: string): string[]
⋮----
private isSymlinkPath(cwd: string, filePath: string): boolean
⋮----
private isDirectoryPath(cwd: string, filePath: string): boolean
⋮----
/**
   * Get updated files with per-commit and uncommitted diffs.
   *
   * Worktree mode grouped: returns files grouped by commit (with per-commit diffs)
   * plus uncommitted files (with uncommitted diffs). A file can appear in multiple groups.
   * Worktree mode flat: returns all changed files with cumulative diff from base,
   * plus uncommitted changes vs HEAD. Each file appears once.
   * Non-worktree mode: returns uncommitted files only (diff vs HEAD).
   */
async getUpdatedFiles(
    worktreePath: string,
    workingMode?: WorkingMode,
    mainBranch?: string,
    groupMode: UpdatedFilesGroupMode = UpdatedFilesGroupMode.Grouped,
): Promise<UpdatedFile[]>
⋮----
private async includeUntrackedFiles(worktreePath: string, files: UpdatedFile[]): Promise<UpdatedFile[]>
⋮----
private async createUntrackedUpdatedFile(worktreePath: string, filePath: string): Promise<UpdatedFile>
⋮----
/**
   * Build the synthetic unified diff for an untracked (new) file.
   */
private buildUntrackedFileDiff(fileContentBuffer: Buffer, filePath: string): string
⋮----
private formatDiffPath(filePath: string): string
⋮----
/**
   * Worktree mode: separate committed (per-commit) and uncommitted file diffs.
   */
private async getWorktreeUpdatedFiles(worktreePath: string, mainBranch: string): Promise<UpdatedFile[]>
⋮----
// 1. Get commits in order (oldest first) between mainBranch and HEAD
⋮----
// 2. For each commit, get its files (diffs are lazy loaded via getFileDiff)
⋮----
// Get numstat for this specific commit
⋮----
// Build a map of filePath -> {additions, deletions} from numstat
⋮----
// Emit one UpdatedFile per file in this commit (diff not included - lazy loaded via getFileDiff)
⋮----
// Skip symlink paths (worktree infrastructure artifacts)
⋮----
// 3. Get uncommitted changes (diff against HEAD, not merge-base)
⋮----
// Check if HEAD exists (no commits yet in worktree)
⋮----
// Skip symlink paths (worktree infrastructure artifacts)
⋮----
/**
   * Worktree flat mode: all changed files with diff from base to working tree,
   * covering both committed and uncommitted changes in a single pass.
   */
private async getWorktreeFlatUpdatedFiles(worktreePath: string, mainBranch: string): Promise<UpdatedFile[]>
⋮----
// Diff from the merge-base instead of the mainBranch tip, so commits on main that are not
// yet in this branch don't show up as reverse diffs.
⋮----
// No HEAD or unrelated histories: fall back to mainBranch
⋮----
// git diff (without ...) compares the base directly to the working tree,
// including both committed and uncommitted changes.
⋮----
// Skip symlink paths (worktree infrastructure artifacts)
⋮----
/**
   * Non-worktree mode: simple uncommitted changes vs HEAD.
   */
private async getNonWorktreeUpdatedFiles(worktreePath: string): Promise<UpdatedFile[]>
⋮----
// Skip symlink paths (worktree infrastructure artifacts)
⋮----
/**
   * No-HEAD mode: repo has no commits yet. Uses the well-known empty tree hash
   * as the diff base, which is equivalent to `git diff HEAD` against an empty repo.
   * Shows staged files and unstaged modifications to staged files (same as `git diff HEAD`
   * would show if HEAD pointed to an empty commit). Untracked files are not shown,
   * consistent with the existing HEAD-based behavior.
   */
private async getNoHeadUpdatedFiles(worktreePath: string): Promise<UpdatedFile[]>
⋮----
/**
   * Lazy load the diff for a single updated file.
   *
   * - commitHash provided: per-commit diff for the file.
   * - No commitHash: untracked file diff (synthesized) or file diff against HEAD,
   *   merge-base (worktree flat mode) or the empty tree hash (no commits yet).
   */
async getFileDiff(worktreePath: string, workingMode?: WorkingMode, mainBranch?: string, filePath?: string, commitHash?: string): Promise<string>
⋮----
// Untracked file: synthesize the diff from the file content
⋮----
// resolve diff base
⋮----
/**
   * Get full old and new contents of a file for a diff group.
   * commitHash group: old = parent commit version, new = commit version.
   * Uncommitted group: old = base (merge-base/HEAD) version, new = working tree.
   */
async getFileContents(
    worktreePath: string,
    workingMode?: WorkingMode,
    mainBranch?: string,
    filePath?: string,
    commitHash?: string,
): Promise<
⋮----
// Resolve the base the same way as getFileDiff
⋮----
private async safeGitShow(worktreePath: string, ref: string, escapedPath: string): Promise<string>
⋮----
/**
   * Check if the file at the given path (relative to worktreePath) is binary.
   */
private async checkBinaryOrSkip(worktreePath: string, filePath: string): Promise<boolean>
⋮----
// If we can't check, assume regular (text) file
⋮----
private async markConflictingFiles(worktreePath: string, files: UpdatedFile[]): Promise<UpdatedFile[]>
⋮----
async addFileToGit(worktreePath: string, filePath: string): Promise<void>
⋮----
// Files explicitly added by the user should always be staged: -f handles ignored files
// (git check-ignore is unreliable here: negation rules under an excluded parent directory,
// e.g. .aider* + !.aider-desk/rules, report "not ignored" but git add still refuses),
// -A stages deletions (git add fails with "pathspec did not match any files" for files removed on disk)
⋮----
// The file exists neither on disk nor in the index (e.g. an already-staged deletion):
// there is nothing to stage, so treat it as a no-op instead of failing
⋮----
async stageFile(worktreePath: string, filePath: string): Promise<boolean>
⋮----
async undoLastCommit(worktreePath: string): Promise<
⋮----
// Use the git root directory to handle monorepo scenarios
⋮----
// Check if file exists at HEAD~1
⋮----
// If it exists, checkout the previous version
⋮----
// Reset --soft HEAD~1
⋮----
async restoreFile(worktreePath: string, filePath: string): Promise<void>
⋮----
// Check if file exists in HEAD (committed)
⋮----
// File exists in HEAD - restore both staged area and working tree from HEAD
⋮----
// New file not in HEAD - remove from index and delete working tree copy
⋮----
// File might not be in the index
⋮----
async getUncommittedDiff(worktreePath: string, filePaths?: string[]): Promise<string | null>
⋮----
// Unborn HEAD (repository with no commits yet): fall back to diffing against the empty tree
⋮----
// no commits yet
⋮----
// The empty tree diff only shows staged changes - also include untracked files
⋮----
cancelCommitChanges(worktreePath: string): boolean
⋮----
async commitChanges(worktreePath: string, message: string, amend: boolean, skipGitHooks: boolean, filePaths?: string[]): Promise<boolean>
⋮----
// Get the list of updated files (unstaged changes that are shown in the UI)
⋮----
// Stage updated files before committing. With a file selection the commit runs in the
// --only pathspec form (see below), which takes tracked files directly from the working
// tree — so staging is only needed for files not yet tracked (untracked, incl. ignored ones).
// Without a selection (full commit / amend) everything must be staged, because a plain
// `git commit` only takes what is in the index.
⋮----
// A selection that covers all updated files is effectively a full commit — keep the
// plain index commit (shorter command, everything is committed anyway)
⋮----
// Retry with -A -f: untracked files must always be staged for the commit.
// -A stages deletions (git add fails with "pathspec did not match any files" for
// files removed on disk), -f handles ignored files (git check-ignore misses
// directory-traversal exclusions like .aider* ignoring .aider-desk/rules
// despite a negation pattern)
⋮----
// The file exists neither on disk nor in the index (e.g. an already-staged
// deletion): there is nothing to stage, the commit picks up the staged
// deletion from the index, so skip instead of aborting the commit
⋮----
// Escape the commit message for shell
⋮----
// If amending and message is empty, use --no-edit to keep previous message
⋮----
// When a file selection is provided, commit only those paths (--only pathspec form).
// Plain `git commit` would also include any other files already staged in the index,
// which breaks partial commits when edited files are left staged.
⋮----
async getRebaseState(worktreePath: string): Promise<RebaseState>
⋮----
// ignore
⋮----
// ignore
⋮----
async getUnmergedFiles(worktreePath: string): Promise<string[]>
⋮----
async getStageFileContent(worktreePath: string, stage: 1 | 2 | 3, filePath: string): Promise<string | null>
⋮----
async collectConflictContext(worktreePath: string, filePath: string): Promise<ConflictResolutionFileContext>
⋮----
/**
   * Reset a temporary commit back to uncommitted changes state
   */
private async resetTempCommit(worktreePath: string): Promise<void>
⋮----
/**
   * Reset temporary commit if it exists by finding it by name pattern
   */
private async resetTempCommitIfExists(worktreePath: string): Promise<void>
⋮----
// Check if HEAD is a temporary commit
⋮----
// Don't throw - this is cleanup logic
⋮----
async stageResolvedFile(worktreePath: string, filePath: string, content: string): Promise<void>
⋮----
async listConflictedFiles(worktreePath: string): Promise<string[]>
⋮----
/**
   * Apply uncommitted changes from worktree to the branch currently checked out in the
   * project directory without merging commits or switching branches
   * This transfers work-in-progress changes while keeping them uncommitted in both branches
   */
async applyUncommittedChangesToMain(projectPath: string, taskId: string, worktreePath: string, symlinkFolders: string[] = []): Promise<void>
⋮----
// 1. Check if there are uncommitted changes in worktree
⋮----
// 2. Stash uncommitted changes from worktree
⋮----
// 3. Stash uncommitted changes in the project directory to keep them safe while applying
⋮----
// 4. Apply stash to the currently checked out branch (keeping uncommitted)
⋮----
// 5. Apply stash back to worktree (keeping uncommitted)
⋮----
// 6. Clean up stash
⋮----
// 7. Restore project directory's original uncommitted changes if any
⋮----
// Recovery: clean up project directory from failed stash apply
⋮----
// Restore project directory's original uncommitted changes
⋮----
// Restore stash to worktree
⋮----
/**
   * Revert a merge operation using the stored MergeState
   * Restores the main branch to its pre-merge state while preserving uncommitted changes
   */
async revertMerge(projectPath: string, taskId: string, worktreePath: string, mergeState: MergeState, symlinkFolders: string[] = []): Promise<void>
⋮----
// For a checkoutless merge the ref is moved back directly, unless the user has since
// checked out the target branch in the project directory
⋮----
// 1. Stash current uncommitted changes in worktree
⋮----
// 2. Move the target branch ref back to the pre-merge state without touching the project working tree
⋮----
// 2. Stash current uncommitted changes in main repo to clean the working directory
// This is crucial to avoid conflicts with untracked files when applying the original stash
⋮----
// 3. Switch to the branch we merged into, then reset it to previous state
⋮----
// 4. Reset worktree branch to previous state
⋮----
// 5. Restore main's original uncommitted changes if they were preserved
⋮----
// Clean up the stash after successful revert
⋮----
// 6. Restore uncommitted changes in worktree
⋮----
// 7. Clean up temporary main stash
// We don't apply it back because we want to revert to the state BEFORE the merge (with original changes)
// Applying it back would re-introduce the changes that were just merged
⋮----
// Recovery: try to restore stashes
⋮----
private async pruneDeleted(projectDir: string): Promise<void>
⋮----
// in case of failure, try to remove the directory manually
⋮----
async mergeWorktreeToWorktree(sourceWorktreeDir: string, targetWorktreeDir: string, includeUncommitted = false): Promise<void>
⋮----
// Ignore abort errors
⋮----
async close(projectDir: string)

================
File: src/main/git/index.ts
================


================
File: src/main/logger/event-transport.ts
================
import Transport from 'winston-transport';
import { SystemLogEntry, SystemLogLevel, SystemLogData, SystemLogsResponse } from '@common/types';
⋮----
import { LogBuffer } from './log-buffer';
⋮----
import { EventManager } from '@/events';
⋮----
/**
 * Custom Winston transport that:
 * 1. Stores logs in sql db
 * 2. Emits logs to renderer via EventManager
 */
export class EventTransport extends Transport
⋮----
constructor(opts?: Transport.TransportStreamOptions)
⋮----
setEventManager(eventManager: EventManager): void
⋮----
setSplashLogCallback(callback: (message: string) => void): void
⋮----
clearSplashLogCallback(): void
⋮----
// eslint-disable-next-line @typescript-eslint/no-explicit-any
log(info: any, callback: () => void): void
⋮----
// Extract log data from winston info object
⋮----
// Remove standard winston fields to avoid duplication
⋮----
// Add to buffer and get the assigned ID
⋮----
// Emit to renderer (only if eventManager is available)
⋮----
// Forward info-level messages to splash screen during startup
⋮----
/**
   * Get logs with pagination.
   * @param fromId - If provided, fetch logs with id < fromId (for loading older logs)
   * @param limit - Maximum number of logs to return
   * @param levels - If provided, only fetch logs with these levels
   * @returns Object with logs array (oldest to newest) and hasMore boolean
   */
getPaged(fromId?: number, limit?: number, levels?: SystemLogLevel[]): SystemLogsResponse
⋮----
/**
   * Clear all logs from the database
   */
clear(): void

================
File: src/main/logger/log-buffer.ts
================
import { DatabaseSync } from 'node:sqlite';
⋮----
import { SystemLogEntry, SystemLogsResponse, SystemLogLevel } from '@common/types';
⋮----
/**
 * JSON.stringify replacement that handles circular references.
 * Returns a string representation with "[Circular]" markers instead of throwing.
 */
const safeStringify = (obj: unknown): string =>
⋮----
/**
 * SQLite-backed storage for system logs.
 * This is session-only storage that clears on app startup.
 */
export class LogBuffer
⋮----
/**
   * Initialize the database connection and clear existing logs.
   * Should be called once at app startup.
   */
async init(): Promise<void>
⋮----
// Drop existing table to clear session logs
⋮----
// Create fresh table for this session
⋮----
/**
   * Add a log entry to the database
   * @returns The ID of the inserted log entry
   */
add(entry: SystemLogEntry): number | undefined
⋮----
/**
   * Get logs with pagination.
   * @param fromId - If provided, fetch logs with id < fromId (for loading older logs)
   * @param limit - Maximum number of logs to return
   * @param levels - If provided, only fetch logs with these levels
   * @returns Object with logs array (oldest to newest) and hasMore boolean
   */
getPaged(fromId?: number, limit: number = DEFAULT_PAGE_SIZE, levels?: SystemLogLevel[]): SystemLogsResponse
⋮----
/**
   * Clear all logs from the database
   */
clear(): void
⋮----
/**
   * Close the database connection
   */
close(): void
⋮----
/**
   * Extract extension name from log message
   * Extensions log with format: "[Extension:name] message"
   */
static extractExtensionName(message: string): string | undefined

================
File: src/main/memory/memory-manager.ts
================
import { MemoryConfig, MemoryEmbeddingProgress, MemoryEmbeddingProgressPhase, MemoryEntry, MemoryEntryType, SettingsData } from '@common/types';
import { v4 as uuidv4 } from 'uuid';
⋮----
import type { FeatureExtractionPipeline } from '@huggingface/transformers';
⋮----
import { AIDER_DESK_CACHE_DIR, AIDER_DESK_MEMORY_FILE } from '@/constants';
import logger from '@/logger';
import { Store } from '@/store';
⋮----
type LanceDbModule = typeof import('@lancedb/lancedb');
type TransformersModule = typeof import('@huggingface/transformers');
⋮----
export class MemoryManager
⋮----
constructor(private readonly store: Store)
⋮----
/**
   * Checks if an error indicates a corrupted or incomplete model file.
   */
private isCorruptedModelError(error: unknown): boolean
⋮----
/**
   * Deletes the cached model directory for a given model name.
   */
private async deleteModelCache(modelName: string): Promise<void>
⋮----
/**
   * Loads the embedding pipeline with retry logic for corrupted models.
   * If the model fails to load due to corruption, it will delete the cache and retry once.
   */
private async loadEmbeddingPipelineWithRetry(modelName: string): Promise<FeatureExtractionPipeline | null>
⋮----
const attemptLoad = async (): Promise<FeatureExtractionPipeline | null> =>
⋮----
// @ts-expect-error progress is not typed properly
⋮----
// Check if this is a corrupted model error
⋮----
// Delete the corrupted cache
⋮----
// Retry loading
⋮----
// Non-corruption error, just log and fail
⋮----
/**
   * Initialize the database connection and the local embedding model.
   * This must be called before using other methods.
   */
public async init(config: MemoryConfig = this.store.getSettings().memory): Promise<void>
⋮----
// 1. Initialize the Database
// Ensure directory exists
⋮----
// Check if table exists, if not, we create it lazily on the first add
⋮----
// 2. Initialize Local Embedding Model (Singleton pattern for the pipeline)
⋮----
getProgress(): MemoryEmbeddingProgress
⋮----
private async waitForInit(): Promise<boolean>
⋮----
async settingsChanged(oldSettings: SettingsData, newSettings: SettingsData): Promise<void>
⋮----
// init with old settings before migration
⋮----
const migrate = async () =>
⋮----
/**
   * Generates a vector embedding for the given text using the local model.
   */
private async getEmbedding(text: string): Promise<number[]>
⋮----
// Generate embedding
// pooling: 'mean' averages the token vectors to get a single sentence vector
// normalize: true ensures the vector is unit length for cosine similarity
⋮----
// Convert Float32Array to standard number array for LanceDB
⋮----
/**
   * Creates and stores a new memory for a specific scope (project directory or agent profile).
   */
async storeMemory(scopeId: string, taskId: string, type: MemoryEntryType, content: string): Promise<string>
⋮----
// Create the table if it doesn't exist yet
⋮----
// Add to existing table
⋮----
/**
   * Retrieves memories for a specific scope (project directory or agent profile) relevant to the user's query.
   * @param scopeId - The scope to filter by (project directory or `agent-profile:{id}`)
   * @param query - The user's search query
   * @param limit - Max number of memories to return (default 5)
   */
async retrieveMemories(scopeId: string, query: string, limit: number = 5): Promise<MemoryEntry[]>
⋮----
return []; // No memories exist yet
⋮----
// Perform Vector Search + SQL Filtering with distance information
⋮----
.nearestTo(queryVector) // Vector similarity search
.where(`projectid = '${scopeId}'`) // SQL-like filtering for the memory scope
⋮----
// Map results back to MemoryEntry interface
// Note: LanceDB returns rows, we cast them to our interface
⋮----
async getMemory(id: string): Promise<MemoryEntry | null>
⋮----
async deleteMemory(id: string): Promise<boolean>
⋮----
async updateMemory(id: string, content: string): Promise<boolean>
⋮----
// Check if memory exists
⋮----
async getAllMemories(): Promise<MemoryEntry[]>
⋮----
// Map results back to MemoryEntry interface
// Note: LanceDB returns rows, we cast them to our interface
⋮----
async deleteMemoriesForScope(scopeId: string): Promise<number>
⋮----
async clearAllMemories(): Promise<boolean>
⋮----
isMemoryEnabled(): boolean
⋮----
setMemoryEnabled(enabled: boolean): void

================
File: src/main/models/providers/alibaba-plan.ts
================
import { Model, ProviderProfile, Reasoning, SettingsData } from '@common/types';
import { isAlibabaPlanProvider, AlibabaPlanProvider, LlmProvider } from '@common/agent';
import { createAlibaba } from '@ai-sdk/alibaba';
⋮----
import type { SharedV4ProviderOptions } from '@ai-sdk/provider';
import type { LanguageModel } from 'ai';
⋮----
import { AiderModelMapping, LlmProviderStrategy, LoadModelsResponse } from '@/models';
import logger from '@/logger';
import { getEffectiveEnvironmentVariable } from '@/utils';
import { getDefaultUsageReport } from '@/models/providers/default';
⋮----
const loadAlibabaPlanModels = async (profile: ProviderProfile, settings: SettingsData): Promise<LoadModelsResponse> =>
⋮----
const hasAlibabaPlanEnvVars = (settings: SettingsData): boolean =>
⋮----
const getAlibabaPlanAiderMapping = (provider: ProviderProfile, modelId: string, settings: SettingsData, projectDir: string): AiderModelMapping =>
⋮----
const createAlibabaPlanLlm = (profile: ProviderProfile, model: Model, settings: SettingsData, projectDir: string): LanguageModel =>
⋮----
const getAlibabaPlanProviderOptions = (llmProvider: LlmProvider, model: Model, reasoning?: Reasoning): SharedV4ProviderOptions | undefined =>

================
File: src/main/models/providers/anthropic-compatible.ts
================
import { Model, ProviderProfile, Reasoning, SettingsData, TlsPolicyRegistrar } from '@common/types';
import { isAnthropicCompatibleProvider, AnthropicCompatibleProvider, LlmProvider } from '@common/agent';
import { createAnthropic } from '@ai-sdk/anthropic';
⋮----
import type { LanguageModel, ToolSet } from 'ai';
import type { SharedV4ProviderOptions } from '@ai-sdk/provider';
⋮----
import { AiderModelMapping, LlmProviderStrategy, LoadModelsResponse } from '@/models';
import logger from '@/logger';
import { getEffectiveEnvironmentVariable } from '@/utils';
import { getAnthropicCacheControl } from '@/models/providers/anthropic';
import { getDefaultUsageReport } from '@/models/providers/default';
import { syncProviderTlsRule } from '@/models/utils';
⋮----
const ensureV1Suffix = (baseUrl: string): string =>
⋮----
const loadAnthropicCompatibleModels = async (
  profile: ProviderProfile,
  settings: SettingsData,
  tlsRegistrar?: TlsPolicyRegistrar,
): Promise<LoadModelsResponse> =>
⋮----
const hasAnthropicCompatibleEnvVars = (settings: SettingsData): boolean =>
⋮----
const getAnthropicCompatibleAiderMapping = (provider: ProviderProfile, modelId: string, settings: SettingsData, projectDir: string): AiderModelMapping =>
⋮----
// Use anthropic prefix for Anthropic-compatible providers
⋮----
// === LLM Creation Functions ===
const createAnthropicCompatibleLlm = (
  profile: ProviderProfile,
  model: Model,
  settings: SettingsData,
  projectDir: string,
  _toolSet?: ToolSet,
  _systemPrompt?: string,
  _providerMetadata?: unknown,
  tlsRegistrar?: TlsPolicyRegistrar,
): LanguageModel =>
⋮----
// Use createAnthropic with custom baseURL to get a provider instance, then get the model.
// The @ai-sdk/anthropic SDK only appends `/messages` to the baseURL, so it must include `/v1`.
⋮----
export const getAnthropicCompatibleProviderOptions = (llmProvider: LlmProvider, _model: Model, reasoning?: Reasoning): SharedV4ProviderOptions | undefined =>
⋮----
// Explicitly request adaptive thinking with summarized display so reasoning/thinking
// text is returned via thinking_delta events. Without this, newer Claude models (opus-4-7+)
// default to 'omitted' display and return empty thinking blocks.
⋮----
// === Complete Strategy Implementation ===
⋮----
// Core LLM functions
⋮----
// Model discovery functions
⋮----
// Cache control

================
File: src/main/models/providers/anthropic.ts
================
import { createAnthropic } from '@ai-sdk/anthropic';
import { AnthropicProvider, isAnthropicProvider, LlmProvider } from '@common/agent';
import { Model, ProviderProfile, Reasoning, SettingsData } from '@common/types';
⋮----
import type { LanguageModel } from 'ai';
import type { SharedV4ProviderOptions } from '@ai-sdk/provider';
⋮----
import logger from '@/logger';
import { AiderModelMapping, CacheControl, LlmProviderStrategy } from '@/models';
import { LoadModelsResponse } from '@/models/types';
import { getEffectiveEnvironmentVariable } from '@/utils';
import { getDefaultModelInfo, getDefaultUsageReport } from '@/models/providers/default';
⋮----
export const loadAnthropicModels = async (profile: ProviderProfile, settings: SettingsData): Promise<LoadModelsResponse> =>
⋮----
export const hasAnthropicEnvVars = (settings: SettingsData): boolean =>
⋮----
export const getAnthropicAiderMapping = (provider: ProviderProfile, modelId: string): AiderModelMapping =>
⋮----
// === LLM Creation Functions ===
export const createAnthropicLlm = (profile: ProviderProfile, model: Model, settings: SettingsData, projectDir: string): LanguageModel =>
⋮----
// === Configuration Helper Functions ===
export const getAnthropicCacheControl = (): CacheControl | undefined =>
⋮----
export const getAnthropicProviderOptions = (llmProvider: LlmProvider, _model: Model, reasoning?: Reasoning): SharedV4ProviderOptions | undefined =>
⋮----
// Explicitly request adaptive thinking with summarized display so reasoning/thinking
// text is returned via thinking_delta events. Without this, newer Claude models (opus-4-7+)
// default to 'omitted' display and return empty thinking blocks.
⋮----
// === Complete Strategy Implementation ===
⋮----
// Core LLM functions
⋮----
// Model discovery functions
⋮----
// Configuration helpers

================
File: src/main/models/providers/azure.ts
================
import { createAzure } from '@ai-sdk/azure';
import { Model, ProviderProfile, ReasoningEffort, SettingsData, Reasoning } from '@common/types';
import { AzureProvider, isAzureProvider, LlmProvider } from '@common/agent';
import { type OpenAIResponsesProviderOptions } from '@ai-sdk/openai';
⋮----
import type { LanguageModel } from 'ai';
import type { SharedV4ProviderOptions } from '@ai-sdk/provider';
⋮----
import logger from '@/logger';
import { AiderModelMapping, LlmProviderStrategy } from '@/models';
import { getEffectiveEnvironmentVariable } from '@/utils';
import { getDefaultModelInfo, getDefaultUsageReport } from '@/models/providers/default';
⋮----
const extractResourceNameFromEndpoint = (endpoint: string): string =>
⋮----
// Extract resource name from hostname like "resource-name.openai.azure.com"
⋮----
export const hasAzureEnvVars = (settings: SettingsData): boolean =>
⋮----
export const getAzureAiderMapping = (provider: ProviderProfile, modelId: string): AiderModelMapping =>
⋮----
// === LLM Creation Functions ===
export const createAzureLlm = (profile: ProviderProfile, model: Model, settings: SettingsData, projectDir: string): LanguageModel =>
⋮----
export const getAzureProviderOptions = (llmProvider: LlmProvider, model: Model, reasoning?: Reasoning): SharedV4ProviderOptions | undefined =>
⋮----
// When the top-level reasoning parameter is set (not undefined or 'provider-default'),
// omit reasoningEffort from providerOptions so the AI SDK's portable reasoning takes effect.
// Keep reasoningSummary so reasoning output is still returned.
⋮----
// Extract reasoningEffort from model overrides or provider config
⋮----
// Map ReasoningEffort enum to AI SDK format
⋮----
export const getAzureProviderParameters = (llmProvider: LlmProvider, model: Model, reasoning?: Reasoning): Record<string, unknown> =>
⋮----
// not supported by Azure with reasoning models
⋮----
// === Complete Strategy Implementation ===
⋮----
// Core LLM functions
⋮----
// Model discovery functions

================
File: src/main/models/providers/bedrock.ts
================
import { BedrockClient, GetFoundationModelAvailabilityCommand, type InferenceProfileSummary, ListInferenceProfilesCommand } from '@aws-sdk/client-bedrock';
import { fromNodeProviderChain } from '@aws-sdk/credential-providers';
import { Model, ModelInfo, ProviderProfile, SettingsData } from '@common/types';
import { BedrockProvider, isBedrockProvider } from '@common/agent';
import { createAmazonBedrock } from '@ai-sdk/amazon-bedrock';
⋮----
import type { LanguageModel } from 'ai';
⋮----
import { AiderModelMapping, LlmProviderStrategy } from '@/models';
import logger from '@/logger';
import { getEffectiveEnvironmentVariable } from '@/utils';
import { LoadModelsResponse } from '@/models/types';
import { getDefaultUsageReport } from '@/models/providers/default';
⋮----
export const loadBedrockModels = async (profile: ProviderProfile, settings: SettingsData): Promise<LoadModelsResponse> =>
⋮----
// Check if we have explicit keys or if AWS_PROFILE is set in the main process env
⋮----
nextToken = response.nextToken; // Will be undefined if no more pages
⋮----
// Collect active profiles
⋮----
// Now, prepare availability checks in parallel
⋮----
// Extract modelId from the first model's modelArn
⋮----
// Check if the model is authorized and available
⋮----
export const hasBedrockEnvVars = (settings: SettingsData): boolean =>
⋮----
export const getBedrockAiderMapping = (provider: ProviderProfile, modelId: string): AiderModelMapping =>
⋮----
// === LLM Creation Functions ===
export const createBedrockLlm = (profile: ProviderProfile, model: Model, settings: SettingsData, projectDir: string): LanguageModel =>
⋮----
const getBedrockModelInfo = (_provider: ProviderProfile, modelId: string, allModelInfos: Record<string, ModelInfo>): ModelInfo | undefined =>
⋮----
// check by model part of the model name (e.g. excluding any prefix before the first dot)
// Remove prefix before first dot if present (e.g., "us.anthropic.claude-3-sonnet-20240229-v1:0" -> "anthropic.claude-3-sonnet-20240229-v1:0")
⋮----
// Check if key ends with the modelId without prefix
⋮----
// === Complete Strategy Implementation ===
⋮----
// Core LLM functions
⋮----
// Model discovery functions

================
File: src/main/models/providers/cerebras.ts
================
import { Model, ProviderProfile, SettingsData } from '@common/types';
import { CerebrasProvider, isCerebrasProvider } from '@common/agent';
import { createCerebras } from '@ai-sdk/cerebras';
⋮----
import type { LanguageModel } from 'ai';
⋮----
import { AiderModelMapping, LlmProviderStrategy } from '@/models';
import logger from '@/logger';
import { getEffectiveEnvironmentVariable } from '@/utils/environment';
import { LoadModelsResponse } from '@/models/types';
import { getDefaultModelInfo, getDefaultUsageReport } from '@/models/providers/default';
⋮----
interface CerebrasModel {
  id: string;
  max_context_length?: number;
  description?: string;
}
⋮----
export const loadCerebrasModels = async (profile: ProviderProfile, settings: SettingsData): Promise<LoadModelsResponse> =>
⋮----
export const hasCerebrasEnvVars = (settings: SettingsData): boolean =>
⋮----
export const getCerebrasAiderMapping = (provider: ProviderProfile, modelId: string): AiderModelMapping =>
⋮----
// === LLM Creation Functions ===
export const createCerebrasLlm = (profile: ProviderProfile, model: Model, settings: SettingsData, projectDir: string): LanguageModel =>
⋮----
// === Complete Strategy Implementation ===
⋮----
// Core LLM functions
⋮----
// Model discovery functions

================
File: src/main/models/providers/clinepass.ts
================
import { Model, ProviderProfile, SettingsData } from '@common/types';
import { ClinePassProvider, isClinePassProvider } from '@common/agent';
import { createOpenAICompatible } from '@ai-sdk/openai-compatible';
⋮----
import type { LanguageModel } from 'ai';
⋮----
import { AiderModelMapping, LlmProviderStrategy, LoadModelsResponse } from '@/models';
import logger from '@/logger';
import { getEffectiveEnvironmentVariable } from '@/utils';
import { getDefaultModelInfo, getDefaultUsageReport } from '@/models/providers/default';
⋮----
interface ClinePassModelMetadata {
  id: string;
  maxInputTokens: number;
  inputCostPerToken: number;
  outputCostPerToken: number;
  cacheReadInputTokenCost: number;
  cacheWriteInputTokenCost?: number;
}
⋮----
inputCostPerToken: 0.0000014, // $1.40 per 1M
outputCostPerToken: 0.0000044, // $4.40 per 1M
cacheReadInputTokenCost: 0.00000026, // $0.26 per 1M
⋮----
inputCostPerToken: 0.00000095, // $0.95 per 1M
outputCostPerToken: 0.000004, // $4.00 per 1M
cacheReadInputTokenCost: 0.00000019, // $0.19 per 1M
⋮----
inputCostPerToken: 0.00000095, // $0.95 per 1M
outputCostPerToken: 0.000004, // $4.00 per 1M
cacheReadInputTokenCost: 0.00000016, // $0.16 per 1M
⋮----
inputCostPerToken: 0.00000174, // $1.74 per 1M
outputCostPerToken: 0.00000348, // $3.48 per 1M
cacheReadInputTokenCost: 0.0000000145, // $0.0145 per 1M
⋮----
inputCostPerToken: 0.00000014, // $0.14 per 1M
outputCostPerToken: 0.00000028, // $0.28 per 1M
cacheReadInputTokenCost: 0.0000000028, // $0.0028 per 1M
⋮----
inputCostPerToken: 0.00000014, // $0.14 per 1M
outputCostPerToken: 0.00000028, // $0.28 per 1M
cacheReadInputTokenCost: 0.0000000028, // $0.0028 per 1M
⋮----
inputCostPerToken: 0.00000174, // $1.74 per 1M
outputCostPerToken: 0.00000348, // $3.48 per 1M
cacheReadInputTokenCost: 0.0000000145, // $0.0145 per 1M
⋮----
inputCostPerToken: 0.0000003, // $0.30 per 1M
outputCostPerToken: 0.0000012, // $1.20 per 1M
cacheReadInputTokenCost: 0.00000006, // $0.06 per 1M
⋮----
inputCostPerToken: 0.0000025, // $2.50 per 1M
outputCostPerToken: 0.0000075, // $7.50 per 1M
cacheReadInputTokenCost: 0.0000005, // $0.50 per 1M
cacheWriteInputTokenCost: 0.000003125, // $3.125 per 1M
⋮----
inputCostPerToken: 0.0000004, // $0.40 per 1M (≤ 256K tier)
outputCostPerToken: 0.0000016, // $1.60 per 1M (≤ 256K tier)
cacheReadInputTokenCost: 0.00000004, // $0.04 per 1M (≤ 256K tier)
cacheWriteInputTokenCost: 0.0000005, // $0.50 per 1M (≤ 256K tier)
⋮----
const toClinePassModel = (metadata: ClinePassModelMetadata, providerId: string): Model => (
⋮----
interface ClinePassApiModel {
  id: string;
}
⋮----
interface ClinePassApiResponse {
  data: ClinePassApiModel[];
}
⋮----
const resolveApiKey = (provider: ClinePassProvider, settings: SettingsData, projectDir?: string): string =>
⋮----
export const loadClinePassModels = async (profile: ProviderProfile, settings: SettingsData): Promise<LoadModelsResponse> =>
⋮----
export const hasClinePassEnvVars = (settings: SettingsData): boolean =>
⋮----
export const getClinePassAiderMapping = (provider: ProviderProfile, modelId: string, settings: SettingsData, projectDir: string): AiderModelMapping =>
⋮----
export const createClinePassLlm = (profile: ProviderProfile, model: Model, settings: SettingsData, projectDir: string): LanguageModel =>

================
File: src/main/models/providers/deepseek.ts
================
import { Model, ProviderProfile, Reasoning, SettingsData } from '@common/types';
import { DeepseekProvider, isDeepseekProvider, LlmProvider } from '@common/agent';
import { createDeepSeek } from '@ai-sdk/deepseek';
⋮----
import type { SharedV4ProviderOptions } from '@ai-sdk/provider';
import type { LanguageModel } from 'ai';
⋮----
import { AiderModelMapping, LlmProviderStrategy, LoadModelsResponse } from '@/models';
import logger from '@/logger';
import { getEffectiveEnvironmentVariable } from '@/utils';
import { getDefaultModelInfo, getDefaultUsageReport } from '@/models/providers/default';
⋮----
export const loadDeepseekModels = async (profile: ProviderProfile, settings: SettingsData): Promise<LoadModelsResponse> =>
⋮----
export const hasDeepseekEnvVars = (settings: SettingsData): boolean =>
⋮----
export const getDeepseekAiderMapping = (provider: ProviderProfile, modelId: string): AiderModelMapping =>
⋮----
// === LLM Creation Functions ===
export const createDeepseekLlm = (profile: ProviderProfile, model: Model, settings: SettingsData, projectDir: string): LanguageModel =>
⋮----
const getDeepseekProviderOptions = (llmProvider: LlmProvider, model: Model, reasoning?: Reasoning): SharedV4ProviderOptions | undefined =>
⋮----
// When the top-level reasoning parameter is set (not undefined or 'provider-default'),
// let the AI SDK handle it. For 'none', explicitly disable thinking.
⋮----
const getDeepseekProviderParameters = (llmProvider: LlmProvider, model: Model, reasoning?: Reasoning): Record<string, unknown> =>
⋮----
// === Complete Strategy Implementation ===
⋮----
// Core LLM functions
⋮----
// Model discovery functions

================
File: src/main/models/providers/default.ts
================
import { Model, ModelInfo, ProviderProfile, UsageReportData } from '@common/types';
⋮----
import type { LanguageModelUsage } from 'ai';
⋮----
import { Task } from '@/task';
import logger from '@/logger';
⋮----
export const getDefaultModelInfo = (provider: ProviderProfile, modelId: string, allModelInfos: Record<string, ModelInfo>): ModelInfo | undefined =>
⋮----
// Try provider.name prefix first
⋮----
// If not found, try provider.id prefix
⋮----
// === Cost and Usage Functions ===
export const calculateCost = (model: Model, sentTokens: number, receivedTokens: number, cacheReadTokens: number = 0, cacheWriteTokens: number = 0): number =>
⋮----
export const getDefaultUsageReport = (task: Task, provider: ProviderProfile, model: Model, usage: LanguageModelUsage): UsageReportData =>

================
File: src/main/models/providers/gemini.ts
================
import { ContextUserMessage, Model, ModelInfo, ProviderProfile, SettingsData, UsageReportData, VoiceSession, Reasoning } from '@common/types';
import { DEFAULT_VOICE_SYSTEM_INSTRUCTIONS, GeminiProvider, GeminiVoiceModel, isGeminiProvider, LlmProvider } from '@common/agent';
import { createGoogle, google, type GoogleLanguageModelOptions } from '@ai-sdk/google';
import { Modality } from '@google/genai';
import { v4 as uuidv4 } from 'uuid';
⋮----
import type { LanguageModel, LanguageModelUsage, ModelMessage, ToolSet } from 'ai';
import type { SharedV4ProviderOptions } from '@ai-sdk/provider';
⋮----
import { AiderModelMapping, LlmProviderStrategy, LoadModelsResponse } from '@/models';
import logger from '@/logger';
import { getEffectiveEnvironmentVariable } from '@/utils';
import { Task } from '@/task/task';
import { calculateCost } from '@/models/providers/default';
⋮----
const loadGeminiModels = async (profile: ProviderProfile, settings: SettingsData): Promise<LoadModelsResponse> =>
⋮----
temperature: 0.7, // Default temperature for Gemini models
⋮----
const hasGeminiEnvVars = (settings: SettingsData): boolean =>
⋮----
const getGeminiAiderMapping = (provider: ProviderProfile, modelId: string): AiderModelMapping =>
⋮----
// === LLM Creation Functions ===
const createGeminiLlm = (profile: ProviderProfile, model: Model, settings: SettingsData, projectDir: string): LanguageModel =>
⋮----
type GoogleMetadata = {
  google: {
    cachedContentTokenCount?: number;
  };
};
⋮----
const getGeminiUsageReport = (task: Task, provider: ProviderProfile, model: Model, usage: LanguageModelUsage, providerMetadata?: unknown): UsageReportData =>
⋮----
// Extract cache read tokens from provider metadata or usage
⋮----
// Calculate sentTokens after deducting cached tokens
⋮----
// Calculate cost internally with already deducted sentTokens
⋮----
const getGeminiProviderOptions = (llmProvider: LlmProvider, model: Model, reasoning?: Reasoning): SharedV4ProviderOptions | undefined =>
⋮----
// Use model-specific overrides, falling back to provider defaults
⋮----
// When the top-level reasoning parameter is set (not undefined or 'provider-default'),
// omit thinkingBudget from thinkingConfig so the AI SDK's portable reasoning takes effect.
// Keep includeThoughts if set so reasoning output is still returned.
⋮----
// === Provider Tools Functions ===
const getGeminiProviderTools = (provider: LlmProvider, model: Model): ToolSet =>
⋮----
// Check for model-specific overrides
⋮----
const getGeminiModelInfo = (_provider: ProviderProfile, modelId: string, allModelInfos: Record<string, ModelInfo>): ModelInfo | undefined =>
⋮----
const normalizeGeminiMessages = (_provider: LlmProvider, _model: Model, messages: ModelMessage[]): ModelMessage[] =>
⋮----
const createGeminiVoiceSession = async (profile: ProviderProfile, settings: SettingsData): Promise<VoiceSession> =>
⋮----
// Default to the model specified in requirements or fallback
⋮----
// The transcription-only live model streams text transcriptions instead of audio
// and does not support system instructions or temperature.
⋮----
// Create ephemeral token
// The token is valid for 1 minute for session initiation, and 30 minutes for the session duration by default.
⋮----
const isGeminiRetryable = (error: unknown): boolean =>
⋮----
// Gemini: thought_signature errors (occurs when resuming from another model's conversation) - non-retryable
⋮----
// All other errors are retryable by default
⋮----
// === Complete Strategy Implementation ===
⋮----
// Core LLM functions
⋮----
// Model discovery functions
⋮----
// Message normalization
⋮----
// Error handling

================
File: src/main/models/providers/gpustack.ts
================
import { Model, ProviderProfile, SettingsData } from '@common/types';
import { GpustackProvider, isGpustackProvider } from '@common/agent';
import { createOpenAICompatible } from '@ai-sdk/openai-compatible';
⋮----
import type { LanguageModel } from 'ai';
⋮----
import { AiderModelMapping, LlmProviderStrategy, LoadModelsResponse } from '@/models';
import logger from '@/logger';
import { getEffectiveEnvironmentVariable } from '@/utils';
import { getDefaultUsageReport } from '@/models/providers/default';
⋮----
interface GpustackModelResponse {
  items: Array<{
    name: string;
    meta?: {
      max_model_len?: number;
    };
  }>;
}
⋮----
const loadGpustackModels = async (profile: ProviderProfile, settings: SettingsData): Promise<LoadModelsResponse> =>
⋮----
// Extract max_model_len from meta if available
⋮----
const hasGpustackEnvVars = (settings: SettingsData): boolean =>
⋮----
const getGpustackAiderMapping = (provider: ProviderProfile, modelId: string, settings: SettingsData, projectDir: string): AiderModelMapping =>
⋮----
// Use openai prefix for GPUStack providers (compatible with OpenAI format)
⋮----
// === LLM Creation Functions ===
const createGpustackLlm = (profile: ProviderProfile, model: Model, settings: SettingsData, projectDir: string): LanguageModel =>
⋮----
// Use createOpenAICompatible to get a provider instance, then get the model
// GPUStack uses /v1-openai prefix for OpenAI compatibility
⋮----
// === Complete Strategy Implementation ===
⋮----
// Core LLM functions
⋮----
// Model discovery functions

================
File: src/main/models/providers/groq.ts
================
import { Model, ProviderProfile, SettingsData } from '@common/types';
import { GroqProvider, isGroqProvider } from '@common/agent';
import { createGroq } from '@ai-sdk/groq';
⋮----
import type { LanguageModel } from 'ai';
⋮----
import { AiderModelMapping, LlmProviderStrategy, LoadModelsResponse } from '@/models';
import logger from '@/logger';
import { getEffectiveEnvironmentVariable } from '@/utils';
import { getDefaultModelInfo, getDefaultUsageReport } from '@/models/providers/default';
⋮----
interface GroqModel {
  id: string;
}
⋮----
interface GroqApiResponse {
  data: GroqModel[];
}
⋮----
export const loadGroqModels = async (profile: ProviderProfile, settings: SettingsData): Promise<LoadModelsResponse> =>
⋮----
export const hasGroqEnvVars = (settings: SettingsData): boolean =>
⋮----
export const getGroqAiderMapping = (provider: ProviderProfile, modelId: string): AiderModelMapping =>
⋮----
// === LLM Creation Functions ===
export const createGroqLlm = (profile: ProviderProfile, model: Model, settings: SettingsData, projectDir: string): LanguageModel =>
⋮----
// === Complete Strategy Implementation ===
⋮----
// Core LLM functions
⋮----
// Model discovery functions

================
File: src/main/models/providers/kimi-plan.ts
================
import { Model, ProviderProfile, SettingsData } from '@common/types';
import { isKimiPlanProvider, KimiPlanProvider } from '@common/agent';
import { createAnthropic } from '@ai-sdk/anthropic';
⋮----
import type { LanguageModel } from 'ai';
⋮----
import { AiderModelMapping, LlmProviderStrategy, LoadModelsResponse } from '@/models';
import logger from '@/logger';
import { getEffectiveEnvironmentVariable } from '@/utils';
import { getAnthropicCacheControl } from '@/models/providers/anthropic';
import { getDefaultUsageReport } from '@/models/providers/default';
⋮----
const loadKimiPlanModels = async (profile: ProviderProfile, settings: SettingsData): Promise<LoadModelsResponse> =>
⋮----
// Return hardcoded models instead of fetching from API
⋮----
const hasKimiPlanEnvVars = (settings: SettingsData): boolean =>
⋮----
const getKimiPlanAiderMapping = (provider: ProviderProfile, modelId: string, settings: SettingsData, projectDir: string): AiderModelMapping =>
⋮----
// remove /v1 from the end of the base url for LiteLLM compatibility
⋮----
// Use anthropic prefix for Kimi since it uses Anthropic-compatible API
⋮----
// === LLM Creation Functions ===
const createKimiPlanLlm = (profile: ProviderProfile, model: Model, settings: SettingsData, projectDir: string): LanguageModel =>
⋮----
// Use createAnthropic with Kimi's base URL
⋮----
// === Complete Strategy Implementation ===
⋮----
// Core LLM functions
⋮----
// Model discovery functions
⋮----
// Cache control

================
File: src/main/models/providers/litellm.ts
================
import { createOpenAICompatible } from '@ai-sdk/openai-compatible';
import { isLitellmProvider, LitellmProvider } from '@common/agent';
import { Model, ProviderProfile, SettingsData } from '@common/types';
⋮----
import type { LanguageModel } from 'ai';
⋮----
import logger from '@/logger';
import { AiderModelMapping, LlmProviderStrategy, LoadModelsResponse } from '@/models';
import { getEffectiveEnvironmentVariable } from '@/utils';
import { getDefaultUsageReport } from '@/models/providers/default';
⋮----
interface LiteLLMModelInfo {
  model_name: string;
  id?: string;
  max_tokens?: number;
  max_input_tokens?: number;
  max_output_tokens?: number;
  context_window?: number;
  input_cost_per_token?: number;
  output_cost_per_token?: number;
  litellm_params?: {
    max_tokens?: number;
    max_input_tokens?: number;
    max_output_tokens?: number;
    context_window?: number;
    input_cost_per_token?: number;
    output_cost_per_token?: number;
  };
  model_info?: {
    max_tokens?: number;
    max_input_tokens?: number;
    max_output_tokens?: number;
    context_window?: number;
    input_cost_per_token?: number;
    output_cost_per_token?: number;
  };
}
⋮----
export const loadLitellmModels = async (profile: ProviderProfile, settings: SettingsData): Promise<LoadModelsResponse> =>
⋮----
// Fetch /model/info for detailed pricing and config
// This is required for LiteLLM provider
⋮----
// Sometimes it might return a map directly
⋮----
// Group by model_name to handle load balancing
⋮----
// Helper to find value in multiple places for a single info object
const getValue = (
        info: LiteLLMModelInfo,
        key: 'context_window' | 'max_input_tokens' | 'max_output_tokens' | 'max_tokens' | 'input_cost_per_token' | 'output_cost_per_token',
) =>
⋮----
// Aggregate values across all backends for this model name
// For limits (context window, max output), use MIN to be safe
// For costs, use MAX to be safe/conservative
⋮----
export const hasLitellmEnvVars = (settings: SettingsData): boolean =>
⋮----
export const getLitellmAiderMapping = (provider: ProviderProfile, modelId: string): AiderModelMapping =>
⋮----
// Ensure no trailing slash to avoid double slashes if Aider appends paths
⋮----
// === LLM Creation Functions ===
export const createLitellmLlm = (profile: ProviderProfile, model: Model, settings: SettingsData, projectDir: string): LanguageModel =>
⋮----
apiKey = 'sk-dummy'; // Dummy key for OpenAI-compatible client which expects a key.
⋮----
// === Complete Strategy Implementation ===
⋮----
// Core LLM functions
⋮----
// Model discovery functions

================
File: src/main/models/providers/llmapi.ts
================
import { Model, ProviderProfile, SettingsData, UsageReportData } from '@common/types';
import { isLlmApiProvider, LlmApiProvider } from '@common/agent';
import { createOpenAICompatible } from '@ai-sdk/openai-compatible';
⋮----
import type { LanguageModel, LanguageModelUsage } from 'ai';
⋮----
import { AIDER_DESK_TITLE, AIDER_DESK_WEBSITE } from '@/constants';
import { AiderModelMapping, LlmProviderStrategy, LoadModelsResponse } from '@/models';
import logger from '@/logger';
import { getEffectiveEnvironmentVariable } from '@/utils';
import { getDefaultUsageReport } from '@/models/providers/default';
import { Task } from '@/task/task';
⋮----
interface LlmApiPricing {
  prompt?: string;
  completion?: string;
  input_cache_read?: string;
  input_cache_write?: string;
}
⋮----
interface LlmApiModel {
  id: string;
  context_length?: number;
  pricing: LlmApiPricing;
}
⋮----
interface LlmApiModelsResponse {
  data: LlmApiModel[];
}
⋮----
const parsePrice = (price: string | undefined): number | undefined =>
⋮----
const loadLlmApiModels = async (profile: ProviderProfile, settings: SettingsData): Promise<LoadModelsResponse> =>
⋮----
export const hasLlmApiEnvVars = (settings: SettingsData): boolean =>
⋮----
export const getLlmApiAiderMapping = (provider: ProviderProfile, modelId: string, settings: SettingsData, projectDir: string): AiderModelMapping =>
⋮----
// LLMAPI is OpenAI-compatible, so we use the OpenAI endpoint for Aider
⋮----
// === LLM Creation Functions ===
export const createLlmApiLlm = (profile: ProviderProfile, model: Model, settings: SettingsData, projectDir: string): LanguageModel =>
⋮----
// === Cost and Usage Functions ===
export const getLlmApiUsageReport = (task: Task, provider: ProviderProfile, model: Model, usage: LanguageModelUsage): UsageReportData
⋮----
// === Complete Strategy Implementation ===
⋮----
// Core LLM functions
⋮----
// Model discovery functions

================
File: src/main/models/providers/lm-studio.ts
================
import { Model, ProviderProfile, SettingsData } from '@common/types';
import { isLmStudioProvider, LmStudioProvider } from '@common/agent';
import { createOpenAICompatible } from '@ai-sdk/openai-compatible';
⋮----
import type { LanguageModel } from 'ai';
⋮----
import { AiderModelMapping, LlmProviderStrategy, LoadModelsResponse } from '@/models';
import logger from '@/logger';
import { getEffectiveEnvironmentVariable } from '@/utils';
import { getDefaultUsageReport } from '@/models/providers/default';
⋮----
export const loadLmStudioModels = async (profile: ProviderProfile, settings: SettingsData): Promise<LoadModelsResponse> =>
⋮----
const normalized = effectiveBaseUrl.replace(/\/+$/g, ''); // Remove all trailing slashes
⋮----
export const hasLmStudioEnvVars = (settings: SettingsData): boolean =>
⋮----
export const getLmStudioAiderMapping = (provider: ProviderProfile, modelId: string): AiderModelMapping =>
⋮----
// === LLM Creation Functions ===
export const createLmStudioLlm = (profile: ProviderProfile, model: Model, settings: SettingsData, projectDir: string): LanguageModel =>
⋮----
// === Complete Strategy Implementation ===
⋮----
// Core LLM functions
⋮----
// Model discovery functions

================
File: src/main/models/providers/minimax.ts
================
import { createAnthropic } from '@ai-sdk/anthropic';
import { isMinimaxProvider, MinimaxProvider, LlmProvider } from '@common/agent';
import { Model, ProviderProfile, Reasoning, SettingsData } from '@common/types';
⋮----
import type { LanguageModel } from 'ai';
import type { SharedV4ProviderOptions } from '@ai-sdk/provider';
⋮----
import logger from '@/logger';
import { AiderModelMapping, CacheControl, LlmProviderStrategy } from '@/models';
import { LoadModelsResponse } from '@/models/types';
import { getEffectiveEnvironmentVariable } from '@/utils';
import { getDefaultUsageReport } from '@/models/providers/default';
⋮----
export const loadMinimaxModels = async (profile: ProviderProfile): Promise<LoadModelsResponse> =>
⋮----
// Hardcoded MiniMax models - no API call needed
⋮----
inputCostPerToken: 0.0000003, // 0.3 per 1M tokens
outputCostPerToken: 0.0000012, // 1.2 per 1M tokens
cacheReadInputTokenCost: 0.00000006, // 0.06 per 1M tokens
cacheWriteInputTokenCost: 0.000000375, // 0.375 per 1M tokens
⋮----
inputCostPerToken: 0.0000003, // 0.3 per 1M tokens
outputCostPerToken: 0.0000012, // 1.2 per 1M tokens
cacheReadInputTokenCost: 0.00000006, // 0.06 per 1M tokens
cacheWriteInputTokenCost: 0.000000375, // 0.375 per 1M tokens
⋮----
inputCostPerToken: 0.0000006, // 0.6 per 1M tokens
outputCostPerToken: 0.0000024, // 2.4 per 1M tokens
cacheReadInputTokenCost: 0.00000006, // 0.06 per 1M tokens
cacheWriteInputTokenCost: 0.000000375, // 0.375 per 1M tokens
⋮----
inputCostPerToken: 0.0000003, // 0.3 per 1M tokens
outputCostPerToken: 0.0000012, // 1.2 per 1M tokens
cacheReadInputTokenCost: 0.00000003, // 0.03 per 1M tokens
cacheWriteInputTokenCost: 0.000000375, // 0.375 per 1M tokens
⋮----
inputCostPerToken: 0.0000006, // 0.6 per 1M tokens
outputCostPerToken: 0.0000024, // 2.4 per 1M tokens
cacheReadInputTokenCost: 0.00000003, // 0.03 per 1M tokens
cacheWriteInputTokenCost: 0.000000375, // 0.375 per 1M tokens
⋮----
inputCostPerToken: 0.0000003, // 0.3 per 1M tokens
outputCostPerToken: 0.0000012, // 1.2 per 1M tokens
cacheReadInputTokenCost: 0.00000003, // 0.03 per 1M tokens
cacheWriteInputTokenCost: 0.000000375, // 0.375 per 1M tokens
⋮----
inputCostPerToken: 0.0000006, // 0.6 per 1M tokens
outputCostPerToken: 0.0000024, // 2.4 per 1M tokens
cacheReadInputTokenCost: 0.00000003, // 0.03 per 1M tokens
cacheWriteInputTokenCost: 0.000000375, // 0.375 per 1M tokens
⋮----
inputCostPerToken: 0.0000003, // 0.3 per 1M tokens
outputCostPerToken: 0.0000012, // 1.2 per 1M tokens
cacheReadInputTokenCost: 0.00000003, // 0.03 per 1M tokens
cacheWriteInputTokenCost: 0.000000375, // 0.375 per 1M tokens
⋮----
export const hasMinimaxEnvVars = (settings: SettingsData): boolean =>
⋮----
export const getMinimaxAiderMapping = (provider: ProviderProfile, modelId: string, settings: SettingsData, projectDir: string): AiderModelMapping =>
⋮----
// === LLM Creation Functions ===
export const createMinimaxLlm = (profile: ProviderProfile, model: Model, settings: SettingsData, projectDir: string): LanguageModel =>
⋮----
// === Configuration Helper Functions ===
export const getMinimaxCacheControl = (): CacheControl | undefined =>
⋮----
export const getMinimaxProviderOptions = (llmProvider: LlmProvider, _model: Model, reasoning?: Reasoning): SharedV4ProviderOptions | undefined =>
⋮----
// Explicitly request adaptive thinking with summarized display so reasoning/thinking
// text is returned via thinking_delta events. Without this, newer Claude models (opus-4-7+)
// default to 'omitted' display and return empty thinking blocks.
⋮----
// === Complete Strategy Implementation ===
⋮----
// Core LLM functions
⋮----
// Model discovery functions
⋮----
// Configuration helpers

================
File: src/main/models/providers/mistral.ts
================
import { Model, ProviderProfile, SettingsData } from '@common/types';
import { MistralProvider, isMistralProvider } from '@common/agent';
import { createMistral } from '@ai-sdk/mistral';
⋮----
import type { LanguageModel } from 'ai';
⋮----
import { AiderModelMapping, LlmProviderStrategy, LoadModelsResponse } from '@/models';
import logger from '@/logger';
import { getEffectiveEnvironmentVariable } from '@/utils';
import { getDefaultModelInfo, getDefaultUsageReport } from '@/models/providers/default';
⋮----
interface MistralModel {
  id: string;
}
⋮----
interface MistralApiResponse {
  data: MistralModel[];
}
⋮----
export const loadMistralModels = async (profile: ProviderProfile, settings: SettingsData): Promise<LoadModelsResponse> =>
⋮----
export const hasMistralEnvVars = (settings: SettingsData): boolean =>
⋮----
export const getMistralAiderMapping = (provider: ProviderProfile, modelId: string): AiderModelMapping =>
⋮----
// === LLM Creation Functions ===
export const createMistralLlm = (profile: ProviderProfile, model: Model, settings: SettingsData, projectDir: string): LanguageModel =>
⋮----
// === Complete Strategy Implementation ===
⋮----
// Core LLM functions
⋮----
// Model discovery functions

================
File: src/main/models/providers/neuralwatt.ts
================
import { Model, ProviderProfile, Reasoning, SettingsData } from '@common/types';
import { isNeuralwattProvider, LlmProvider, NeuralwattProvider } from '@common/agent';
import { createOpenAICompatible } from '@ai-sdk/openai-compatible';
⋮----
import type { LanguageModel } from 'ai';
import type { SharedV4ProviderOptions } from '@ai-sdk/provider';
⋮----
import { AiderModelMapping, LlmProviderStrategy, LoadModelsResponse } from '@/models';
import logger from '@/logger';
import { getEffectiveEnvironmentVariable } from '@/utils';
import { getDefaultModelInfo, getDefaultUsageReport } from '@/models/providers/default';
⋮----
interface NeuralwattModelPricing {
  input_per_million: number;
  output_per_million: number;
  cached_input_per_million?: number | null;
  cached_output_per_million?: number | null;
  currency: string;
  pricing_tbd: boolean;
}
⋮----
interface NeuralwattModelCapabilities {
  tools: boolean;
  json_mode: boolean;
  vision: boolean;
  reasoning: boolean;
  reasoning_effort: boolean;
  streaming: boolean;
  system_role: boolean;
  developer_role: boolean;
}
⋮----
interface NeuralwattModelLimits {
  max_context_length?: number | null;
  max_output_tokens?: number | null;
  max_images?: number | null;
}
⋮----
interface NeuralwattModelMetadata {
  display_name: string;
  description?: string | null;
  provider?: string;
  pricing: NeuralwattModelPricing;
  capabilities: NeuralwattModelCapabilities;
  limits: NeuralwattModelLimits;
  deprecated?: boolean;
  deprecated_message?: string | null;
}
⋮----
interface NeuralwattModelEntry {
  id: string;
  object: string;
  created: number;
  owned_by: string;
  max_model_len?: number;
  metadata?: NeuralwattModelMetadata;
}
⋮----
const loadNeuralwattModels = async (profile: ProviderProfile, settings: SettingsData): Promise<LoadModelsResponse> =>
⋮----
const hasNeuralwattEnvVars = (settings: SettingsData): boolean =>
⋮----
const getNeuralwattAiderMapping = (provider: ProviderProfile, modelId: string): AiderModelMapping =>
⋮----
const createNeuralwattLlm = (profile: ProviderProfile, model: Model, settings: SettingsData, projectDir: string): LanguageModel =>
⋮----
const getNeuralwattProviderOptions = (llmProvider: LlmProvider, model: Model, reasoning?: Reasoning): SharedV4ProviderOptions | undefined =>
⋮----
// When the top-level reasoning parameter is set (not undefined or 'provider-default'),
// omit reasoningEffort so the AI SDK's portable reasoning takes effect.
⋮----
// Extract reasoningEffort from model overrides or provider config
⋮----
// Map ReasoningEffort enum to AI SDK format

================
File: src/main/models/providers/ollama.ts
================
import { Model, ProviderProfile, SettingsData } from '@common/types';
import { isOllamaProvider, OllamaProvider } from '@common/agent';
import { createOllama } from 'ollama-ai-provider-v2';
import { simulateStreamingMiddleware, wrapLanguageModel } from 'ai';
⋮----
import type { LanguageModel } from 'ai';
⋮----
import { AiderModelMapping, LlmProviderStrategy, LoadModelsResponse } from '@/models';
import logger from '@/logger';
import { getEffectiveEnvironmentVariable } from '@/utils';
import { getDefaultUsageReport } from '@/models/providers/default';
⋮----
export const loadOllamaModels = async (profile: ProviderProfile, settings: SettingsData): Promise<LoadModelsResponse> =>
⋮----
let normalized = effectiveBaseUrl.replace(/\/+$/, ''); // Remove all trailing slashes
⋮----
export const hasOllamaEnvVars = (settings: SettingsData): boolean =>
⋮----
export const getOllamaAiderMapping = (provider: ProviderProfile, modelId: string): AiderModelMapping =>
⋮----
// === LLM Creation Functions ===
export const createOllamaLlm = (profile: ProviderProfile, model: Model, settings: SettingsData, projectDir: string): LanguageModel =>
⋮----
let normalized = baseUrl.replace(/\/+$/, ''); // Remove all trailing slashes
⋮----
// === Complete Strategy Implementation ===
⋮----
// Core LLM functions
⋮----
// Model discovery functions

================
File: src/main/models/providers/openai-compatible.ts
================
import { Model, ProviderProfile, ReasoningEffort, SettingsData, Reasoning, TlsPolicyRegistrar } from '@common/types';
import { isOpenAiCompatibleProvider, LlmProvider, OpenAiCompatibleProvider } from '@common/agent';
import { createOpenAICompatible } from '@ai-sdk/openai-compatible';
⋮----
import type { JSONValue, SharedV4ProviderOptions } from '@ai-sdk/provider';
import type { LanguageModel, ToolSet } from 'ai';
⋮----
import { AiderModelMapping, LlmProviderStrategy, LoadModelsResponse } from '@/models';
import logger from '@/logger';
import { getEffectiveEnvironmentVariable } from '@/utils';
import { getDefaultUsageReport } from '@/models/providers/default';
import { syncProviderTlsRule } from '@/models/utils';
⋮----
const loadOpenaiCompatibleModels = async (profile: ProviderProfile, settings: SettingsData, tlsRegistrar?: TlsPolicyRegistrar): Promise<LoadModelsResponse> =>
⋮----
const hasOpenAiCompatibleEnvVars = (settings: SettingsData): boolean =>
⋮----
const getOpenAiCompatibleAiderMapping = (provider: ProviderProfile, modelId: string): AiderModelMapping =>
⋮----
// Use openai prefix for OpenAI-compatible providers
⋮----
// === LLM Creation Functions ===
const createOpenAiCompatibleLlm = (
  profile: ProviderProfile,
  model: Model,
  settings: SettingsData,
  projectDir: string,
  _toolSet?: ToolSet,
  _systemPrompt?: string,
  _providerMetadata?: unknown,
  tlsRegistrar?: TlsPolicyRegistrar,
): LanguageModel =>
⋮----
// Use createOpenAICompatible to get a provider instance, then get the model
⋮----
// === Configuration Helper Functions ===
const getOpenAiCompatibleProviderOptions = (provider: LlmProvider, model: Model, reasoning?: Reasoning): SharedV4ProviderOptions | undefined =>
⋮----
// Extract reasoningEffort from model overrides or provider config
⋮----
// When the top-level reasoning parameter is set (not undefined or 'provider-default'),
// omit reasoningEffort from providerOptions so the AI SDK's portable reasoning takes effect.
// Still apply extraBody since it may contain unrelated provider settings.
⋮----
// === Complete Strategy Implementation ===
⋮----
// Core LLM functions
⋮----
// Model discovery functions
⋮----
// Configuration helper functions

================
File: src/main/models/providers/openai.ts
================
import { createOpenAI } from '@ai-sdk/openai';
import { DEFAULT_VOICE_SYSTEM_INSTRUCTIONS, isOpenAiProvider, LlmProvider, OpenAiProvider, OpenAiVoiceModel } from '@common/agent';
import { Reasoning, Model, ProviderProfile, ReasoningEffort, SettingsData, VoiceSession } from '@common/types';
⋮----
import type { LanguageModel, ToolSet } from 'ai';
import type { SharedV4ProviderOptions } from '@ai-sdk/provider';
⋮----
import logger from '@/logger';
import { AiderModelMapping, LlmProviderStrategy, LoadModelsResponse } from '@/models';
import { getEffectiveEnvironmentVariable } from '@/utils';
import { getDefaultModelInfo, getDefaultUsageReport } from '@/models/providers/default';
⋮----
export const loadOpenAiModels = async (profile: ProviderProfile, settings: SettingsData): Promise<LoadModelsResponse> =>
⋮----
export const hasOpenAiEnvVars = (settings: SettingsData): boolean =>
⋮----
export const getOpenAiAiderMapping = (provider: ProviderProfile, modelId: string): AiderModelMapping =>
⋮----
// clear any custom base URL
⋮----
// === LLM Creation Functions ===
export const createOpenAiLlm = (profile: ProviderProfile, model: Model, settings: SettingsData, projectDir: string): LanguageModel =>
⋮----
// === Configuration Helper Functions ===
export const getOpenAiProviderOptions = (provider: LlmProvider, model: Model, reasoning?: Reasoning): SharedV4ProviderOptions | undefined =>
⋮----
// Extract store setting from model overrides or provider config.
// Defaults to true (AI SDK default). Set to false for stateless/ZDR mode so
// resuming tasks does not reference server-side items that are not persisted.
⋮----
// When the top-level reasoning parameter is set (not undefined or 'provider-default'),
// omit reasoningEffort from providerOptions so the AI SDK's portable reasoning takes effect.
// Keep reasoningSummary so reasoning output is still returned.
⋮----
// Extract reasoningEffort from model overrides or provider config
⋮----
// Map ReasoningEffort enum to AI SDK format
⋮----
// === Provider Tools Functions ===
export const getOpenAiProviderTools = (provider: LlmProvider, model: Model): ToolSet =>
⋮----
// Check for model-specific overrides
⋮----
// === Complete Strategy Implementation ===
const createOpenAIVoiceSession = async (profile: ProviderProfile, settings: SettingsData): Promise<VoiceSession> =>
⋮----
// Generate ephemeral token for OpenAI Realtime API
// This creates a short-lived token specifically for Realtime API usage
⋮----
// === Complete Strategy Implementation ===
⋮----
// Core LLM functions
⋮----
// Model discovery functions
⋮----
// Configuration helper functions
⋮----
// Voice support

================
File: src/main/models/providers/opencode-go.ts
================
import { Model, ProviderProfile, SettingsData, TlsPolicyRegistrar } from '@common/types';
import { isOpenCodeGoProvider, LlmProvider, OpenCodeGoProvider } from '@common/agent';
import { createOpenAI } from '@ai-sdk/openai';
import { createAnthropic } from '@ai-sdk/anthropic';
import { createOpenAICompatible } from '@ai-sdk/openai-compatible';
import { omit } from 'lodash';
⋮----
import { getDefaultModelInfo, getDefaultUsageReport } from './default';
⋮----
import type { LanguageModel, ModelMessage, ToolSet } from 'ai';
⋮----
import { AiderModelMapping, LlmProviderStrategy, LoadModelsResponse } from '@/models';
import { getEffectiveEnvironmentVariable } from '@/utils';
⋮----
interface OpenCodeGoModel {
  id: string;
  object: string;
  created: number;
  owned_by: string;
}
⋮----
interface OpenCodeGoModelsResponse {
  object: string;
  data: OpenCodeGoModel[];
}
⋮----
type ModelEndpointType = 'openai-responses' | 'anthropic' | 'openai-compatible';
⋮----
// Based on the documented Go endpoints (https://opencode.ai/docs/go#endpoints). Model IDs for Go
// don't follow a predictable naming scheme, so unlike the OpenCode Zen provider we map them explicitly.
⋮----
const getModelEndpointType = (modelId: string): ModelEndpointType =>
⋮----
// Fallback for models not yet in the documented list.
⋮----
const loadOpencodeGoModels = async (profile: ProviderProfile, settings: SettingsData): Promise<LoadModelsResponse> =>
⋮----
export const hasOpencodeGoEnvVars = (settings: SettingsData): boolean =>
⋮----
export const getOpencodeGoAiderMapping = (provider: ProviderProfile, modelId: string, settings: SettingsData, projectDir: string): AiderModelMapping =>
⋮----
// === LLM Creation Functions ===
export const createOpencodeGoLlm = (
  profile: ProviderProfile,
  model: Model,
  settings: SettingsData,
  projectDir: string,
  _toolSet?: ToolSet,
  _systemPrompt?: string,
  _providerMetadata?: unknown,
  _tlsRegistrar?: TlsPolicyRegistrar,
  sessionId?: string,
): LanguageModel =>
⋮----
// Grok's upstream rejects the `item_reference` input items that @ai-sdk/openai emits for
// Responses history parts carrying OpenAI item IDs. Stripping that metadata makes the SDK
// inline full content instead while keeping store (and thus caching behavior) at its default.
const normalizeOpencodeGoMessages = (llmProvider: LlmProvider, model: Model, messages: ModelMessage[]): ModelMessage[] =>
⋮----
// === Complete Strategy Implementation ===
⋮----
// Core LLM functions
⋮----
// Model discovery functions
⋮----
// Message normalization

================
File: src/main/models/providers/opencode.ts
================
import { Model, ProviderProfile, SettingsData } from '@common/types';
import { isOpenCodeProvider, OpenCodeProvider } from '@common/agent';
import { createOpenAI } from '@ai-sdk/openai';
import { createAnthropic } from '@ai-sdk/anthropic';
import { createGoogle } from '@ai-sdk/google';
import { createOpenAICompatible } from '@ai-sdk/openai-compatible';
⋮----
import { getDefaultModelInfo, getDefaultUsageReport } from './default';
⋮----
import type { LanguageModel } from 'ai';
⋮----
import { AiderModelMapping, LlmProviderStrategy, LoadModelsResponse } from '@/models';
import { getEffectiveEnvironmentVariable } from '@/utils';
⋮----
interface OpenCodeModel {
  id: string;
  object: string;
  created: number;
  owned_by: string;
}
⋮----
interface OpenCodeModelsResponse {
  object: string;
  data: OpenCodeModel[];
}
⋮----
type ModelEndpointType = 'openai' | 'anthropic' | 'gemini' | 'openai-compatible';
⋮----
const getModelEndpointType = (modelId: string): ModelEndpointType =>
⋮----
const getDefaultModelTemperature = (modelId: string) =>
⋮----
const loadOpencodeModels = async (profile: ProviderProfile, settings: SettingsData): Promise<LoadModelsResponse> =>
⋮----
export const hasOpencodeEnvVars = (settings: SettingsData): boolean =>
⋮----
export const getOpencodeAiderMapping = (provider: ProviderProfile, modelId: string, settings: SettingsData, projectDir: string): AiderModelMapping =>
⋮----
// Use openai prefix for OpenCode providers
⋮----
// === LLM Creation Functions ===
export const createOpencodeLlm = (profile: ProviderProfile, model: Model, settings: SettingsData, projectDir: string): LanguageModel =>
⋮----
// === Complete Strategy Implementation ===
⋮----
// Core LLM functions
⋮----
// Model discovery functions

================
File: src/main/models/providers/openrouter.ts
================
import { Model, ProviderProfile, SettingsData, UsageReportData } from '@common/types';
import { isOpenRouterProvider, LlmProvider, OpenRouterProvider } from '@common/agent';
import { createOpenRouter } from '@openrouter/ai-sdk-provider';
⋮----
import type { LanguageModel, LanguageModelUsage } from 'ai';
⋮----
import { AIDER_DESK_TITLE, AIDER_DESK_WEBSITE } from '@/constants';
import { AiderModelMapping, CacheControl, LlmProviderStrategy, LoadModelsResponse } from '@/models';
import logger from '@/logger';
import { getEffectiveEnvironmentVariable } from '@/utils';
import { Task } from '@/task/task';
import { calculateCost } from '@/models/providers/default';
⋮----
interface OpenRouterTopProvider {
  is_moderated: boolean;
  context_length: number;
  max_completion_tokens: number;
}
⋮----
interface OpenRouterPricing {
  prompt: string;
  completion: string;
  input_cache_read?: string;
  input_cache_write?: string;
}
⋮----
interface OpenRouterModel {
  id: string;
  name: string;
  created: number;
  description: string;
  top_provider: OpenRouterTopProvider;
  pricing: OpenRouterPricing;
  context_length: number;
}
⋮----
interface OpenRouterModelsResponse {
  data: OpenRouterModel[];
}
⋮----
const getDefaultModelTemperature = (modelId: string) =>
⋮----
const loadOpenrouterModels = async (profile: ProviderProfile, settings: SettingsData): Promise<LoadModelsResponse> =>
⋮----
export const hasOpenRouterEnvVars = (settings: SettingsData): boolean =>
⋮----
export const getOpenRouterAiderMapping = (provider: ProviderProfile, modelId: string): AiderModelMapping =>
⋮----
// === LLM Creation Functions ===
export const createOpenRouterLlm = (profile: ProviderProfile, model: Model, settings: SettingsData, projectDir: string): LanguageModel =>
⋮----
type OpenRouterMetadata = {
  openrouter: {
    usage: {
      completionTokens: number;
      completionTokensDetails: {
        reasoningTokens: number;
      };
      cost: number;
      promptTokens: number;
      promptTokensDetails?: {
        cachedTokens: number;
      };
      totalTokens: number;
    };
  };
};
⋮----
export const getOpenRouterUsageReport = (
  task: Task,
  provider: ProviderProfile,
  model: Model,
  usage: LanguageModelUsage,
  providerMetadata?: unknown,
): UsageReportData =>
⋮----
// Extract cache tokens from usage details, falling back to raw provider metadata
⋮----
// Calculate sentTokens after deducting cached tokens
⋮----
// Use cost from provider metadata if available, otherwise calculate
⋮----
// === Configuration Helper Functions ===
export const getOpenRouterCacheControl = (llmProvider: LlmProvider, model: Model): CacheControl | undefined =>
⋮----
// === Complete Strategy Implementation ===
⋮----
// Core LLM functions
⋮----
// Model discovery functions
⋮----
// Configuration helpers

================
File: src/main/models/providers/requesty.ts
================
import { ContextUserMessage, Model, ProviderProfile, ReasoningEffort, SettingsData, UsageReportData } from '@common/types';
import { isRequestyProvider, LlmProvider, RequestyProvider } from '@common/agent';
import { createRequesty, type RequestyProviderMetadata } from '@requesty/ai-sdk';
import { v4 as uuidv4 } from 'uuid';
⋮----
import type { LanguageModel, LanguageModelUsage, ModelMessage } from 'ai';
⋮----
import { AIDER_DESK_TITLE, AIDER_DESK_WEBSITE } from '@/constants';
import { AiderModelMapping, CacheControl, LlmProviderStrategy, LoadModelsResponse } from '@/models';
import logger from '@/logger';
import { getEffectiveEnvironmentVariable } from '@/utils';
import { Task } from '@/task/task';
⋮----
interface RequestyModel {
  id: string;
  created: number;
  owned_by: string;
  input_price: number;
  caching_price?: number;
  cached_price?: number;
  output_price: number;
  max_output_tokens: number;
  context_window: number;
  supports_caching: boolean;
  supports_vision: boolean;
  supports_computer_use: boolean;
  supports_reasoning: boolean;
  description: string;
}
⋮----
interface RequestyModelsResponse {
  data: RequestyModel[];
}
⋮----
const getDefaultModelTemperature = (modelId: string) =>
⋮----
const loadRequestyModels = async (profile: ProviderProfile, settings: SettingsData): Promise<LoadModelsResponse> =>
⋮----
export const hasRequestyEnvVars = (settings: SettingsData): boolean =>
⋮----
export const getRequestyAiderMapping = (provider: ProviderProfile, modelId: string, settings: SettingsData, projectDir: string): AiderModelMapping =>
⋮----
// Requesty doesn't have direct Aider support, so we use OpenAI-compatible endpoint
⋮----
// === LLM Creation Functions ===
export const createRequestyLlm = (profile: ProviderProfile, model: Model, settings: SettingsData, projectDir: string): LanguageModel =>
⋮----
// === Cost and Usage Functions ===
export const calculateRequestyCost = (
  model: Model,
  sentTokens: number,
  receivedTokens: number,
  cacheWriteTokens: number = 0,
  cacheReadTokens: number = 0,
): number =>
⋮----
export const getRequestyUsageReport = (
  task: Task,
  provider: ProviderProfile,
  model: Model,
  usage: LanguageModelUsage,
  providerMetadata?: unknown,
): UsageReportData =>
⋮----
// Extract cache tokens from usage details, falling back to Requesty provider metadata
⋮----
// Prefer non-cached token count from usage details to avoid double counting cached tokens
⋮----
// Calculate cost internally with already deducted sentTokens
⋮----
// === Message Normalization Functions ===
export const normalizeRequestyMessages = (_provider: LlmProvider, model: Model, messages: ModelMessage[]): ModelMessage[] =>
⋮----
// Apply Gemini normalization for Google/Gemini models
⋮----
// === Configuration Helper Functions ===
export const getRequestyCacheControl = (llmProvider: LlmProvider, model: Model): CacheControl | undefined =>
⋮----
// === Complete Strategy Implementation ===
⋮----
// Core LLM functions
⋮----
// Model discovery functions
⋮----
// Configuration helpers
⋮----
// Message normalization

================
File: src/main/models/providers/synthetic.ts
================
import { Model, ModelInfo, ProviderProfile, SettingsData } from '@common/types';
import { isSyntheticProvider, SyntheticProvider } from '@common/agent';
import { createOpenAICompatible } from '@ai-sdk/openai-compatible';
⋮----
import { getDefaultUsageReport } from './default';
⋮----
import type { LanguageModel } from 'ai';
⋮----
import { AiderModelMapping, LlmProviderStrategy, LoadModelsResponse } from '@/models';
import logger from '@/logger';
import { getEffectiveEnvironmentVariable } from '@/utils';
⋮----
const loadSyntheticModels = async (profile: ProviderProfile, settings: SettingsData): Promise<LoadModelsResponse> =>
⋮----
const hasSyntheticEnvVars = (): boolean
⋮----
const getSyntheticAiderMapping = (provider: ProviderProfile, modelId: string, settings: SettingsData, projectDir: string): AiderModelMapping =>
⋮----
const createSyntheticLlm = (profile: ProviderProfile, model: Model, settings: SettingsData, projectDir: string): LanguageModel =>
⋮----
const getSyntheticModelInfo = (_provider: ProviderProfile, modelId: string, allModelInfos: Record<string, ModelInfo>): ModelInfo | undefined =>

================
File: src/main/models/providers/vertex-ai.ts
================
import { v1beta1 } from '@google-cloud/aiplatform';
import { GoogleAuth } from 'google-auth-library';
import { Model, ModelInfo, ProviderProfile, Reasoning, SettingsData, UsageReportData } from '@common/types';
import { isVertexAiProvider, LlmProvider, VertexAiProvider } from '@common/agent';
import { createVertex } from '@ai-sdk/google-vertex';
⋮----
import type { LanguageModel, LanguageModelUsage } from 'ai';
import type { SharedV4ProviderOptions } from '@ai-sdk/provider';
⋮----
import { AiderModelMapping, LlmProviderStrategy, LoadModelsResponse } from '@/models';
import logger from '@/logger';
import { getEffectiveEnvironmentVariable } from '@/utils';
import { Task } from '@/task/task';
import { calculateCost } from '@/models/providers/default';
⋮----
export const loadVertexAIModels = async (profile: ProviderProfile, settings: SettingsData): Promise<LoadModelsResponse> =>
⋮----
// Use provided credentials JSON
⋮----
// Use default credentials (e.g., gcloud, environment variables, or service account)
⋮----
temperature: 0.7, // Default temperature for Vertex AI models
⋮----
export const hasVertexAiEnvVars = (): boolean =>
⋮----
// Vertex AI doesn't have a simple environment variable check like other providers
// It requires project, location, and potentially credentials
⋮----
export const getVertexAiAiderMapping = (provider: ProviderProfile, modelId: string): AiderModelMapping =>
⋮----
// Aider uses vertex_ai prefix instead of vertex-ai
⋮----
// === LLM Creation Functions ===
export const createVertexAiLlm = (profile: ProviderProfile, model: Model, settings: SettingsData, projectDir: string): LanguageModel =>
⋮----
// using custom base URL to fix the 'global' location
⋮----
type VertexGoogleMetadata = {
  vertex: {
    cachedContentTokenCount?: number;
  };
};
⋮----
const getVertexAiUsageReport = (
  task: Task,
  provider: ProviderProfile,
  model: Model,
  usage: LanguageModelUsage,
  providerMetadata?: unknown,
): UsageReportData =>
⋮----
// Extract cache read tokens from provider metadata
⋮----
// Calculate sentTokens after deducting cached tokens
⋮----
// Calculate cost internally with already deducted sentTokens
⋮----
export const getVertexAiProviderOptions = (llmProvider: LlmProvider, model: Model, reasoning?: Reasoning): SharedV4ProviderOptions | undefined =>
⋮----
// Use model-specific overrides, falling back to provider defaults
⋮----
// When the top-level reasoning parameter is set (not undefined or 'provider-default'),
// omit thinkingBudget from thinkingConfig so the AI SDK's portable reasoning takes effect.
// Keep includeThoughts if set so reasoning output is still returned.
⋮----
const getVertexAiModelInfo = (_provider: ProviderProfile, modelId: string, allModelInfos: Record<string, ModelInfo>): ModelInfo | undefined =>
⋮----
// === Complete Strategy Implementation ===
⋮----
// Core LLM functions
⋮----
// Model discovery functions
⋮----
// Configuration helpers

================
File: src/main/models/providers/zai-plan.ts
================
import { Model, ModelInfo, ProviderProfile, Reasoning, ReasoningEffort, SettingsData } from '@common/types';
import { isZaiPlanProvider, LlmProvider, ZaiPlanProvider } from '@common/agent';
import { createOpenAICompatible } from '@ai-sdk/openai-compatible';
⋮----
import { getDefaultUsageReport } from './default';
⋮----
import type { SharedV4ProviderOptions } from '@ai-sdk/provider';
import type { LanguageModel } from 'ai';
⋮----
import { AiderModelMapping, LlmProviderStrategy, LoadModelsResponse } from '@/models';
import logger from '@/logger';
import { getEffectiveEnvironmentVariable } from '@/utils';
⋮----
const loadZaiPlanModels = async (profile: ProviderProfile, settings: SettingsData): Promise<LoadModelsResponse> =>
⋮----
// ZAI uses specific endpoint for model discovery
⋮----
temperature: 0.7, // Default temperature for ZAI models
⋮----
const hasZaiPlanEnvVars = (): boolean
⋮----
const getZaiPlanAiderMapping = (provider: ProviderProfile, modelId: string, settings: SettingsData, projectDir: string): AiderModelMapping =>
⋮----
// Set the base URL for ZAI Plan
⋮----
// Use zai-plan prefix for ZAI providers
⋮----
// === LLM Creation Functions ===
const createZaiPlanLlm = (profile: ProviderProfile, model: Model, settings: SettingsData, projectDir: string): LanguageModel =>
⋮----
// Use createOpenAICompatible to get a provider instance, then get the model
// ZAI uses specific base URL for chat completions
⋮----
const getZaiPlanModelInfo = (_provider: ProviderProfile, modelId: string, allModelInfos: Record<string, ModelInfo>): ModelInfo | undefined =>
⋮----
const getZaiPlanProviderOptions = (llmProvider: LlmProvider, model: Model, reasoning?: Reasoning): SharedV4ProviderOptions | undefined =>
⋮----
// When the top-level reasoning parameter is set (not undefined or 'provider-default'),
// omit thinking and reasoningEffort so the AI SDK's portable reasoning takes effect.
// For 'none', explicitly disable thinking. Keep tool_stream if set.
⋮----
// Only disable thinking if explicitly set to false
⋮----
// === Complete Strategy Implementation ===
⋮----
// Core LLM functions
⋮----
// Model discovery functions

================
File: src/main/models/index.ts
================


================
File: src/main/models/model-manager.ts
================
import { promises as fs } from 'fs';
import path from 'path';
⋮----
import { AVAILABLE_PROVIDERS, getDefaultProviderParams, LlmProvider, LlmProviderName } from '@common/agent';
import { ProviderDefinition } from '@common/extensions';
import {
  Model,
  ModelInfo,
  ModelOverrides,
  ProviderModelsData,
  ProviderProfile,
  Reasoning,
  SettingsData,
  TlsPolicyRegistrar,
  UsageReportData,
  VoiceSession,
} from '@common/types';
import { extractProviderModel } from '@common/utils';
⋮----
import { anthropicProviderStrategy } from './providers/anthropic';
import { anthropicCompatibleProviderStrategy } from './providers/anthropic-compatible';
import { azureProviderStrategy } from './providers/azure';
import { bedrockProviderStrategy } from './providers/bedrock';
import { cerebrasProviderStrategy } from './providers/cerebras';
import { clinePassProviderStrategy } from './providers/clinepass';
import { deepseekProviderStrategy } from './providers/deepseek';
import { geminiProviderStrategy } from './providers/gemini';
import { gpustackProviderStrategy } from './providers/gpustack';
import { groqProviderStrategy } from './providers/groq';
import { alibabaPlanProviderStrategy } from './providers/alibaba-plan';
import { kimiPlanProviderStrategy } from './providers/kimi-plan';
import { litellmProviderStrategy } from './providers/litellm';
import { llmApiProviderStrategy } from './providers/llmapi';
import { lmStudioProviderStrategy } from './providers/lm-studio';
import { minimaxProviderStrategy } from './providers/minimax';
import { mistralProviderStrategy } from './providers/mistral';
import { neuralwattProviderStrategy } from './providers/neuralwatt';
import { ollamaProviderStrategy } from './providers/ollama';
import { openaiProviderStrategy } from './providers/openai';
import { openaiCompatibleProviderStrategy } from './providers/openai-compatible';
import { opencodeProviderStrategy } from './providers/opencode';
import { opencodeGoProviderStrategy } from './providers/opencode-go';
import { openrouterProviderStrategy } from './providers/openrouter';
import { requestyProviderStrategy } from './providers/requesty';
import { syntheticProviderStrategy } from './providers/synthetic';
import { vertexAiProviderStrategy } from './providers/vertex-ai';
import { zaiPlanProviderStrategy } from './providers/zai-plan';
⋮----
import type { RegisteredProvider } from '@/extensions/extension-manager';
import type { SharedV4ProviderOptions } from '@ai-sdk/provider';
import type { LanguageModel, LanguageModelUsage, ModelMessage, ToolSet } from 'ai';
⋮----
import { getDefaultUsageReport } from '@/models/providers/default';
import { AIDER_DESK_CACHE_DIR, AIDER_DESK_DATA_DIR } from '@/constants';
import logger from '@/logger';
import { Store } from '@/store';
import { EventManager } from '@/events';
import { Task } from '@/task/task';
import { AiderModelMapping, CacheControl, LoadModelsResponse, LlmProviderRegistry, LlmProviderStrategy } from '@/models/types';
⋮----
type ProviderModelsCache = {
  version: number;
  providerModels: Record<string, Model[]>;
  providerErrors: Record<string, string>;
};
⋮----
type ModelsMetaResponse = Record<
  string,
  {
    models: Record<
      string,
      {
        id: string;
        cost?: {
          input?: number;
          output?: number;
          cache_read?: number;
          cache_write?: number;
        };
        temperature?: boolean;
        limit: {
          context: number;
          output: number;
        };
      }
    >;
  }
>;
⋮----
export class ModelManager
⋮----
// Provider registry for strategy pattern
⋮----
constructor(
    private store: Store,
    private eventManager: EventManager,
    private readonly tlsRegistrar?: TlsPolicyRegistrar,
)
⋮----
private async init(): Promise<void>
⋮----
async waitForInit(): Promise<void>
⋮----
private async loadModelsInfo(): Promise<void>
⋮----
// Try to load from cache first
⋮----
// Cache file doesn't exist or is invalid, we'll fetch fresh data
⋮----
const fetchFreshDataAndCache = async (cacheFile: string): Promise<void> =>
⋮----
// Save the fresh data to cache
⋮----
// Fetch fresh data in background if cache was loaded, otherwise await it
⋮----
private async loadProviderModelsFromCache(): Promise<boolean>
⋮----
private async saveProviderModelsToCache(): Promise<void>
⋮----
private loadProviderModelsInBackground(providers: ProviderProfile[]): void
⋮----
private processModelsMeta(data: ModelsMetaResponse)
⋮----
getModelInfo(modelName: string): ModelInfo | undefined
⋮----
private createEnvVarProvider(providerName: LlmProviderName): ProviderProfile
⋮----
private getChangedProviders(oldProviders: ProviderProfile[], newProviders: ProviderProfile[]): ProviderProfile[]
⋮----
// Check for added/modified providers
⋮----
private async loadModelsWithRetry(strategy: LlmProviderStrategy, profile: ProviderProfile, retryCount = 3): Promise<LoadModelsResponse>
⋮----
async providersChanged(oldProviders: ProviderProfile[], newProviders: ProviderProfile[])
⋮----
// Clear models for removed providers
⋮----
// Clear models for providers that became disabled
⋮----
private async withModelLoadTimeout(promise: Promise<void>, profile: ProviderProfile): Promise<void>
⋮----
private async loadProviderModels(providers: ProviderProfile[]): Promise<void>
⋮----
const loadModels = async () =>
⋮----
// Emit the updated provider models event
⋮----
// Update agent profiles with the new models
// Note: agent profiles are now file-based, so this update is handled differently
⋮----
private enrichWithModelInfo(models: Model[], profile: ProviderProfile, strategy: LlmProviderStrategy): Model[]
⋮----
// remove temperature if model does not support it
⋮----
private enrichWithOverrides(models: Model[], providerId: string): Model[]
⋮----
// maxOutputTokens and temperature should be also overridden by undefined values
⋮----
/**
   * Detect and add automatic providers from environment variables
   */
private updateEnvVarsProviders()
⋮----
async getProviderModels(reload = false): Promise<ProviderModelsData>
⋮----
// Clear cached models if reloading
⋮----
// Load models from all enabled providers (including extension providers)
⋮----
private async loadModelOverrides(): Promise<void>
⋮----
private async saveModelOverrides(): Promise<void>
⋮----
async upsertModel(providerId: string, modelId: string, model: Model): Promise<void>
⋮----
async deleteModel(providerId: string, modelId: string): Promise<void>
⋮----
async updateModels(modelUpdates: Array<
⋮----
// Reload models for all affected providers at once
⋮----
getAiderModelMapping(modelName: string, projectDir: string): AiderModelMapping
⋮----
private getProviderAiderMapping(provider: ProviderProfile, modelId: string, projectDir: string): AiderModelMapping
⋮----
getModelSettings(providerId: string, modelId: string, useModelInfoFallback = false): Model | undefined
⋮----
createLlm(
    provider: ProviderProfile,
    model: string | Model,
    settings: SettingsData,
    projectDir: string,
    toolSet?: ToolSet,
    systemPrompt?: string,
    providerMetadata?: unknown,
    sessionId?: string,
): LanguageModel | Promise<LanguageModel>
⋮----
// Resolve Model object if string is provided
⋮----
// Fallback to creating a minimal Model object if not found
⋮----
getUsageReport(task: Task, provider: ProviderProfile, model: string | Model, usage: LanguageModelUsage, providerMetadata?: unknown): UsageReportData
⋮----
// Resolve Model object, falling back to a minimal object so usage reports
// still work when provider models failed to load (e.g. network issues)
⋮----
getCacheControl(provider: ProviderProfile, modelId: string): CacheControl | undefined
⋮----
isStreamingDisabled(provider: ProviderProfile, modelId: string): boolean
⋮----
isToolCallStreamingDisabled(provider: ProviderProfile, modelId: string): boolean
⋮----
getProviderOptions(provider: ProviderProfile, modelId: string, reasoning?: Reasoning): SharedV4ProviderOptions | undefined
⋮----
// Look up the actual Model object from providerModels
⋮----
getProviderParameters(provider: ProviderProfile, modelId: string, reasoning?: Reasoning): Record<string, unknown>
⋮----
// Look up the actual Model object from providerModels
⋮----
/**
   * Returns provider-specific tools for the given provider and model
   */
async getProviderTools(provider: ProviderProfile, modelId: string): Promise<ToolSet>
⋮----
// Resolve Model object
⋮----
/**
   * Creates a voice session if supported by the provider
   */
async createVoiceSession(provider: ProviderProfile): Promise<VoiceSession>
⋮----
/**
   * Normalizes messages for provider-specific requirements
   */
normalizeMessages(provider: ProviderProfile, model: string | Model, messages: ModelMessage[]): ModelMessage[]
⋮----
// Resolve Model object
⋮----
/**
   * Determines if an error is retryable for the given provider and model
   * Defaults to true (retryable) if the provider doesn't implement isRetryable
   */
isRetryable(provider: ProviderProfile, _modelId: string, error: unknown): boolean
⋮----
// Default to retryable if provider doesn't implement this method
⋮----
registerExtensionProviders(providers: RegisteredProvider[]): void
⋮----
unregisterExtensionProviders(extensionId: string): void
⋮----
getProviders(): ProviderProfile[]
⋮----
getExtensionProviderProfiles(): ProviderProfile[]

================
File: src/main/models/types.ts
================
import { Model, ModelInfo, ProviderProfile, Reasoning, SettingsData, TlsPolicyRegistrar, UsageReportData, VoiceSession } from '@common/types';
import { LlmProvider } from '@common/agent';
⋮----
import type { SharedV4ProviderOptions } from '@ai-sdk/provider';
import type { LanguageModel, LanguageModelUsage, ModelMessage, ToolSet } from 'ai';
⋮----
import { Task } from '@/task';
⋮----
export interface CacheControl {
  providerOptions: SharedV4ProviderOptions;
  placement?: 'message' | 'message-part';
}
⋮----
export interface AiderModelMapping {
  modelName: string;
  environmentVariables: Record<string, string>;
}
⋮----
/**
 * Complete strategy interface for LLM providers
 * Encapsulates all provider-specific functionality including:
 * - LLM creation and usage tracking
 * - Model discovery and configuration
 * - Cost calculation and usage reporting
 * - Environment variable detection
 * - Aider integration
 */
export interface LoadModelsResponse {
  models: Model[];
  success: boolean;
  error?: string;
}
⋮----
export interface LlmProviderStrategy {
  // === LLM Creation and Usage Functions ===
  /**
   * Creates a LanguageModel instance for the given provider and model
   * Each provider is responsible for loading its own credentials using getEffectiveEnvironmentVariable
   */
  createLlm: (
    profile: ProviderProfile,
    model: Model,
    settings: SettingsData,
    projectDir: string,
    toolSet?: ToolSet,
    systemPrompt?: string,
    providerMetadata?: unknown,
    tlsRegistrar?: TlsPolicyRegistrar,
    sessionId?: string,
  ) => LanguageModel | Promise<LanguageModel>;

  /**
   * Generates usage reports with provider-specific metadata and calculates cost internally
   */
  getUsageReport: (task: Task, provider: ProviderProfile, model: Model, usage: LanguageModelUsage, providerMetadata?: unknown) => UsageReportData;

  // === Model Discovery and Configuration Functions ===
  /**
   * Loads available models from the provider's API
   */
  loadModels: (profile: ProviderProfile, settings: SettingsData, tlsRegistrar?: TlsPolicyRegistrar) => Promise<LoadModelsResponse>;

  /**
   * Checks if required environment variables are available
   */
  hasEnvVars: (settings: SettingsData) => boolean;

  /**
   * Generates Aider-compatible model mapping with environment variables
   */
  getAiderMapping: (provider: ProviderProfile, modelId: string, settings: SettingsData, projectDir: string) => AiderModelMapping;

  // === Optional Configuration Helper Functions ===
  /**
   * Returns provider-specific cache control configuration
   */
  getCacheControl?: (provider: LlmProvider, model: Model) => CacheControl | undefined;

  /**
   * Returns provider-specific options for model instantiation.
   * When `reasoning` is set (not undefined or 'provider-default'), the implementation
   * should omit reasoning-effort/budget fields so the top-level AI SDK `reasoning`
   * parameter takes effect. When `reasoning` is 'none', implementations should
   * explicitly disable thinking where the provider supports an on/off toggle.
   * See https://ai-sdk.dev/docs/ai-sdk-core/reasoning for precedence rules.
   */
  getProviderOptions?: (provider: LlmProvider, model: Model, reasoning?: Reasoning) => SharedV4ProviderOptions | undefined;

  /**
   * Returns provider-specific tools that should be available to the agent
   */
  getProviderTools?: (provider: LlmProvider, model: Model) => ToolSet | Promise<ToolSet>;

  /**
   * Returns provider-specific parameters for the given model and effective reasoning override
   */
  getProviderParameters?: (provider: LlmProvider, model: Model, reasoning?: Reasoning) => Record<string, unknown>;

  /**
   * Returns model info for a specific model ID
   */
  getModelInfo?: (provider: ProviderProfile, modelId: string, allModelInfos: Record<string, ModelInfo>) => ModelInfo | undefined;

  /**
   * Creates a voice session configuration if supported
   */
  createVoiceSession?: (profile: ProviderProfile, settings: SettingsData) => Promise<VoiceSession>;

  /**
   * Normalizes messages for provider-specific requirements
   */
  normalizeMessages?: (provider: LlmProvider, model: Model, messages: ModelMessage[]) => ModelMessage[];

  /**
   * Determines if an error is retryable for this provider
   * Returns false for non-retryable errors (e.g., auth issues, invalid requests)
   * Returns true for transient errors that may succeed on retry
   */
  isRetryable?: (error: unknown) => boolean;
}
⋮----
// === LLM Creation and Usage Functions ===
/**
   * Creates a LanguageModel instance for the given provider and model
   * Each provider is responsible for loading its own credentials using getEffectiveEnvironmentVariable
   */
⋮----
/**
   * Generates usage reports with provider-specific metadata and calculates cost internally
   */
⋮----
// === Model Discovery and Configuration Functions ===
/**
   * Loads available models from the provider's API
   */
⋮----
/**
   * Checks if required environment variables are available
   */
⋮----
/**
   * Generates Aider-compatible model mapping with environment variables
   */
⋮----
// === Optional Configuration Helper Functions ===
/**
   * Returns provider-specific cache control configuration
   */
⋮----
/**
   * Returns provider-specific options for model instantiation.
   * When `reasoning` is set (not undefined or 'provider-default'), the implementation
   * should omit reasoning-effort/budget fields so the top-level AI SDK `reasoning`
   * parameter takes effect. When `reasoning` is 'none', implementations should
   * explicitly disable thinking where the provider supports an on/off toggle.
   * See https://ai-sdk.dev/docs/ai-sdk-core/reasoning for precedence rules.
   */
⋮----
/**
   * Returns provider-specific tools that should be available to the agent
   */
⋮----
/**
   * Returns provider-specific parameters for the given model and effective reasoning override
   */
⋮----
/**
   * Returns model info for a specific model ID
   */
⋮----
/**
   * Creates a voice session configuration if supported
   */
⋮----
/**
   * Normalizes messages for provider-specific requirements
   */
⋮----
/**
   * Determines if an error is retryable for this provider
   * Returns false for non-retryable errors (e.g., auth issues, invalid requests)
   * Returns true for transient errors that may succeed on retry
   */
⋮----
export type LlmProviderRegistry = Record<string, LlmProviderStrategy>;

================
File: src/main/models/utils.ts
================
import type { TlsPolicyOptions, TlsPolicyRegistrar } from '@common/types';
⋮----
export const syncProviderTlsRule = (registrar: TlsPolicyRegistrar | undefined, baseUrl: string | undefined, sslVerify?: boolean, caCertPath?: string): void =>

================
File: src/main/project/index.ts
================


================
File: src/main/project/migrations.ts
================
import fs from 'fs/promises';
import path from 'path';
⋮----
import { v4 as uuidv4 } from 'uuid';
import { ContextFile, ContextMessage, TaskContext, TaskData } from '@common/types';
import { fileExists } from '@common/utils';
import { DEFAULT_AIDER_MAIN_MODEL } from '@common/agent';
⋮----
import { AIDER_DESK_DIR, AIDER_DESK_TASKS_DIR } from '@/constants';
import logger from '@/logger';
import { Project } from '@/project';
⋮----
interface SessionData {
  version: number;
  contextMessages: ContextMessage[];
  contextFiles: ContextFile[];
}
⋮----
/**
 * @deprecated this will be removed in 0.40.0
 *
 * Migrates legacy sessions to the new task system.
 *
 * This function:
 * 1. Checks if sessions directory exists
 * 2. Reads all session JSON files
 * 3. Creates tasks for each session
 * 4. Deletes the sessions directory after successful migration
 */
export const migrateSessionsToTasks = async (project: Project): Promise<void> =>
⋮----
// Check if sessions directory exists
⋮----
// Ensure tasks directory exists
⋮----
// Read all files in sessions directory
⋮----
// Process each session file
⋮----
// Delete the entire sessions directory after successful migration
⋮----
/**
 * Migrates a single session file to a task.
 */
const migrateSessionFile = async (project: Project, sessionsDirPath: string, tasksDirPath: string, sessionFile: string): Promise<void> =>
⋮----
// Read session data
⋮----
// Determine taskId and task folder name
⋮----
// Create task directory
⋮----
// Extract name from session file (without .json extension)
⋮----
// Write settings.json file
⋮----
// Create TaskContext by merging session data with required properties
⋮----
// Merge any other properties from session data, but don't override required ones
⋮----
// Write context.json file

================
File: src/main/project/project-manager.ts
================
import { normalizeBaseDir } from '@common/utils';
import { AgentProfile, ProjectSettings, SettingsData, ModeDefinition } from '@common/types';
⋮----
import { TelemetryManager } from '@/telemetry';
import { AgentProfileManager, McpConfigManager, McpManager } from '@/agent';
import { DataManager } from '@/data-manager';
import logger from '@/logger';
import { Project } from '@/project';
import { Store } from '@/store';
import { EventManager } from '@/events';
import { ModelManager } from '@/models';
import { GitManager } from '@/git';
import { MemoryManager } from '@/memory/memory-manager';
import { PromptsManager } from '@/prompts';
import { ExtensionManager } from '@/extensions/extension-manager';
import { PythonDependenciesInstaller } from '@/python-dependencies-installer';
⋮----
export class ProjectManager
⋮----
constructor(
    private readonly store: Store,
    private readonly mcpManager: McpManager,
    private readonly mcpConfigManager: McpConfigManager,
    private readonly telemetryManager: TelemetryManager,
    private readonly dataManager: DataManager,
    private readonly eventManager: EventManager,
    private readonly modelManager: ModelManager,
    gitManager: GitManager,
    private readonly agentProfileManager: AgentProfileManager,
    private readonly memoryManager: MemoryManager,
    private readonly promptsManager: PromptsManager,
    private readonly extensionManager: ExtensionManager,
    private readonly pythonInstaller: PythonDependenciesInstaller,
)
⋮----
private findProject(baseDir: string): Project | undefined
⋮----
private createProject(baseDir: string)
⋮----
public getProject(baseDir: string)
⋮----
public getOpenProject(baseDir: string): Project | undefined
⋮----
public async startProject(baseDir: string)
⋮----
public async closeProject(baseDir: string)
⋮----
public async restartProject(baseDir: string): Promise<void>
⋮----
public async close(): Promise<void>
⋮----
async settingsChanged(oldSettings: SettingsData, newSettings: SettingsData)
⋮----
modelsUpdated()
⋮----
projectSettingsChanged(baseDir: string, oldSettings: ProjectSettings, newSettings: ProjectSettings)
⋮----
public getCommands(baseDir: string)
⋮----
public getCustomModes(baseDir?: string): ModeDefinition[]
⋮----
public getProjects(): Project[]
⋮----
agentProfileUpdated(oldProfile: AgentProfile, profile: AgentProfile)

================
File: src/main/project/project.ts
================
import fs from 'fs/promises';
import path from 'path';
⋮----
import { AgentProfile, BranchInfo, CreateTaskParams, DefaultTaskState, ModeDefinition, ProjectSettings, SettingsData, TaskData } from '@common/types';
import { fileExists } from '@common/utils';
import { v4 as uuidv4 } from 'uuid';
⋮----
import { AgentProfileManager, McpConfigManager, McpManager } from '@/agent';
import { Connector } from '@/connector';
import { DataManager } from '@/data-manager';
import logger from '@/logger';
import { Store } from '@/store';
import { ModelManager } from '@/models';
import { CustomCommandManager } from '@/custom-commands';
import { TelemetryManager } from '@/telemetry';
import { EventManager } from '@/events';
import { INTERNAL_TASK_ID, Task } from '@/task';
import { migrateSessionsToTasks } from '@/project/migrations';
import { GitManager } from '@/git';
import { MemoryManager } from '@/memory/memory-manager';
import { PromptsManager } from '@/prompts';
import { ExtensionManager } from '@/extensions/extension-manager';
import { AIDER_DESK_TASKS_DIR, AIDER_DESK_WATCH_FILES_LOCK } from '@/constants';
import { PythonDependenciesInstaller } from '@/python-dependencies-installer';
import { determineMainModel, determineWeakModel } from '@/utils';
import { FileWatcherManager } from '@/file-watcher-manager';
⋮----
export class Project
⋮----
constructor(
    public readonly baseDir: string,
    private readonly store: Store,
    private readonly mcpManager: McpManager,
    private readonly mcpConfigManager: McpConfigManager,
    private readonly telemetryManager: TelemetryManager,
    private readonly dataManager: DataManager,
    private readonly eventManager: EventManager,
    private readonly modelManager: ModelManager,
    private readonly gitManager: GitManager,
    private readonly agentProfileManager: AgentProfileManager,
    private readonly memoryManager: MemoryManager,
    private readonly promptsManager: PromptsManager,
    private readonly extensionManager: ExtensionManager,
    private readonly pythonInstaller: PythonDependenciesInstaller,
)
⋮----
public isStarted(): boolean
⋮----
public start(): Promise<void>
⋮----
private async prepareInternalTask()
⋮----
public async createNewTask(params?: CreateTaskParams)
⋮----
// when a subtask for non-saved task is created we need to save the parent task first
⋮----
// Allow extensions to modify initial task data before task is created
⋮----
// Apply modifications from extension
⋮----
// Local mode must never carry a worktree (e.g. inherited from parent while workingMode was overridden to local)
⋮----
// adding files from internal task that keeps track of files to new task
⋮----
private generateShortTaskId(): string
⋮----
private async prepareTask(taskId: string = this.generateShortTaskId(), initialTaskData?: Partial<TaskData>)
⋮----
// Allow extensions to modify task data after preparation
⋮----
// Apply modifications from extension
⋮----
private async getTaskIdsFromDisk(): Promise<string[]>
⋮----
private async loadTasks()
⋮----
// Migrate sessions to tasks before starting
⋮----
public getMostRecentTask()
⋮----
public getTask(taskId: string = INTERNAL_TASK_ID)
⋮----
public getInternalTask()
⋮----
public getProjectSettings()
⋮----
public addConnector(connector: Connector)
⋮----
// Set input history file if provided by the connector
⋮----
public removeConnector(connector: Connector)
⋮----
public async loadInputHistory(): Promise<string[]>
⋮----
public async addToInputHistory(message: string)
⋮----
private async sendInputHistoryUpdatedEvent()
⋮----
public getCustomModes(): ModeDefinition[]
⋮----
public getCustomCommandManager(): CustomCommandManager
⋮----
public getAgentProfiles(): AgentProfile[]
⋮----
public resolveAgentProfile(id: string): AgentProfile | null
⋮----
/**
   * Checks if any other task (excluding the specified taskId) uses the given worktree path.
   */
public isWorktreeSharedWithOtherTasks(worktreePath: string, excludeTaskId: string): boolean
⋮----
private async removeTaskWorktree(taskId: string, taskData: TaskData | undefined): Promise<void>
⋮----
private async deleteTaskInternal(taskId: string): Promise<void>
⋮----
public async deleteTask(taskId: string): Promise<void>
⋮----
// First, find and delete all subtasks recursively
⋮----
const collectDescendantIds = (parentId: string, visited: Set<string>): string[]
⋮----
// Then delete the parent task
⋮----
/**
   * Validates that a task can be assigned the given parent: the parent must exist
   * and must not be the task itself or one of its descendants (which would create a cycle).
   */
public validateParentAssignment(taskId: string, parentId: string): void
⋮----
const isDescendantOf = (candidateId: string, ancestorId: string): boolean =>
⋮----
public async duplicateTask(taskId: string): Promise<TaskData>
⋮----
// When the source task has a worktree, make the duplicate a subtask
// so both tasks sharing the same worktree are clearly related
⋮----
public async forkTask(taskId: string, messageId: string): Promise<TaskData>
⋮----
async getTasks(): Promise<TaskData[]>
⋮----
async reloadTasks(): Promise<TaskData[]>
⋮----
private async reloadTasksInternal(): Promise<TaskData[]>
⋮----
forEachTask(callback: (task: Task) => void, initializedOnly = true)
⋮----
async settingsChanged(oldSettings: SettingsData, newSettings: SettingsData)
⋮----
public getFileWatcherManager(): FileWatcherManager
⋮----
async projectSettingsChanged(oldSettings: ProjectSettings, newSettings: ProjectSettings)
⋮----
public listBranches(): Promise<BranchInfo[]>
⋮----
async close()
⋮----
// Remove watch-files lock file if it exists
⋮----
// Ignore error if file doesn't exist

================
File: src/main/prompts/helpers/conditional.ts
================
import Handlebars from 'handlebars';
import { ToolApprovalState } from '@common/types';
⋮----
export const registerConditionalHelpers = (): void =>

================
File: src/main/prompts/helpers/file.ts
================
import fs from 'fs';
import path from 'path';
⋮----
import Handlebars from 'handlebars';
⋮----
export const registerFileHelpers = (): void =>

================
File: src/main/prompts/helpers/formatting.ts
================
import Handlebars from 'handlebars';
⋮----
export const registerFormattingHelpers = (): void =>

================
File: src/main/prompts/helpers/index.ts
================
import { registerConditionalHelpers } from './conditional';
import { registerFileHelpers } from './file';
import { registerFormattingHelpers } from './formatting';
⋮----
import logger from '@/logger';
⋮----
export const registerAllHelpers = (): void =>

================
File: src/main/prompts/index.ts
================


================
File: src/main/prompts/prompts-manager.ts
================
import fs from 'fs/promises';
import path from 'path';
⋮----
import Handlebars from 'handlebars';
import { FSWatcher, watch } from 'chokidar';
import debounce from 'lodash/debounce';
import { AgentProfile, AutonomyMode, DEFAULT_AUTONOMY_MODE, ConflictResolutionFileContext, SettingsData, ToolApprovalState } from '@common/types';
import {
  AIDER_TOOL_ADD_CONTEXT_FILES,
  AIDER_TOOL_DROP_CONTEXT_FILES,
  AIDER_TOOL_GET_CONTEXT_FILES,
  AIDER_TOOL_GROUP_NAME,
  AIDER_TOOL_RUN_PROMPT,
  HELPERS_TOOL_GROUP_NAME,
  MEMORY_TOOL_DELETE,
  MEMORY_TOOL_GROUP_NAME,
  MEMORY_TOOL_LIST,
  MEMORY_TOOL_RETRIEVE,
  MEMORY_TOOL_STORE,
  POWER_TOOL_BASH,
  POWER_TOOL_FILE_EDIT,
  POWER_TOOL_FILE_READ,
  POWER_TOOL_FILE_WRITE,
  POWER_TOOL_GLOB,
  POWER_TOOL_GREP,
  POWER_TOOL_GROUP_NAME,
  POWER_TOOL_SEMANTIC_SEARCH,
  SKILLS_TOOL_ACTIVATE_SKILL,
  SKILLS_TOOL_GROUP_NAME,
  SUBAGENTS_TOOL_GROUP_NAME,
  SUBAGENTS_TOOL_RUN_TASK,
  TODO_TOOL_CLEAR_ITEMS,
  TODO_TOOL_GET_ITEMS,
  TODO_TOOL_GROUP_NAME,
  TODO_TOOL_SET_ITEMS,
  TODO_TOOL_UPDATE_ITEM_COMPLETION,
  TOOL_GROUP_NAME_SEPARATOR,
} from '@common/tools';
⋮----
import { registerAllHelpers } from './helpers';
import {
  CodeChangeRequestsPromptData,
  CommitMessagePromptData,
  CompactConversationPromptData,
  ConflictResolutionPromptData,
  ConflictResolutionSystemPromptData,
  GitErrorResolutionPromptData,
  GitErrorResolutionSystemPromptData,
  HandoffPromptData,
  InitProjectPromptData,
  PromptTemplateData,
  PromptTemplateName,
  TaskNamePromptData,
  ToolPermissions,
  UpdateTaskStateData,
} from './types';
⋮----
import type { ExtensionManager } from '@/extensions/extension-manager';
import type { Store } from '@/store';
⋮----
import { AIDER_DESK_BUILTIN_PROMPTS_DIR, AIDER_DESK_GLOBAL_PROMPTS_DIR, AIDER_DESK_PROMPTS_DIR } from '@/constants';
import logger from '@/logger';
import { Task } from '@/task';
import { shouldUsePolling } from '@/utils/file-watch';
⋮----
export class PromptsManager
⋮----
constructor(
    private readonly extensionManager: ExtensionManager,
    private readonly store: Store,
    private readonly builtinTemplatesDir = AIDER_DESK_BUILTIN_PROMPTS_DIR,
    private readonly globalPromptsDir = AIDER_DESK_GLOBAL_PROMPTS_DIR,
)
⋮----
public async init(): Promise<void>
⋮----
private async compileGlobalTemplates(): Promise<void>
⋮----
private async compileProjectTemplates(projectDir: string): Promise<void>
⋮----
private getTemplateNames(): PromptTemplateName[]
⋮----
private async loadGlobalTemplateSource(name: PromptTemplateName): Promise<string | null>
⋮----
// Check global prompts
⋮----
// Fall back to default templates from resources
⋮----
private async loadProjectTemplateSource(projectPromptsDir: string, name: PromptTemplateName): Promise<string | null>
⋮----
// It's fine if project template doesn't exist
⋮----
private async setupGlobalWatcher(): Promise<void>
⋮----
public async watchProject(projectDir: string): Promise<void>
⋮----
public async unwatchProject(projectDir: string): Promise<void>
⋮----
public async dispose(): Promise<void>
⋮----
async settingsChanged(oldSettings: SettingsData, newSettings: SettingsData): Promise<void>
⋮----
private async render(name: PromptTemplateName, data: unknown, projectDir: string, task?: Task): Promise<string>
⋮----
// Dispatch onPromptTemplate event to allow extensions to override the prompt
⋮----
const isAllowed = (tool: string)
⋮----
anyEnabled: false, // Will be set below
⋮----
workflow: '', // Placeholder

================
File: src/main/prompts/types.ts
================
import { AutonomyMode, ContextFile } from '@common/types';
⋮----
export interface ToolPermissions {
  aiderTools: boolean;
  powerTools: {
    semanticSearch: boolean;
    fileRead: boolean;
    fileWrite: boolean;
    fileEdit: boolean;
    glob: boolean;
    grep: boolean;
    bash: boolean;
    anyEnabled: boolean;
  };
  todoTools: boolean;
  subagents: boolean;
  memory: {
    enabled: boolean;
    retrieveAllowed: boolean;
    storeAllowed: boolean;
    listAllowed: boolean;
    deleteAllowed: boolean;
  };
  skills: {
    allowed: boolean;
  };
  autonomyMode: AutonomyMode;
}
⋮----
export interface PromptTemplateData {
  projectDir: string;
  taskDir: string;
  additionalInstructions?: string;
  osName: string;
  currentDate: string;
  rulesFiles: string;
  customInstructions: string;
  toolPermissions: ToolPermissions;
  toolConstants: Record<string, string>;
  workflow: string;
  projectGitRootDirectory?: string;
}
⋮----
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
export interface InitProjectPromptData {
  // Static for now
}
⋮----
// Static for now
⋮----
export interface CompactConversationPromptData {
  customInstructions?: string;
}
⋮----
export interface HandoffPromptData {
  focus?: string;
  contextFiles?: ContextFile[];
}
⋮----
export interface ChangeRequestItem {
  filename: string;
  lineNumber: number;
  fileExtension: string;
  contextLines: { lineNumber: number; content: string }[];
  userComment: string;
}
⋮----
export interface CodeChangeRequestsPromptData {
  requests: ChangeRequestItem[];
}
⋮----
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
export interface CommitMessagePromptData {
  // Static for now
}
⋮----
// Static for now
⋮----
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
export interface TaskNamePromptData {
  // Static for now
}
⋮----
// Static for now
⋮----
export interface ConflictResolutionSystemPromptData {
  POWER_TOOL_GROUP_NAME: string;
  TOOL_GROUP_NAME_SEPARATOR: string;
  POWER_TOOL_FILE_EDIT: string;
}
⋮----
export interface ConflictResolutionPromptData {
  filePath: string;
  basePath?: string;
  oursPath?: string;
  theirsPath?: string;
}
⋮----
export interface GitErrorResolutionSystemPromptData {
  POWER_TOOL_GROUP_NAME: string;
  TOOL_GROUP_NAME_SEPARATOR: string;
  POWER_TOOL_BASH: string;
  POWER_TOOL_FILE_READ: string;
  POWER_TOOL_GREP: string;
}
⋮----
export interface GitErrorResolutionPromptData {
  action: string;
  error: string;
}
⋮----
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
export interface UpdateTaskStateData {
  // Static for now
}
⋮----
// Static for now
⋮----
export type PromptTemplateName =
  | 'system-prompt'
  | 'init-project'
  | 'workflow'
  | 'compact-conversation'
  | 'commit-message'
  | 'task-name'
  | 'conflict-resolution'
  | 'conflict-resolution-system'
  | 'git-error-resolution'
  | 'git-error-resolution-system'
  | 'update-task-state'
  | 'handoff'
  | 'code-change-requests';

================
File: src/main/server/rest-api/agent-api.ts
================
import { Router } from 'express';
import { z } from 'zod';
⋮----
import { BaseApi } from './base-api';
⋮----
import { EventsHandler } from '@/events-handler';
⋮----
profile: z.any(), // AgentProfile schema
⋮----
profile: z.any(), // AgentProfile schema
⋮----
agentProfiles: z.array(z.any()), // AgentProfile array
⋮----
export class AgentApi extends BaseApi
⋮----
constructor(private eventsHandler: EventsHandler)
⋮----
registerRoutes(router: Router): void
⋮----
// GET /agent-profiles
⋮----
// POST /agent-profile/create
⋮----
// POST /agent-profile/update
⋮----
// POST /agent-profile/delete
⋮----
// POST /agent-profiles/order

================
File: src/main/server/rest-api/base-api.ts
================
import { Request, Response, Router } from 'express';
import { z } from 'zod';
⋮----
import { Project } from '@/project';
⋮----
export abstract class BaseApi
⋮----
protected constructor()
⋮----
/**
   * Finds a project by directory path and validates it exists and is started
   * Returns null if project not found or not started, and sends appropriate error response
   */
protected findProject(project: Project | undefined, projectDir: string, res: Response): Project | null
⋮----
/**
   * Generic request handler wrapper with consistent error handling
   */
protected handleRequest<T>(handler: (req: Request, res: Response) => Promise<T>)
⋮----
// eslint-disable-next-line no-console
⋮----
/**
   * Validates request data using a Zod schema and handles invalid requests
   */
protected validateRequest<T>(schema: z.ZodSchema<T>, data: unknown, res: Response): T | null
⋮----
/**
   * Abstract method for registering routes - each API module must implement this
   */
abstract registerRoutes(router: Router): void;

================
File: src/main/server/rest-api/commands-api.ts
================
import { Router } from 'express';
import { z } from 'zod';
⋮----
import { BaseApi } from './base-api';
⋮----
import { EventsHandler } from '@/events-handler';
⋮----
export class CommandsApi extends BaseApi
⋮----
constructor(private readonly eventsHandler: EventsHandler)
⋮----
registerRoutes(router: Router): void
⋮----
// Get commands
⋮----
// Run custom command

================
File: src/main/server/rest-api/context-api.ts
================
import { Router } from 'express';
import { z } from 'zod';
⋮----
import { BaseApi } from './base-api';
⋮----
import { EventsHandler } from '@/events-handler';
import { ProjectManager } from '@/project';
⋮----
export class ContextApi extends BaseApi
⋮----
constructor(
    private readonly projectManager: ProjectManager,
    private readonly eventsHandler: EventsHandler,
)
⋮----
registerRoutes(router: Router): void
⋮----
// Add context file
⋮----
// Drop context file
⋮----
// Get context files
⋮----
// Get addable files

================
File: src/main/server/rest-api/extensions-api.ts
================
import { Router } from 'express';
import { z } from 'zod';
⋮----
import { BaseApi } from './base-api';
⋮----
import type { EventsHandler } from '@/events-handler';
⋮----
repositories: z.string().optional(), // Comma-separated URLs
⋮----
export class ExtensionsApi extends BaseApi
⋮----
constructor(private readonly eventsHandler: EventsHandler)
⋮----
registerRoutes(router: Router): void
⋮----
// Get installed extensions
⋮----
// Get extension tools info
⋮----
// Get available extensions from repositories
⋮----
// Install extension
⋮----
// Uninstall extension
⋮----
// Update extension
⋮----
// Reload extension
⋮----
// Get extension UI components
⋮----
// Get extension UI component data
⋮----
// Execute extension UI action
⋮----
// Load extension library (resolve from esm.sh with disk cache)
⋮----
// Get extension config component (per-extension settings)
⋮----
// Get extension config data
⋮----
// Save extension config

================
File: src/main/server/rest-api/index.ts
================


================
File: src/main/server/rest-api/mcp-api.ts
================
import { Router } from 'express';
import { z } from 'zod';
⋮----
import { BaseApi } from './base-api';
⋮----
import { EventsHandler } from '@/events-handler';
⋮----
export class McpApi extends BaseApi
⋮----
constructor(private readonly eventsHandler: EventsHandler)
⋮----
registerRoutes(router: Router): void
⋮----
// Load MCP server tools
⋮----
// Reload MCP servers
⋮----
// Reload single MCP server
⋮----
// GET /mcp/servers
⋮----
// POST /mcp/server/add
⋮----
// POST /mcp/server/update
⋮----
// POST /mcp/server/remove
⋮----
// POST /mcp/servers/replace

================
File: src/main/server/rest-api/memory-api.ts
================
import { Router } from 'express';
import { z } from 'zod';
⋮----
import { BaseApi } from './base-api';
⋮----
import { EventsHandler } from '@/events-handler';
⋮----
projectId: z.string().min(1).optional(), // legacy field name, kept for backward compatibility
⋮----
export class MemoryApi extends BaseApi
⋮----
constructor(private readonly eventsHandler: EventsHandler)
⋮----
registerRoutes(router: Router): void

================
File: src/main/server/rest-api/project-api.ts
================
import { Router } from 'express';
import { z } from 'zod';
import { CreateTaskParams, ProjectSettingsSchema, TaskDataSchema, ModeDefinition, QueuedPromptData } from '@common/types';
⋮----
import { BaseApi } from './base-api';
⋮----
import { EventsHandler } from '@/events-handler';
⋮----
// Git branch operations
⋮----
export class ProjectApi extends BaseApi
⋮----
constructor(private readonly eventsHandler: EventsHandler)
⋮----
registerRoutes(router: Router): void
⋮----
// Get projects
⋮----
// Get input history
⋮----
// Redo user prompt
⋮----
// Resume task
⋮----
// Validate path
⋮----
// Is project path
⋮----
// Clone project from repository
⋮----
// Cancel active project clone
⋮----
// Get file path suggestions
⋮----
// Paste image
⋮----
// Apply edits
⋮----
// Run command
⋮----
// Init project rules file
⋮----
// Create new task
⋮----
// Save task
⋮----
// Load task messages
⋮----
// List tasks
⋮----
// Reload tasks from disk
⋮----
// Delete session
⋮----
// Duplicate task
⋮----
// Fork task from message
⋮----
// Reset task
⋮----
// Load task data
⋮----
// Export session to markdown
⋮----
// Remove last message
⋮----
// Remove message by ID
⋮----
// Remove messages up to specified message ID
⋮----
// Compact conversation
⋮----
// Smart compact conversation
⋮----
// Undo context change
⋮----
// Handoff conversation
⋮----
// Run code change requests
⋮----
// Scrape web
⋮----
// Merge worktree to main
⋮----
// Switch to local working mode
⋮----
// Switch to worktree working mode
⋮----
// Get local uncommitted files
⋮----
// Apply uncommitted changes
⋮----
// Revert last merge
⋮----
// Restore file
⋮----
// Read file
⋮----
// Save file
⋮----
// Generate commit message
⋮----
// Commit changes
⋮----
// Cancel commit changes
⋮----
// List branches
⋮----
// Git branch operations
// List git branches
⋮----
// Get outgoing and incoming commits relative to the target branch or upstream
⋮----
// Create git branch
⋮----
// Checkout git branch
⋮----
// Delete git branch
⋮----
// Merge branch into current branch
⋮----
// Rebase current branch onto another branch
⋮----
// Update branch from remote
⋮----
// Git pull
⋮----
// Check whether the project has a git repository
⋮----
// Initialize a git repository for the project
⋮----
// Git push
⋮----
// Task git status (current branch, uncommitted files, rebase state, worktree integration)
⋮----
// Deprecated: use /project/git/status. Returns the legacy flat WorktreeIntegrationStatus shape,
// or null when the task has no materialized worktree (matches the documented pre-deprecation contract).
⋮----
// Rebase worktree from branch
⋮----
// Abort rebase (worktree or local branch)
⋮----
// Continue rebase (worktree or local branch)
⋮----
// Resolve conflicts with agent (worktree or local branch)
⋮----
// Deprecated: use /project/git/abort-rebase
⋮----
// Deprecated: use /project/git/continue-rebase
⋮----
// Deprecated: use /project/git/resolve-conflicts-with-agent
⋮----
// Resolve git error with agent
⋮----
// Rename branch (local or worktree)
⋮----
// Rename worktree branch (deprecated alias for /project/git/branch/rename)
⋮----
// Update open projects order
⋮----
// Remove open project
⋮----
// Set active project
⋮----
// Restart project
⋮----
// Get project settings
⋮----
// Update project settings
⋮----
// Get custom modes
⋮----
// Interrupt project
⋮----
// Clear project context
⋮----
// Answer project question
⋮----
// Remove queued prompt
⋮----
// Send queued prompt immediately
⋮----
// Reorder queued prompts
⋮----
// Edit queued prompt
⋮----
// Update main model
⋮----
// Update weak model
⋮----
// Update architect model
⋮----
// Update edit formats
⋮----
// Start project
⋮----
// Stop project
⋮----
// Add open project

================
File: src/main/server/rest-api/prompt-api.ts
================
import { Request, Response, Router } from 'express';
import { z } from 'zod';
import { v4 as uuidv4 } from 'uuid';
import { AutonomyMode, CreateTaskParams } from '@common/types';
⋮----
import { BaseApi } from './base-api';
⋮----
import { EventsHandler } from '@/events-handler';
import logger from '@/logger';
⋮----
const parseTaskParams = (model?: string, agentProfileId?: string, autonomyMode?: AutonomyMode): CreateTaskParams | undefined =>
⋮----
export class PromptApi extends BaseApi
⋮----
constructor(private readonly eventsHandler: EventsHandler)
⋮----
registerRoutes(router: Router): void
⋮----
private async handleRunPromptJSON(
    res: Response,
    projectDir: string,
    prompt: string,
    taskId?: string,
    mode?: string,
    images?: string[],
    model?: string,
    agentProfileId?: string,
    autonomyMode?: AutonomyMode,
): Promise<void>
⋮----
private async handleRunPromptSSE(
    res: Response,
    projectDir: string,
    prompt: string,
    taskId?: string,
    mode?: string,
    images?: string[],
    model?: string,
    agentProfileId?: string,
    autonomyMode?: AutonomyMode,
): Promise<void>
⋮----
// Set SSE headers
⋮----
// Send initial comment to establish connection
⋮----
const listener = (event:
⋮----
const cleanup = () =>
⋮----
// already closed
⋮----
// Client disconnected
⋮----
// Client disconnected

================
File: src/main/server/rest-api/providers-api.ts
================
import { Router } from 'express';
import { z } from 'zod';
⋮----
import { BaseApi } from './base-api';
⋮----
import { EventsHandler } from '@/events-handler';
⋮----
const LlmProviderProfileSchema = z.any(); // Placeholder - can be refined based on LlmProviderProfile type
⋮----
model: z.any(), // Model object - will be validated at runtime
⋮----
export class ProvidersApi extends BaseApi
⋮----
constructor(private readonly eventsHandler: EventsHandler)
⋮----
registerRoutes(router: Router): void
⋮----
// Get providers
⋮----
// Update providers
⋮----
// Get provider models
⋮----
// Bulk update models
⋮----
// Upsert a single model
⋮----
// Delete a model

================
File: src/main/server/rest-api/readonly-api.ts
================
import path from 'path';
⋮----
import { BrowserBootstrap } from '@common/types';
import { Request, Response, Router } from 'express';
import { z } from 'zod';
⋮----
import { BaseApi } from '@/server/rest-api/base-api';
import { isReadonlyExtensionUiEnabled } from '@/server/readonly';
import { ProjectManager } from '@/project';
import { EventsHandler } from '@/events-handler';
import { Store } from '@/store';
import { READONLY_MODE } from '@/constants';
⋮----
export class ReadonlyApi extends BaseApi
⋮----
constructor(
    private readonly projectManager: ProjectManager,
    private readonly eventsHandler: EventsHandler,
    private readonly store: Store,
    private readonly isReady: () => boolean,
)
⋮----
private get isReadonlyMode(): boolean
⋮----
private resolveProject(projectDir: string, res: Response)
⋮----
private validateTask(projectDir: string, taskId: string | undefined, res: Response): boolean
⋮----
private findComponent(projectDir: string, extensionId: string, componentId: string, taskId: string | undefined)
⋮----
registerRoutes(router: Router): void

================
File: src/main/server/rest-api/settings-api.ts
================
import { Router } from 'express';
import { z } from 'zod';
⋮----
import { BaseApi } from './base-api';
⋮----
import { EventsHandler } from '@/events-handler';
⋮----
const SettingsDataSchema = z.any(); // Placeholder - can be refined based on SettingsData type
⋮----
export class SettingsApi extends BaseApi
⋮----
constructor(private readonly eventsHandler: EventsHandler)
⋮----
registerRoutes(router: Router): void
⋮----
// Get settings
⋮----
// Update settings
⋮----
// Get recent projects
⋮----
// Add recent project
⋮----
// Remove recent project
⋮----
// Set zoom level
⋮----
// Get versions
⋮----
// Download latest AiderDesk
⋮----
// Get release notes
⋮----
// Clear release notes
⋮----
// Get OS

================
File: src/main/server/rest-api/skills-api.ts
================
import { Router } from 'express';
import { z } from 'zod';
⋮----
import { BaseApi } from './base-api';
⋮----
import { EventsHandler } from '@/events-handler';
⋮----
export class SkillsApi extends BaseApi
⋮----
constructor(private readonly eventsHandler: EventsHandler)
⋮----
registerRoutes(router: Router): void

================
File: src/main/server/rest-api/system-api.ts
================
import { Router } from 'express';
import { z } from 'zod';
import { SystemLogLevel } from '@common/types';
⋮----
import { BaseApi } from './base-api';
⋮----
import { EventsHandler } from '@/events-handler';
import { PythonDependenciesInstaller } from '@/python-dependencies-installer';
⋮----
export class SystemApi extends BaseApi
⋮----
constructor(
    private readonly eventsHandler: EventsHandler,
    private readonly pythonInstaller: PythonDependenciesInstaller,
)
⋮----
registerRoutes(router: Router): void
⋮----
// Health check
⋮----
// Get system logs
⋮----
// Clear system logs
⋮----
// Get aider connector status
⋮----
// Respond to input prompt

================
File: src/main/server/rest-api/terminal-api.ts
================
import { Router } from 'express';
import { z } from 'zod';
⋮----
import { BaseApi } from './base-api';
⋮----
import { EventsHandler } from '@/events-handler';
⋮----
export class TerminalApi extends BaseApi
⋮----
constructor(private readonly eventsHandler: EventsHandler)
⋮----
registerRoutes(router: Router): void
⋮----
// Create terminal
⋮----
// Write to terminal
⋮----
// Resize terminal
⋮----
// Close terminal
⋮----
// Get recent terminal output buffer (for re-attaching clients)
⋮----
// Get terminal for task
⋮----
// Get all terminals for task

================
File: src/main/server/rest-api/todo-api.ts
================
import { Router } from 'express';
import { z } from 'zod';
⋮----
import { BaseApi } from './base-api';
⋮----
import { EventsHandler } from '@/events-handler';
⋮----
export class TodoApi extends BaseApi
⋮----
constructor(private readonly eventsHandler: EventsHandler)
⋮----
registerRoutes(router: Router): void
⋮----
// Get todos
⋮----
// Add todo
⋮----
// Update todo
⋮----
// Delete todo
⋮----
// Clear all todos

================
File: src/main/server/rest-api/usage-api.ts
================
import { Router } from 'express';
import { z } from 'zod';
⋮----
import { BaseApi } from './base-api';
⋮----
import { EventsHandler } from '@/events-handler';
⋮----
export class UsageApi extends BaseApi
⋮----
constructor(private readonly eventsHandler: EventsHandler)
⋮----
registerRoutes(router: Router): void

================
File: src/main/server/rest-api/voice-api.ts
================
import { Router } from 'express';
import { z } from 'zod';
⋮----
import { BaseApi } from './base-api';
⋮----
import { EventsHandler } from '@/events-handler';
⋮----
export class VoiceApi extends BaseApi
⋮----
constructor(private readonly eventsHandler: EventsHandler)
⋮----
registerRoutes(router: Router): void

================
File: src/main/server/cloudflare-tunnel-manager.ts
================
import { CloudflareTunnelStatus } from '@common/types';
import { Tunnel, use as setCloudflaredBinary } from 'cloudflared';
⋮----
import { CLOUDFLARED_BINARY_PATH, SERVER_PORT } from '@/constants';
import logger from '@/logger';
import { isDev } from '@/app';
⋮----
export class CloudflareTunnelManager
⋮----
async start(): Promise<void>
⋮----
// set cloudflared binary path only in production app
⋮----
// Wait for URL
⋮----
// Wait for connection
⋮----
// Keep listeners for ongoing events
⋮----
stop(): void
⋮----
getStatus(): CloudflareTunnelStatus

================
File: src/main/server/cors.ts
================
import { Store } from '@/store';
import { CORS_ALLOWED_ORIGINS } from '@/constants';
⋮----
export const createCorsOriginValidator =
(store: Store)

================
File: src/main/server/index.ts
================
// Export main controller

================
File: src/main/server/readonly.ts
================
import { READONLY_EXTENSION_UI } from '@/constants';
import { Store } from '@/store';
⋮----
export const isReadonlyExtensionUiEnabled = (store: Store): boolean =>

================
File: src/main/server/runner.ts
================
import { normalizeBaseDir } from '@common/utils';
import { ProjectData } from '@common/types';
⋮----
import logger from '@/logger';
import { initManagers } from '@/managers';
import { performStartUp } from '@/start-up';
import { Store } from '@/store';
import { AIDER_DESK_DATA_DIR, READONLY_MODE, SERVER_PORT } from '@/constants';
import { getDefaultProjectSettings } from '@/utils';
import { ModelManager } from '@/models';
import { AgentProfileManager } from '@/agent';
⋮----
export const addProjectsFromEnv = async (store: Store, modelManager: ModelManager, agentProfileManager: AgentProfileManager): Promise<void> =>
⋮----
// Create all projects from the env var
⋮----
// Override open projects with the ones from env var
⋮----
// Preserve active state from existing projects if they match
⋮----
// Set the first project as active if none are active
⋮----
const main = async (): Promise<void> =>
⋮----
// Force headless mode for node-runner
⋮----
const updateProgress = (
⋮----
// Initialize managers first (creates pythonInstaller)
⋮----
// Check for AIDER_DESK_PROJECTS environment variable and add projects
⋮----
// Only run main if this file is being executed directly (not imported for testing)

================
File: src/main/server/server-controller.ts
================
import { Server } from 'http';
import path from 'path';
⋮----
import express, { NextFunction, Request, Response } from 'express';
import cors from 'cors';
⋮----
import {
  AgentApi,
  CommandsApi,
  ContextApi,
  McpApi,
  ProjectApi,
  PromptApi,
  ProvidersApi,
  SettingsApi,
  SystemApi,
  TodoApi,
  UsageApi,
  MemoryApi,
  VoiceApi,
  TerminalApi,
  ExtensionsApi,
  SkillsApi,
  ReadonlyApi,
} from '@/server/rest-api';
import { AUTH_PASSWORD, AUTH_USERNAME, MCP_OAUTH_CALLBACK_PATH, READONLY_MODE, SERVER_PORT } from '@/constants';
import logger from '@/logger';
import { ProjectManager } from '@/project';
import { EventsHandler } from '@/events-handler';
import { Store } from '@/store';
import { isDev, isElectron } from '@/app';
import { PythonDependenciesInstaller } from '@/python-dependencies-installer';
import { createCorsOriginValidator } from '@/server/cors';
⋮----
const REQUEST_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
⋮----
export class ServerController
⋮----
private get isReadonlyMode(): boolean
⋮----
constructor(
    private readonly server: Server,
    private readonly projectManager: ProjectManager,
    private readonly eventsHandler: EventsHandler,
    private readonly store: Store,
    private readonly pythonInstaller: PythonDependenciesInstaller,
)
⋮----
private serverGuardMiddleware(req: Request, res: Response, next: NextFunction): void
⋮----
// Always allow health check and OAuth callback regardless of server.enabled setting
⋮----
// In headless mode, always allow requests regardless of server.enabled setting
⋮----
private timeoutMiddleware(req: Request, res: Response, next: NextFunction): void
⋮----
// Skip timeout for SSE responses (Accept: text/event-stream)
⋮----
private basicAuthMiddleware(req: Request, res: Response, next: NextFunction): void
⋮----
// Check if environment variables for auth are provided, which overrides settings
⋮----
private setupApiRoutes(): void
⋮----
// Create API router
⋮----
// Register all API modules
⋮----
// Mount the API router globally under /api
⋮----
private setupCors(): void
⋮----
private init()
⋮----
// Add server guard middleware as the first middleware
⋮----
// Set timeout for all requests
⋮----
// Add Basic Auth
⋮----
// Serve static renderer files in production (for browser access)
⋮----
// Handle SPA routing: serve index.html for non-API routes
⋮----
setReadonlyReady(): void
⋮----
async close()
⋮----
// No need to stop server anymore - it's always attached
⋮----
async startServer()
⋮----
async stopServer()

================
File: src/main/skills/skill-manager.ts
================
import fs from 'fs/promises';
import path from 'path';
⋮----
import { v4 as uuidv4 } from 'uuid';
import { loadFront } from 'yaml-front-matter';
import { SKILLS_TOOL_GROUP_NAME, SKILLS_TOOL_ACTIVATE_SKILL, TOOL_GROUP_NAME_SEPARATOR } from '@common/tools';
import { ContextAssistantMessage, ContextMessage, ContextToolMessage, SkillDefinition as Skill, SkillLocation } from '@common/types';
⋮----
import type { Project } from '@/project';
import type { Task } from '@/task';
⋮----
import { AIDER_DESK_BUILTIN_SKILLS_DIR, AIDER_DESK_DIR, AIDER_DESK_HOME_DIR } from '@/constants';
import { ExtensionManager } from '@/extensions/extension-manager';
⋮----
const parseBooleanFrontMatterField = (value: unknown, defaultValue: boolean): boolean =>
⋮----
const parseSkillFrontMatter = (markdown: string):
⋮----
const safeReadDir = async (dirPath: string): Promise<string[]> =>
⋮----
const safeStat = async (filePath: string): Promise<import('fs').Stats | null> =>
⋮----
const loadSkillsFromDir = async (skillsRootDir: string, location: SkillLocation): Promise<Skill[]> =>
⋮----
export class SkillManager
⋮----
constructor(project: Project, extensionManager?: ExtensionManager)
⋮----
async loadAllSkills(): Promise<Skill[]>
⋮----
async getSkills(contextMessages?: ContextMessage[]): Promise<Skill[]>
⋮----
async getEnabledSkills(): Promise<Skill[]>
⋮----
async getSkillContent(skillName: string): Promise<string | null>
⋮----
getActivatedSkillNames(contextMessages: ContextMessage[]): Set<string>
⋮----
buildActivateSkillMessages(skillName: string, content: string): [ContextAssistantMessage, ContextToolMessage]

================
File: src/main/store/migrations/v0-to-v1.ts
================
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const migrateSettingsV0toV1 = (settings: any): any =>
⋮----
// Create base providers array

================
File: src/main/store/migrations/v1-to-v2.ts
================
/* eslint-disable @typescript-eslint/no-explicit-any */
import { DEFAULT_SETTINGS } from '../store';
⋮----
export const migrateSettingsV1toV2 = (settings: any): any => (
⋮----
// @ts-expect-error the config has changed

================
File: src/main/store/migrations/v10-to-v11.ts
================
/* eslint-disable @typescript-eslint/no-explicit-any */
import { SettingsData } from '@common/types';
⋮----
export const migrateV10ToV11 = (settings: any): SettingsData =>
⋮----
// If the profile has a description, move it to the subagent
⋮----
// Remove description from the main profile
⋮----
// Move description to subagent

================
File: src/main/store/migrations/v11-to-v12.ts
================
/* eslint-disable @typescript-eslint/no-explicit-any */
import { ContextMemoryMode, SettingsData } from '@common/types';
⋮----
export const migrateV11ToV12 = (settings: any): SettingsData =>
⋮----
hasContextMemory: undefined, // Remove old field

================
File: src/main/store/migrations/v12-to-v13.ts
================
/* eslint-disable @typescript-eslint/no-explicit-any */
import { SettingsData } from '@common/types';
⋮----
export const migrateV12ToV13 = (settings: any): SettingsData =>

================
File: src/main/store/migrations/v13-to-v14.ts
================
/* eslint-disable @typescript-eslint/no-explicit-any */
import { ProviderProfile } from '@common/types';
import { LlmProvider, LlmProviderName } from '@common/agent';
⋮----
export const migrateProvidersV13toV14 = (settings:

================
File: src/main/store/migrations/v14-to-v15.ts
================
/* eslint-disable @typescript-eslint/no-explicit-any */
import { SettingsData } from '@common/types';
⋮----
export const migrateSettingsV14toV15 = (settings: any): SettingsData =>
⋮----
// Combine aiderPreferred and agentPreferred into preferredModels
// Add aiderPreferred first, then agentPreferred, removing duplicates
⋮----
// Remove the old models structure

================
File: src/main/store/migrations/v15-to-v16.ts
================
/* eslint-disable @typescript-eslint/no-explicit-any */
import { SettingsData } from '@common/types';
import { DEFAULT_AGENT_PROFILE } from '@common/agent';
⋮----
export const migrateSettingsV15toV16 = (settings: any): SettingsData =>
⋮----
// Ensure toolSettings from DEFAULT_AGENT_PROFILE are included in existing agent profiles

================
File: src/main/store/migrations/v16-to-v17.ts
================
/* eslint-disable @typescript-eslint/no-explicit-any */
import fs from 'fs/promises';
import path from 'path';
import { homedir } from 'os';
⋮----
import { SettingsData, AgentProfile } from '@common/types';
⋮----
import { AIDER_DESK_AGENTS_DIR } from '@/constants';
import logger from '@/logger';
import { deriveDirName } from '@/utils';
⋮----
const getExistingDirNames = async (agentsDir: string): Promise<Set<string>> =>
⋮----
// Directory doesn't exist or can't be read
⋮----
const saveProfileToFile = async (profile: any, filePath: string): Promise<void> =>
⋮----
export const migrateSettingsV16toV17 = async (settings: SettingsData): Promise<SettingsData> =>
⋮----
// Create global agents directory
⋮----
// Migrate existing agent profiles from settings to directory structure
⋮----
// Get existing directory names for collision handling
⋮----
// Derive unique directory name from profile name
⋮----
// Check if directory already exists (don't overwrite existing directories)
⋮----
// Directory doesn't exist, create it
⋮----
existingDirNames.add(dirName); // Add to set to avoid collisions in same loop
⋮----
// Create order.json file using profile IDs
⋮----
// keeping the agentProfiles in settings for now, just in case user needs to revert the version

================
File: src/main/store/migrations/v17-to-v18.ts
================
/* eslint-disable @typescript-eslint/no-explicit-any */
import { SettingsData } from '@common/types';
⋮----
export const migrateSettingsV17toV18 = (settings: any): SettingsData =>

================
File: src/main/store/migrations/v18-to-v19.ts
================
/* eslint-disable @typescript-eslint/no-explicit-any */
import { AIDER_DESK_EXTENSIONS_REPO_URL } from '@common/extensions';
import { SettingsData } from '@common/types';
⋮----
export const migrateSettingsV18toV19 = (settings: any): SettingsData =>

================
File: src/main/store/migrations/v19-to-v20.ts
================
/* eslint-disable @typescript-eslint/no-explicit-any */
import { SettingsData } from '@common/types';
⋮----
export const migrateSettingsV19toV20 = (settings: any): SettingsData =>

================
File: src/main/store/migrations/v2-to-v3.ts
================
/* eslint-disable @typescript-eslint/no-explicit-any */
import { DEFAULT_SETTINGS } from '../store';
⋮----
export const migrateSettingsV2toV3 = (settings: any): any =>
⋮----
disabledTools: [], // Clear disabled tools
⋮----
mcpAgent: undefined, // Remove the old key

================
File: src/main/store/migrations/v20-to-v21.ts
================
import { ProviderProfile } from '@common/types';
⋮----
export const migrateProvidersV20toV21 = (providers: ProviderProfile[]): ProviderProfile[] =>

================
File: src/main/store/migrations/v21-to-v22.ts
================
import fs from 'fs/promises';
import path from 'path';
⋮----
import { McpServerConfig, SettingsData } from '@common/types';
import { fileExists } from '@common/utils';
⋮----
import { AIDER_DESK_HOME_DIR, AIDER_DESK_MCP_SERVERS_FILE } from '@/constants';
import logger from '@/logger';
⋮----
export const migrateSettingsV21toV22 = async (settings: SettingsData): Promise<SettingsData> =>
⋮----
// eslint-disable-next-line @typescript-eslint/no-explicit-any
⋮----
// keeping mcpServers in settings for now, just in case user needs to revert the version

================
File: src/main/store/migrations/v3-to-v4.ts
================
/* eslint-disable @typescript-eslint/no-explicit-any */
import { SettingsData } from '@common/types';
import { DEFAULT_AGENT_PROFILE, LlmProvider, LlmProviderName } from '@common/agent';
⋮----
export const migrateSettingsV3toV4 = (settingsV3: any): SettingsData =>
⋮----
// Ensure agentConfig and its properties exist, providing defaults if necessary
⋮----
export const migrateOpenProjectsV3toV4 = (openProjectsV3: any): string[] =>

================
File: src/main/store/migrations/v4-to-v5.ts
================
/* eslint-disable @typescript-eslint/no-explicit-any */
import { SettingsData } from '@common/types';
⋮----
export const migrateSettingsV4toV5 = (settingsV4: any): SettingsData =>

================
File: src/main/store/migrations/v5-to-v6.ts
================
/* eslint-disable @typescript-eslint/no-explicit-any */
import { SettingsData } from '@common/types';
⋮----
export const migrateSettingsV5toV6 = (settingsV5: any): SettingsData =>

================
File: src/main/store/migrations/v6-to-v7.ts
================
/* eslint-disable @typescript-eslint/no-explicit-any */
import { AgentProfile, SettingsData, ToolApprovalState } from '@common/types';
⋮----
export const migrateV6ToV7 = (settings: SettingsData): SettingsData =>
⋮----
// Update AIDER_TOOL_ADD_CONTEXT_FILE to AIDER_TOOL_ADD_CONTEXT_FILES
⋮----
// Update AIDER_TOOL_DROP_CONTEXT_FILE to AIDER_TOOL_DROP_CONTEXT_FILES

================
File: src/main/store/migrations/v7-to-v8.ts
================
/* eslint-disable @typescript-eslint/no-explicit-any */
import { AgentProfile, SettingsData } from '@common/types';
⋮----
export const migrateV7ToV8 = (settings: SettingsData): SettingsData =>
⋮----
// Add temperature field with default value of 0.7 if it doesn't exist

================
File: src/main/store/migrations/v8-to-v9.ts
================
import { SettingsData } from '@common/types';
import { isOpenRouterProvider } from '@common/agent';
⋮----
export const migrateV8ToV9 = (settings: SettingsData): SettingsData =>

================
File: src/main/store/migrations/v9-to-v10.ts
================
import { SettingsData } from '@common/types';
import { isOpenRouterProvider } from '@common/agent';
⋮----
export const migrateV9ToV10 = (settings: SettingsData): SettingsData =>

================
File: src/main/store/index.ts
================


================
File: src/main/store/store.ts
================
import { v4 as uuidv4 } from 'uuid';
import {
  ContextCompactionType,
  DiffViewMode,
  FileWatchMode,
  MemoryEmbeddingProvider,
  MessageViewMode,
  ProjectData,
  ProjectSettings,
  ProjectStartMode,
  ProviderProfile,
  SettingsData,
  SuggestionMode,
  WindowState,
} from '@common/types';
import { AIDER_DESK_EXTENSIONS_REPO_URL } from '@common/extensions';
import { normalizeBaseDir } from '@common/utils';
⋮----
import { migrateSettingsV0toV1 } from './migrations/v0-to-v1';
import { migrateSettingsV1toV2 } from './migrations/v1-to-v2';
import { migrateSettingsV2toV3 } from './migrations/v2-to-v3';
import { migrateOpenProjectsV3toV4, migrateSettingsV3toV4 } from './migrations/v3-to-v4';
import { migrateSettingsV4toV5 } from './migrations/v4-to-v5';
import { migrateSettingsV5toV6 } from './migrations/v5-to-v6';
import { migrateV6ToV7 } from './migrations/v6-to-v7';
import { migrateV7ToV8 } from './migrations/v7-to-v8';
import { migrateV8ToV9 } from './migrations/v8-to-v9';
import { migrateV9ToV10 } from './migrations/v9-to-v10';
import { migrateV10ToV11 } from './migrations/v10-to-v11';
import { migrateV11ToV12 } from './migrations/v11-to-v12';
import { migrateV12ToV13 } from './migrations/v12-to-v13';
⋮----
import { getDefaultProjectSettings } from '@/utils';
import logger from '@/logger';
import { migrateProvidersV13toV14 } from '@/store/migrations/v13-to-v14';
import { migrateSettingsV14toV15 } from '@/store/migrations/v14-to-v15';
import { migrateSettingsV15toV16 } from '@/store/migrations/v15-to-v16';
import { migrateSettingsV16toV17 } from '@/store/migrations/v16-to-v17';
import { migrateSettingsV17toV18 } from '@/store/migrations/v17-to-v18';
import { migrateSettingsV18toV19 } from '@/store/migrations/v18-to-v19';
import { migrateSettingsV19toV20 } from '@/store/migrations/v19-to-v20';
import { migrateProvidersV20toV21 } from '@/store/migrations/v20-to-v21';
import { migrateSettingsV21toV22 } from '@/store/migrations/v21-to-v22';
⋮----
const compareBaseDirs = (baseDir1: string, baseDir2: string): boolean =>
⋮----
interface StoreSchema {
  windowState: WindowState;
  openProjects: ProjectData[];
  recentProjects: string[]; // baseDir paths of recently closed projects
  settings: SettingsData;
  providers: ProviderProfile[];
  settingsVersion: number;
  releaseNotes?: string | null;
  userId?: string;
}
⋮----
recentProjects: string[]; // baseDir paths of recently closed projects
⋮----
export class Store
⋮----
// @ts-expect-error expected to be initialized
⋮----
async init(path?: string): Promise<void>
⋮----
getUserId(): string
⋮----
createDefaultSettings(): SettingsData
⋮----
getSettings(): SettingsData
⋮----
// Ensure proper merging for nested objects
⋮----
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private async migrateSettings(settings: any, openProjects: any, providers: any): Promise<SettingsData>
⋮----
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private findOutCurrentVersion(settings: any): number
⋮----
// Check if agentProfiles still exists in settings (pre-v17)
⋮----
saveSettings(settings: SettingsData): void
⋮----
getOpenProjects(): ProjectData[]
⋮----
setOpenProjects(projects: ProjectData[]): void
⋮----
updateOpenProjectsOrder(baseDirs: string[]): ProjectData[]
⋮----
// This case should ideally not happen if baseDirs comes from the existing open projects.
// If it can happen, we might need to decide how to handle it (e.g., log a warning).
⋮----
getRecentProjects(): string[]
⋮----
addRecentProject(baseDir: string): void
⋮----
removeRecentProject(baseDir: string): void
⋮----
getProjectSettings(baseDir: string): ProjectSettings
⋮----
saveProjectSettings(baseDir: string, settings: ProjectSettings): ProjectSettings
⋮----
getWindowState(): StoreSchema['windowState']
⋮----
private getDefaultWindowState(): WindowState
⋮----
setWindowState(windowState: WindowState): void
⋮----
getReleaseNotes(): string | null
⋮----
clearReleaseNotes(): void
⋮----
setReleaseNotes(releaseNotes: string)
⋮----
getProviders(): ProviderProfile[]
⋮----
setProviders(providers: ProviderProfile[]): void

================
File: src/main/task/migrations/v1-to-v2.ts
================
import { ContextMessage, TaskContext } from '@common/types';
import { AIDER_TOOL_GROUP_NAME, AIDER_TOOL_RUN_PROMPT, SUBAGENTS_TOOL_GROUP_NAME, SUBAGENTS_TOOL_RUN_TASK } from '@common/tools';
import { extractServerNameToolName } from '@common/utils';
⋮----
import logger from '@/logger';
⋮----
/**
 * Migrates a single ContextMessage from V1 (AI SDK v4) to V2 (AI SDK v5) format.
 * This function is reusable and can handle recursive migration for subagent messages.
 */
const migrateContextMessage = (message: ContextMessage): ContextMessage =>
⋮----
// If content is already an array, it might be V1 format that needs migration or already V2 format
⋮----
// Check if this is V1 format by looking for 'args' or 'result' properties
⋮----
// Migrate V1 format to V2
⋮----
// V1 tool-call: change 'args' to 'input'
⋮----
args: undefined, // Remove old property
⋮----
// V1 tool-result: change 'result' to 'output' with proper typing
⋮----
// Check if this is a subagent tool result with messages that need migration
⋮----
// @ts-expect-error value is expected to have messages
⋮----
// Recursively migrate each message in the subagent result
⋮----
// @ts-expect-error value is expected to have responses
⋮----
// Recursively migrate each response message
⋮----
result: undefined, // Remove old property
⋮----
// Leave other parts (text, reasoning, etc.) unchanged
⋮----
// Return the message as is if it doesn't match migration conditions
⋮----
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const migrateContextV1toV2 = (taskId: string, sessionData: any): TaskContext =>

================
File: src/main/task/aider-manager.ts
================
import { ChildProcessWithoutNullStreams, spawn } from 'child_process';
import { createHash } from 'crypto';
import { unlinkSync } from 'fs';
import fs from 'fs/promises';
import path from 'path';
⋮----
import { EditFormat, ModelsData } from '@common/types';
import { fileExists } from '@common/utils';
import treeKill from 'tree-kill';
import { DEFAULT_AIDER_MAIN_MODEL } from '@common/agent';
⋮----
import { AIDER_DESK_CONNECTOR_DIR, AIDER_DESK_PROJECT_RULES_DIR, AIDER_DESK_TASKS_DIR, PID_FILES_DIR, PYTHON_COMMAND, SERVER_PORT } from '@/constants';
import { Connector } from '@/connector';
import { CONNECTOR_TOKEN } from '@/connector/connector-auth';
import logger from '@/logger';
import { Store } from '@/store';
import { ModelManager } from '@/models';
import { EventManager } from '@/events';
import { getEnvironmentVariablesForAider } from '@/utils';
import { Task } from '@/task/task';
import { PythonDependenciesInstaller } from '@/python-dependencies-installer';
import { getNetworkEnvVars } from '@/network-manager';
⋮----
export class AiderManager
⋮----
constructor(
⋮----
public async start(forceRestart = false): Promise<void>
⋮----
// Set aiderStarting to true when starting aider
⋮----
// Ensure Python dependencies are installed before spawning
⋮----
i++; // Skip the model value
⋮----
// Spawn without shell to have direct process control
⋮----
public async kill(): Promise<void>
⋮----
public isStarted(): boolean
⋮----
private handleAiderProcessExit(process: ChildProcessWithoutNullStreams, code: number | null): void
⋮----
// A forced restart can spawn a replacement before the old close event is
// delivered. Never let that stale event clear the replacement process.
⋮----
private createAiderStartPromise(): Promise<void>
⋮----
public async waitForStart(): Promise<void>
⋮----
private getAiderProcessPidFilePath(): string
⋮----
private async writeAiderProcessPidFile(): Promise<void>
⋮----
private removeAiderProcessPidFile()
⋮----
private async checkAndCleanupPidFile(): Promise<void>
⋮----
public handleConnectorAdded(connector: Connector): void
⋮----
// Set aiderStarting to false when a connector with source==='aider' is added
⋮----
public updateAiderModels(modelsData: ModelsData): void
⋮----
const getWeakModelProvider = () =>
⋮----
// use the provider prefix from the main model when Aider's provider prefix is different
⋮----
sendUpdateAiderModels()
⋮----
public updateModels(mainModel: string, weakModel: string | null, editFormat: EditFormat = 'diff'): void
⋮----
private sendSetModels(mainModel: string, weakModel: string | null, editFormat: EditFormat = 'diff', environmentVariables?: Record<string, string>): void
⋮----
public setArchitectModel(architectModel: string): void
⋮----
public getArchitectModel(): string | null
⋮----
public getAiderModelsData(): ModelsData | null
⋮----
public getRepoMap(): string
⋮----
public setRepoMap(repoMap: string): void
⋮----
public openCommandOutput(command: string): void
⋮----
private addCommandOutput(command: string, output: string): void
⋮----
// Append output to the commandOutputs map
⋮----
public closeCommandOutput(addToContext = true):
⋮----
public sendUpdateEnvVars(environmentVariables: Record<string, unknown>): void

================
File: src/main/task/context-manager.ts
================
import path from 'path';
import { promises as fs } from 'fs';
⋮----
import { v4 as uuidv4 } from 'uuid';
import debounce from 'lodash/debounce';
import { isEqual } from 'lodash';
import {
  ToolResultPart,
  ConnectorMessage,
  ContextFile,
  ContextMessage,
  ContextToolMessage,
  MessageRole,
  ResponseCompletedData,
  TaskContext,
  ToolData,
  UsageReportData,
  UserMessageData,
} from '@common/types';
import { extractServerNameToolName, extractTextContent, fileExists, isMessageEmpty, isTextContent } from '@common/utils';
import { AIDER_TOOL_GROUP_NAME, AIDER_TOOL_RUN_PROMPT, SUBAGENTS_TOOL_GROUP_NAME, SUBAGENTS_TOOL_RUN_TASK } from '@common/tools';
⋮----
import logger from '@/logger';
import { Task } from '@/task';
import { isDirectory, isFileIgnored, withLock } from '@/utils';
import { atomicWriteJsonFile, cleanupStaleTempFiles } from '@/utils/atomic-write';
import { extractPromptContextFromToolResult } from '@/agent/utils';
import { migrateContextV1toV2 } from '@/task/migrations/v1-to-v2';
import { AIDER_DESK_TASKS_DIR } from '@/constants';
⋮----
export class ContextManager
⋮----
constructor(
    private readonly task: Task,
    private readonly taskId: string,
    initialMessages: ContextMessage[] = [],
    initialFiles: ContextFile[] = [],
)
⋮----
// Task-specific storage path - single context per task
⋮----
hasUndoSnapshot(): boolean
⋮----
undoContextChange(): ContextMessage[] | null
⋮----
public enableAutosave()
⋮----
public disableAutosave()
⋮----
addContextMessage(role: MessageRole, content: string, usageReport?: UsageReportData): void;
addContextMessage(message: ContextMessage): void;
addContextMessage(roleOrMessage: MessageRole | ContextMessage, content?: string, usageReport?: UsageReportData)
⋮----
private async isFileIgnored(contextFile: ContextFile): Promise<boolean>
⋮----
async addContextFile(contextFile: ContextFile): Promise<ContextFile[]>
⋮----
/**
   * Resolves a relative file path by delegating to Task.resolveContextFilePath
   * which tries taskDir first, then falls back to projectDir.
   */
private async resolveContextFilePath(relativePath: string): Promise<string | null>
⋮----
/**
   * Finds an already-added context file matching the given resolved absolute path, if any.
   * Accounts for files that may have been resolved from either taskDir or projectDir.
   */
private findExistingContextFile(absolutePath: string): ContextFile | undefined
⋮----
dropContextFile(filePath: string): ContextFile[]
⋮----
setContextFiles(contextFiles: ContextFile[], save = true)
⋮----
getContextFiles(): ContextFile[]
⋮----
async getContextFilesEnsureLoaded(): Promise<ContextFile[]>
⋮----
setContextMessages(contextMessages: ContextMessage[], save = true)
⋮----
async getContextMessages(): Promise<ContextMessage[]>
⋮----
// Deep clone messages to prevent external modifications from affecting internal state
⋮----
clearContextFiles(save = true)
⋮----
clearMessages(save = true, createSnapshot = true)
⋮----
removeMessageById(messageId: string): string[]
⋮----
// First, try to find a message with the exact ID
⋮----
// If it's an assistant message with array content
⋮----
// Check if it has mixed content (text/reasoning + tool calls)
⋮----
// Remove only text and reasoning parts, keep the message with tool calls
⋮----
// Otherwise, remove the whole message
⋮----
// If not found by ID, try to find by tool call ID
⋮----
removeMessagesByIds(ids: string[]): void
⋮----
private removeMessageByIndex(index: number, messageId: string): string[]
⋮----
private removeByToolCallId(toolCallId: string): string[]
⋮----
// Find tool message with matching toolCallId
⋮----
// Remove only the tool-result part from the tool message
⋮----
// If tool message is now empty, remove the whole message
⋮----
// Search backwards for assistant message with matching tool-call
⋮----
// If assistant message is now empty, remove the whole message
⋮----
removeLastMessage(): void
⋮----
removeMessagesUpToUserMessage(messageId: string): ContextMessage[]
⋮----
toConnectorMessages(contextMessages: ContextMessage[] = this.messages): ConnectorMessage[]
⋮----
async save(): Promise<void>
⋮----
// Serialize writes so concurrent saves cannot interleave and corrupt the file
⋮----
// Remove temp files orphaned by a crash between writeFile and rename
⋮----
// Write atomically: write to a temp file, then rename over the target so a
// crash mid-write never leaves context.json half-written.
⋮----
/**
   * Creates a backup of the current context.json file before a destructive operation
   * (e.g., smart compaction). Backups are named context.backup.001.json, context.backup.002.json, etc.
   * For debugging purposes only.
   */
async backupContext(): Promise<void>
⋮----
private async cleanupContext(): Promise<void>
⋮----
// Filter out files that no longer exist
⋮----
private async migrateContext(contextData: unknown): Promise<TaskContext>
⋮----
// Sequential migration chain
⋮----
// Future migrations will be added here
// if (sessionVersion === 2) {
//   migratedData = migrateSessionV2toV3(migratedData);
//   sessionVersion = 3;
// }
⋮----
async load(): Promise<void>
⋮----
async reloadFromDisk(): Promise<boolean>
⋮----
private async readContextFromDisk(): Promise<TaskContext | null>
⋮----
/**
   * Recovers from a corrupt/truncated context.json by falling back to the newest
   * backup if one exists, otherwise moving the corrupt file aside and returning an
   * empty context so the task still loads instead of failing outright.
   */
private async recoverCorruptContext(): Promise<TaskContext | null>
⋮----
// No usable backup: move the corrupt file aside so it does not keep breaking loads.
⋮----
private async loadInternal(): Promise<void>
⋮----
async loadMessages(messages: ContextMessage[], updateTaskState = true): Promise<void>
⋮----
// Clear all current messages
⋮----
// Get the messages data and send them to the UI
⋮----
// UserMessageData
⋮----
// ResponseCompletedData
⋮----
// ToolData
⋮----
// send messages to Connectors (Aider)
⋮----
async delete(): Promise<void>
⋮----
private autosave()
⋮----
public getContextMessagesData(contextMessages: ContextMessage[] = this.messages): (UserMessageData | ResponseCompletedData | ToolData)[]
⋮----
const processToolResult = (part: ToolResultPart) =>
⋮----
// Handle aider tool responses - create ResponseCompletedData for each response
⋮----
// Handle agent tool results - process all messages from subagent
⋮----
// Collect reasoning and text parts to combine them if both exist
⋮----
// Process tool-call parts
⋮----
const pushResponseCompletedData = (content = '') =>
⋮----
// Create tool message for tool-call parts
⋮----
// Now update tool messages with their responses from tool-results
⋮----
async generateContextMarkdown(): Promise<string | null>
⋮----
getMessagesUpTo(messageId: string): ContextMessage[]
⋮----
// If not found as a message ID, check if it's a toolCallId
⋮----
// If it's an assistant message with tool calls, we need to filter out tool calls
⋮----
// Filter the assistant message to keep only reasoning and text parts, exclude tool calls
⋮----
// If it's a tool message, we need to handle the assistant message with tool calls
⋮----
// messageId might be a toolCallId passed directly, or the message ID
// Try to get the toolCallId from the message content
⋮----
// If messageId is a toolCallId that doesn't match the first result, search for it
⋮----
// Find the assistant message that contains this tool call
⋮----
// Clone messages up to and including the assistant message
⋮----
// Clone the assistant message and keep only tool calls up to and including the target
⋮----
// Add all tool messages up to and including the target
⋮----
// Check if this tool message contains a tool-result for a tool-call we kept
⋮----
// Check if this tool call is in the assistant message we kept
⋮----
// For non-tool messages or if we couldn't find the assistant message, return all messages up to the target
⋮----
/**
   * Removes all messages after the specified message ID, keeping messages up to and including the specified message.
   * Uses intelligent slicing logic from getMessagesUpTo to handle tool messages and assistant messages with tool calls.
   * For assistant messages that are the target, inverts the filtering to keep tool calls instead of text/reasoning.
   *
   * @param messageId - The ID of the message to keep (all messages after this will be removed), or a toolCallId
   * @returns Array of removed message IDs
   */
removeMessagesAfter(messageId: string): string[]
⋮----
// Find the target message index in the original messages array
⋮----
// If not found as a message ID, check if it's a toolCallId
⋮----
// Get the messages to keep using the intelligent slicing logic
⋮----
// If no messages would be removed, return early
⋮----
// Process messages: deep clone all messages and handle assistant message filtering
⋮----
// Deep clone the message object
⋮----
// For target assistant message, get original content and keep text/reasoning (remove tool calls)
⋮----
// For non-target assistant messages, keep tool calls and text, remove reasoning only
⋮----
// Collect IDs of messages that will be removed
⋮----
// Replace the messages array with the processed messages
⋮----
// Trigger autosave if messages were removed

================
File: src/main/task/index.ts
================


================
File: src/main/task/task.ts
================
import fs from 'fs/promises';
import path from 'path';
import { homedir } from 'os';
⋮----
import {
  AgentProfile,
  AIDER_COMMANDS,
  AIDER_MODES,
  AiderRunOptions,
  AutonomyMode,
  ChangeRequestItem,
  ConnectorMessage,
  ContextAssistantMessage,
  ContextFile,
  ContextMessage,
  ContextToolMessage,
  DEFAULT_AUTONOMY_MODE,
  DefaultTaskState,
  EditFormat,
  FileEdit,
  BranchInfo,
  GitSyncCommits,
  LogData,
  LogLevel,
  MergeState,
  MessageRole,
  Mode,
  ModelInfo,
  ModelsData,
  ProjectSettings,
  PromptContext,
  QuestionData,
  QueuedPromptData,
  ResponseChunkData,
  ResponseCompletedData,
  SettingsData,
  SkillDefinition as Skill,
  TaskData,
  TaskStateData,
  TaskStateEmoji,
  TodoItem,
  TokensInfoData,
  ToolApprovalState,
  ToolCallPart,
  ToolData,
  ToolInputChunkData,
  ToolResultPart,
  UpdatedFile,
  UpdatedFilesGroupMode,
  UsageReportData,
  UserMessageData,
  SwitchToLocalOptions,
  SwitchToWorktreeOptions,
  TaskGitStatus,
  TaskGitWorktreeStatus,
  WorkingMode,
  UncommittedFiles,
} from '@common/types';
import { parsePartialJson } from 'ai';
import {
  extractImagesFromContent,
  extractProviderModel,
  extractServerNameToolName,
  extractTextContent,
  fileExists,
  parseCommandArgs,
  parseSkillCommand,
  parseUsageReport,
} from '@common/utils';
import {
  COMPACT_CONVERSATION_AGENT_PROFILE,
  CONFLICT_RESOLUTION_PROFILE,
  HANDOFF_AGENT_PROFILE,
  INIT_PROJECT_AGENTS_PROFILE,
  getSubagentId,
} from '@common/agent';
import {
  POWER_TOOL_BASH,
  POWER_TOOL_GROUP_NAME,
  SKILLS_TOOL_ACTIVATE_SKILL,
  SKILLS_TOOL_GROUP_NAME,
  SUBAGENTS_TOOL_GROUP_NAME,
  SUBAGENTS_TOOL_RUN_TASK,
  TOOL_GROUP_NAME_SEPARATOR,
} from '@common/tools';
import { v4 as uuidv4 } from 'uuid';
import debounce from 'lodash/debounce';
import { isEqual } from 'lodash';
⋮----
import type { z } from 'zod';
import type { ToolContent, JSONValue } from '@common/types';
import type { RegisteredCommand } from '@/extensions/extension-manager';
import type { WatchHandle } from '@/file-watcher-manager';
import type { SmartCompactionPassSelection } from '@/agent/smart-compaction';
import type { SmartCompactionOptions } from '@common/extensions';
⋮----
import { ExtensionEventMap, ExtensionManager } from '@/extensions/extension-manager';
import { getAllFiles, isValidProjectFile } from '@/utils/file-system';
import { atomicWriteJsonFile } from '@/utils/atomic-write';
import {
  AIDER_DESK_GLOBAL_RULES_DIR,
  AIDER_DESK_PROJECT_RULES_DIR,
  AIDER_DESK_TASKS_DIR,
  AIDER_DESK_TMP_DIR,
  AIDER_DESK_TODOS_FILE,
  WORKTREE_BRANCH_PREFIX,
} from '@/constants';
import { Agent, AgentProfileManager, McpConfigManager, McpManager } from '@/agent';
import { safeJsonStringify } from '@/agent/utils';
import { findEnabledSubagent, runSubagentTask } from '@/agent/subagent';
import { Connector } from '@/connector';
import { DataManager } from '@/data-manager';
import logger from '@/logger';
import { MessageAction, ResponseMessage } from '@/messages';
import { Store } from '@/store';
import { ModelManager } from '@/models';
import { CustomCommandManager, ShellCommandError } from '@/custom-commands';
import { TelemetryManager } from '@/telemetry';
import { EventManager } from '@/events';
import { execWithShellPath, getEnvironmentVariablesForAider, isDirectory } from '@/utils';
import { ContextManager } from '@/task/context-manager';
import { Project } from '@/project';
import { AiderManager } from '@/task/aider-manager';
import { SkillManager } from '@/skills/skill-manager';
import { GitError, GitManager } from '@/git';
import { MemoryManager } from '@/memory/memory-manager';
import { getElectronApp } from '@/app';
import { PromptsManager } from '@/prompts';
import { PythonDependenciesInstaller } from '@/python-dependencies-installer';
import { getNetworkEnvVars } from '@/network-manager';
import { CompactionLevel, extractSummary, smartCompactMessages } from '@/agent/compaction';
⋮----
const getTaskFinishedNotificationText = (task: TaskData) =>
⋮----
export class Task
⋮----
constructor(
    public readonly project: Project,
    public readonly taskId: string,
    private readonly store: Store,
    private readonly mcpManager: McpManager,
    private readonly mcpConfigManager: McpConfigManager,
    private readonly customCommandManager: CustomCommandManager,
    private readonly agentProfileManager: AgentProfileManager,
    private readonly telemetryManager: TelemetryManager,
    private readonly dataManager: DataManager,
    private readonly eventManager: EventManager,
    private readonly modelManager: ModelManager,
    private readonly gitManager: GitManager,
    private readonly memoryManager: MemoryManager,
    private readonly promptsManager: PromptsManager,
    private readonly extensionManager: ExtensionManager,
    private readonly pythonInstaller: PythonDependenciesInstaller,
    initialTaskData?: Partial<TaskData>,
)
⋮----
public async waitForTaskDataLoad(): Promise<void>
⋮----
public async getTaskAgentProfile(): Promise<AgentProfile | null>
⋮----
// Check task-level agent profile first
⋮----
// If no task-level profile, fall back to project-level
⋮----
// Apply task-level provider/model overrides if present
⋮----
// Create a temporary profile with task-level overrides
⋮----
private getAuxiliaryModelId(baseProfile: AgentProfile, auxiliaryModelId: string | null | undefined): string
⋮----
private async loadTaskData()
⋮----
private async readTaskDataFromDisk(): Promise<Partial<TaskData> | null>
⋮----
private applyTaskData(data: Partial<TaskData>)
⋮----
public async reloadFromDisk(): Promise<
⋮----
/**
   * Generate a branch name from task name (first 7 words, separated by '-')
   */
private generateBranchName(): string
⋮----
// Split into words, filter out empty strings, and take first 7
⋮----
.replace(/[^a-z0-9\s-]/g, '') // Remove special characters except spaces and hyphens
⋮----
// Join with hyphens and ensure it's a valid branch name
⋮----
// Ensure branch name doesn't start with a dot or dash, and replace consecutive dashes
⋮----
.replace(/^[.-]+/, '') // Remove leading dots or dashes
.replace(/-+/g, '-') // Replace multiple dashes with single dash
.replace(/-$/, ''); // Remove trailing dash
⋮----
// If result is empty, use shortened taskId (first segment of UUID)
⋮----
private async renameWorktreeBranchIfNeeded(): Promise<void>
⋮----
// Don't rename a branch owned by another task sharing this worktree (e.g. parent set it)
⋮----
/**
   * Resolves missing `baseBranch` for worktrees created while the main repo
   * was in detached HEAD state, where `baseBranch` was never stored.
   */
private async resolveMissingWorktreeBaseBranch(): Promise<boolean>
⋮----
// The task branch itself always contains its base commit - never a valid base branch candidate
⋮----
private isInternal()
⋮----
public async saveTask(updates?: Partial<TaskData>, updateTimestamps = true): Promise<TaskData>
⋮----
// Internal task is not saved
⋮----
// Allow extensions to modify task data before saving
⋮----
// only save if task is not new
⋮----
/**
   * Starts/stops watching the task dir for file changes while the task is in progress.
   */
private syncFileWatching(previousTaskState: TaskData['state'] | undefined)
⋮----
private stopFileWatching()
⋮----
public async onWatchedFilesChanged()
⋮----
public async init(readonly = false)
⋮----
private async initInternal(readonly: boolean)
⋮----
// Readonly init must not mutate worktrees, spawn connectors, or run expensive scans
⋮----
// Check if worktree is enabled for this task
⋮----
// getTaskWorktree reports baseCommit as the worktree's current HEAD, so preserve
// the stored rebase fork-point when re-adopting an existing worktree
⋮----
// Worktree is already set (e.g. inherited from parent)
⋮----
// Create a default worktree for this task
⋮----
// Check if worktree exists and set worktreeEnabled accordingly
⋮----
// Only remove the worktree if no other tasks share it
⋮----
public async load(readonly = false): Promise<TaskStateData>
⋮----
private async loadContext()
⋮----
public addConnector(connector: Connector)
⋮----
// Handle connector addition in AiderManager
⋮----
public removeConnector(connector: Connector)
⋮----
public getProjectDir()
⋮----
public getTaskDir()
⋮----
public compileCustomSystemPrompt(template: string): Promise<string>
⋮----
/**
   * Resolves a relative file path against taskDir first, then falls back to projectDir.
   * This handles git worktree cases where files may exist in the project directory
   * but not in the worktree (taskDir).
   *
   * @param relativePath - Relative file path to resolve
   * @returns The resolved absolute path if the file/dir exists in either location, or null otherwise
   */
public async resolveContextFilePath(relativePath: string): Promise<string | null>
⋮----
/**
   * Dispatches an extension event through the ExtensionManager.
   * This public method provides controlled access to extension event dispatch
   * without exposing the ExtensionManager instance directly.
   *
   * @param eventName - The name of the event to dispatch
   * @param event - The event payload
   * @returns Object containing potentially modified event, blocked status, and modified result
   */
public dispatchExtensionEvent<K extends keyof ExtensionEventMap>(eventName: K, event: ExtensionEventMap[K]): Promise<ExtensionEventMap[K]>
⋮----
private normalizeFilePath(filePath: string): string
⋮----
private cleanupChunkBuffers()
⋮----
public async close(clearContext = false, cleanupEmptyTask = true)
⋮----
private async cleanUpEmptyTask()
⋮----
private findMessageConnectors(action: MessageAction): Connector[]
⋮----
private async waitForCurrentPromptToFinish()
⋮----
public async waitForCurrentAgentToFinish()
⋮----
private resolveAgentRunPromises()
⋮----
public isPromptRunning()
⋮----
public hasQueuedPrompts(): boolean
⋮----
public async waitForIdle(): Promise<void>
⋮----
public async runPrompt(
    prompt: string,
    mode: Mode = this.getCurrentMode(),
    addToInputHistory = true,
    userMessageId = uuidv4(),
    sendNotification = true,
    images?: string[],
): Promise<ResponseCompletedData[]>
⋮----
// Queue the prompt for later execution
⋮----
public async savePromptOnly(prompt: string, addInputHistory = true): Promise<void>
⋮----
// Add to context manager
⋮----
// Add user message to context
⋮----
public async saveEditedPrompt(messageId: string, prompt: string): Promise<void>
⋮----
private async runNextQueuedPrompt(): Promise<ResponseCompletedData[]>
⋮----
public async runPromptInAider(mode: Mode, prompt: string, promptContext: PromptContext, sendNotification = true): Promise<ResponseCompletedData[]>
⋮----
// Detect files in prompt and ask to add them to context (only for aider modes)
⋮----
// Persist user message to the task context before running Aider so it can be redone even if the first call fails.
⋮----
// Create enhanced assistant message with full metadata
⋮----
public async runPromptInAgent(
    profile: AgentProfile,
    mode: Mode,
    prompt: string | null,
    promptContext: PromptContext = { id: uuidv4() },
    contextMessages?: ContextMessage[],
    contextFiles?: ContextFile[],
    systemPrompt?: string,
    waitForCurrentAgentToFinish = true,
    sendNotification = true,
    images?: string[],
    skillsToActivate?: string[],
): Promise<ResponseCompletedData[]>
⋮----
// reset smart compaction level on agent start
⋮----
// send messages to connectors
⋮----
// Determine task state based on the last assistant message
⋮----
// check once again after determining task state which can task some time
⋮----
private getTaskNameFromPrompt(prompt: string): string
⋮----
private async generateTaskNameInBackground(prompt: string): Promise<string | null>
⋮----
private isWaitingForApproval(resultMessages: ContextMessage[]): boolean
⋮----
private async determineTaskState(resultMessages: ContextMessage[]): Promise<string | null>
⋮----
// Find the last assistant message from result messages
⋮----
// Extract reasoning and text from the last assistant message
⋮----
// Create a user message wrapping the last assistant message information
⋮----
public async runSubagent(
    profile: AgentProfile,
    prompt: string,
    contextMessages?: ContextMessage[],
    contextFiles?: ContextFile[],
    systemPrompt?: string,
    abortController?: AbortController,
    promptContext?: PromptContext,
): Promise<ContextMessage[]>
⋮----
// Register abort controller if provided and promptContext has interruptId
⋮----
// Unregister abort controller if it was registered
⋮----
public sendPromptToAider(
    prompt: string,
    promptContext: PromptContext = { id: uuidv4() },
    mode?: Mode,
    messages: ConnectorMessage[] = this.contextManager.toConnectorMessages(),
    files: ContextFile[] = this.contextManager.getContextFiles(),
    options?: AiderRunOptions,
): Promise<ResponseCompletedData[]>
⋮----
// Wait for prompt to finish and return collected responses
⋮----
public promptFinished(promptId?: string)
⋮----
// Notify waiting prompts with collected responses
⋮----
public async processResponseMessage(message: ResponseMessage, saveToDb = true)
⋮----
const sendResponseChunk = async (content?: string, reasoning?: string) =>
⋮----
// First chunk: send immediately and create interval
⋮----
// No buffered chunk, stop interval
⋮----
// Subsequent chunks: append to buffer
⋮----
// Collect the completed response
⋮----
// Sort by sequence number when adding
⋮----
sendResponseCompleted(data: ResponseCompletedData)
⋮----
public startToolInput(toolCallId: string, serverName: string, toolName: string, promptContext?: PromptContext): void
⋮----
public processToolInputDelta(toolCallId: string, delta: string): void
⋮----
public finishToolInput(toolCallId: string): void
⋮----
private async flushToolInput(toolCallId: string, isComplete: boolean): Promise<void>
⋮----
// Graceful fallback — skip this flush
⋮----
private sendToolInputChunk(toolCallId: string, partialArgs: unknown, isComplete: boolean): void
⋮----
private notifyIfEnabled(title: string, text: string)
⋮----
// eslint-disable-next-line @typescript-eslint/no-require-imports
⋮----
private getQuestionKey(question: QuestionData): string
⋮----
public async answerQuestion(answer: string, userInput?: string): Promise<boolean>
⋮----
// Send question-answered event
⋮----
public async addFiles(...contextFiles: ContextFile[])
⋮----
// Extension event uses plural name
⋮----
// Send add file message for each added file
⋮----
public sendAddFile(contextFile: ContextFile, noUpdate?: boolean)
⋮----
public async dropFile(filePath: string)
⋮----
// Extension event uses plural name and ContextFile array
⋮----
// Send drop file message for each dropped file
⋮----
public sendDropFile(filePath: string, readOnly?: boolean, noUpdate?: boolean): void
⋮----
public async addToGit(absolutePath: string): Promise<void>
⋮----
private sendStateUpdated()
⋮----
private async sendContextFilesUpdated()
⋮----
public async refreshContextFiles(): Promise<void>
⋮----
public async runCommand(command: string, addToHistory = true)
⋮----
private async handleSubagentCommand(commandArgs: string): Promise<void>
⋮----
public updateContextFiles(contextFiles: ContextFile[])
⋮----
public async askQuestion(questionData: QuestionData, awaitAnswer = true): Promise<[string, string | undefined]>
⋮----
// Wait if another question is already pending
⋮----
// group questions have a default set of answers
⋮----
// At this point, this.currentQuestion should be null due to the loop above,
// or it was null initially.
⋮----
// Auto-answer based on stored preference
⋮----
// Store the resolve function for the promise
⋮----
public updateAiderModels(modelsData: ModelsData)
⋮----
public updateModels(mainModel: string, weakModel: string | null, editFormat: EditFormat = 'diff')
⋮----
public setArchitectModel(architectModel: string)
⋮----
private async sendUpdateModelsInfo(): Promise<void>
⋮----
// Helper function to extract model info using getModel with fallback
const extractModelInfo = (modelName: string | null | undefined) =>
⋮----
// Extract info for main model
⋮----
// Extract info for weak model
⋮----
// Extract info for architect model
⋮----
// Send to connectors that listen to 'update-models-info'
⋮----
private async detectAndAddFilesFromPrompt(prompt: string): Promise<void>
⋮----
// First pass: match files by full path
⋮----
// Check if any context file matches a path in the prompt and track those names
⋮----
// Second pass: match files by filename only (if not already matched by path)
⋮----
// Skip if already matched by path
⋮----
// Skip if filename was already matched by path (prevents false positives)
⋮----
// Only match by filename if it's not empty and appears in the prompt
⋮----
public async getAddableFiles(searchRegex?: string): Promise<string[]>
⋮----
public async getAllFiles(useGit = true): Promise<string[]>
⋮----
public async getUpdatedFiles(): Promise<UpdatedFile[]>
⋮----
public async getUpdatedFileDiff(filePath: string, commitHash?: string): Promise<string>
⋮----
public async getUpdatedFileContents(filePath: string, commitHash?: string): Promise<
⋮----
public async getContextFiles(includeRuleFiles = false): Promise<ContextFile[]>
⋮----
public async getRuleFilesAsContextFiles(profile?: AgentProfile, includeDisabled = false): Promise<ContextFile[]>
⋮----
// Get global rule files
⋮----
// Convert to relative path with ~/ prefix
⋮----
// Global rules directory doesn't exist or can't be read
⋮----
// Include AGENTS.md from project root if it exists
⋮----
// Get project rule files
⋮----
// Project rules directory doesn't exist or can't be read
⋮----
// Get agent profile rule files
⋮----
// Convert to relative path with ~/ prefix if in home directory
⋮----
// Rule file doesn't exist or can't be accessed
⋮----
// Dispatch extension event to allow modification of rule files
⋮----
// Filter out disabled rule files
⋮----
public getRepoMap(): string
⋮----
public updateRepoMapFromConnector(repoMap: string): void
⋮----
public openCommandOutput(command: string)
⋮----
public closeCommandOutput(addToContext = true)
⋮----
public addLogMessage(level: LogLevel, message?: string, finished = false, promptContext?: PromptContext, actionIds?: string[])
⋮----
public getContextMessages()
⋮----
public getSkillManager(): SkillManager
⋮----
public async getSkills(): Promise<Skill[]>
⋮----
public async createSkillMessages(...skillNames: string[]): Promise<ContextMessage[] | null>
⋮----
public async activateSkill(skillName: string): Promise<[ContextAssistantMessage, ContextToolMessage] | null>
⋮----
public async deactivateSkill(skillName: string): Promise<string[]>
⋮----
// Find the assistant message with the skill's tool-call
⋮----
// Find the tool message
⋮----
// Check if assistant message has other tool-calls besides this one
⋮----
// Has other tool-calls — only remove the skill tool-call part, keep the assistant message
⋮----
// No other tool-calls — remove the entire assistant message too
⋮----
public async addRoleContextMessage(role: MessageRole, content: string, usageReport?: UsageReportData)
⋮----
public async addContextMessage(message: ContextMessage, updateContextInfo = false)
⋮----
public sendAddMessage(role: MessageRole = MessageRole.User, content: string, acknowledge = true)
⋮----
public sendUserMessage(data: UserMessageData)
⋮----
public sendToolMessage(data: ToolData)
⋮----
public async clearContext(addToHistory = false, updateContextInfo = true, updateTaskState = true, createSnapshot = true)
⋮----
public async resetContext()
⋮----
sendContextInfoUpdated()
⋮----
async undoContextChange(): Promise<boolean>
⋮----
/**
   * Load context messages into the task context and send them to the UI.
   * This is used for loading pre-authored context (e.g., from extensions).
   */
public async loadContextMessages(messages: ContextMessage[])
⋮----
public async interruptResponse(interruptId?: string)
⋮----
// Check for subagent abort controller first
⋮----
// Check for conflict resolution agent
⋮----
// No specific interruptId - check for running subagents first
⋮----
// Cancel the first running subagent
⋮----
// Default behavior: interrupt main agent
⋮----
// cancel the task state determination and set the task to ready for review
⋮----
public registerSubagentAbortController(interruptId: string, abortController: AbortController): void
⋮----
public unregisterSubagentAbortController(interruptId: string): void
⋮----
public getQueuedPrompts(): QueuedPromptData[]
⋮----
public removeQueuedPrompt(promptId: string): void
⋮----
public reorderQueuedPrompts(prompts: QueuedPromptData[]): void
⋮----
public editQueuedPrompt(promptId: string, newText: string): void
⋮----
public async sendQueuedPromptNow(promptId: string): Promise<void>
⋮----
// interrupting to allow the next queued prompt to be sent
⋮----
public applyEdits(edits: FileEdit[])
⋮----
public addToolMessage(
    id: string,
    serverName: string,
    toolName: string,
    args?: unknown,
    response?: string,
    usageReport?: UsageReportData,
    promptContext?: PromptContext,
    saveToDb = true,
    finished = !!response,
)
⋮----
// Update total costs when adding the tool message
⋮----
private updateTotalCosts(usageReport: UsageReportData, promptContext?: PromptContext)
⋮----
// Subagent and other grouped runs use their own (different) model/context, so they must
// not override the main run's context size — only update the accumulated cost.
⋮----
addUserMessage(id: string, content: string, promptContext?: PromptContext, images?: string[])
⋮----
public async removeLastMessage()
⋮----
public async removeMessage(messageId: string): Promise<string[]>
⋮----
public async removeMessagesUpTo(messageId: string): Promise<string[]>
⋮----
public sendTaskMessageRemoved(messageIds: string[])
⋮----
public reloadGroupMessages(messages: ContextMessage[])
⋮----
public async redoUserPrompt(messageId: string, mode: Mode, updatedPrompt?: string, updatedImages?: string[])
⋮----
// No need to await runPrompt here, let it run in the background
⋮----
public async resumeTask()
⋮----
// In other modes, check if last message is user
⋮----
// Last message is from user, redo it
⋮----
// Last message is not from user, send "Continue" to aider
⋮----
private tryCustomCommand(prompt: string, mode: Mode): boolean
⋮----
private getCurrentMode()
⋮----
private async shouldStartAider(): Promise<boolean>
⋮----
private async reloadConnectorMessages()
⋮----
private findSkillActivationMessages(contextMessages: ContextMessage[]): ContextMessage[]
⋮----
// Collect all skill activations with their skill names
// We'll deduplicate by keeping only the most recent activation per skill
⋮----
// Collect skill activation tool calls from this message
⋮----
// Extract skill name from the tool call input
⋮----
// Find corresponding tool-result message
⋮----
// Find the tool result part
⋮----
// Store the activation, overwriting any previous one for the same skill
// This keeps the most recent activation for each skill
⋮----
// Convert the deduplicated activations to the expected message format
⋮----
// Create a filtered assistant message that only contains the skill activation tool-call
⋮----
public async smartCompactConversation(
    contextMessages?: ContextMessage[],
    infoMessage = 'Conversation smart-compacted.',
    options?: SmartCompactionOptions,
): Promise<ContextMessage[]>
⋮----
// backing up the current context before compacting for debugging purposes
⋮----
// Determine compaction level based on messages since last compaction
⋮----
// else: 4-5 messages since last compaction → keep current level
⋮----
public async compactConversation(
    mode: Mode,
    customInstructions?: string,
    profile: AgentProfile | null = null,
    contextMessages?: ContextMessage[],
    promptContext?: PromptContext,
    abortSignal?: AbortSignal,
    waitForAgentCompletion = true,
    loadingMessage = 'Compacting conversation...',
): Promise<void>
⋮----
// Get profile if not provided
⋮----
// Find skill activation messages before generating summary
⋮----
// Agent mode logic
⋮----
// Clear existing context and add the summary
⋮----
// Collect all new messages before setting the context
⋮----
// Create final messages array with skill activations inserted between user message and rest
⋮----
// Set all messages at once
⋮----
// Prevent memory leaks by cleaning up pending prompt resources
⋮----
public async handoffConversation(
    mode: Mode,
    focus: string = '',
    execute = false,
    contextMessages?: ContextMessage[],
    waitForAgentCompletion = true,
    loadingMessage = 'Preparing handoff...',
): Promise<void>
⋮----
// Get context messages
⋮----
// Get context files to transfer
⋮----
// Agent mode logic
⋮----
// Create new task without sending event (sendEvent flag defaults to true)
⋮----
// Get the newly created Task instance
⋮----
// Add prompt to new task
⋮----
// Transfer context files
⋮----
// Send task-created event to trigger activation and handoff
⋮----
public async generateContextMarkdown(): Promise<string | null>
⋮----
updateTokensInfo(data: Partial<TokensInfoData>)
⋮----
async updateContextInfo(checkContextFilesIncluded = false, checkRepoMapIncluded = false)
⋮----
private async sendRequestContextInfo()
⋮----
public async updateAutocompletionData(words?: string[], force = false, useGit = true)
⋮----
async updateAgentEstimatedTokens(checkContextFilesIncluded = false, checkRepoMapIncluded = false)
⋮----
public async getEstimatedTokens(): Promise<number>
⋮----
async settingsChanged(oldSettings: SettingsData, newSettings: SettingsData)
⋮----
// For old profile, we can't easily get it from old settings since they're now file-based
// We'll just use null for old profile comparison
// Note: agent profile changes are now handled differently since profiles are file-based
⋮----
// Check for changes in agent config properties that affect token count
const modelChanged = false; // oldAgentProfile is null, so no change
const disabledServersChanged = false; // oldAgentProfile is null, so no change
const toolApprovalsChanged = false; // oldAgentProfile is null, so no change
const includeContextFilesChanged = false; // oldAgentProfile is null, so no change
const includeRepoMapChanged = false; // oldAgentProfile is null, so no change
const useAiderToolsChanged = false; // oldAgentProfile is null, so no change
const usePowerToolsChanged = false; // oldAgentProfile is null, so no change
const customInstructionsChanged = false; // oldAgentProfile is null, so no change
⋮----
// Check for changes in Aider
⋮----
async modelsUpdated()
⋮----
async projectSettingsChanged(oldSettings: ProjectSettings, newSettings: ProjectSettings)
⋮----
private sendUpdateEnvVars(environmentVariables: Record<string, unknown>)
⋮----
private getTodoFilePath(): string
⋮----
public async readTodoFile(): Promise<
⋮----
public async writeTodoFile(data:
⋮----
public async getTodos(): Promise<TodoItem[]>
⋮----
public async setTodos(items: TodoItem[], initialUserPrompt = ''): Promise<void>
⋮----
public async addTodo(name: string): Promise<TodoItem[]>
⋮----
public async updateTodo(name: string, updates: Partial<TodoItem>): Promise<TodoItem[]>
⋮----
public async deleteTodo(name: string): Promise<TodoItem[]>
⋮----
public async clearAllTodos(): Promise<TodoItem[]>
⋮----
async initProjectAgentsFile(args?: string): Promise<void>
⋮----
// clear context before execution
⋮----
// Get the active agent profile
⋮----
// Run the agent with the modified profile, using init-project template as system prompt
⋮----
// Check if the AGENTS.md file was created
⋮----
private async runExtensionCommand(extensionCommand: RegisteredCommand, args: string[], mode: Mode, images?: string[]): Promise<void>
⋮----
// Images attached to the prompt are automatically included in the first prompt submitted
// by the extension (via runPrompt/runPromptInAgent), unless it passes images explicitly
⋮----
// Execute the command - extension is fully responsible for its logic
⋮----
takePendingCommandImages(): string[] | undefined
⋮----
public async runCustomCommand(commandName: string, args: string[], mode: Mode = 'agent', images?: string[]): Promise<void>
⋮----
// Queue the custom command for later execution
⋮----
// First, check if this is an extension command
⋮----
// Handle extension command execution
⋮----
// Fall back to file-based custom command
⋮----
// Handle shell command execution errors
⋮----
// Re-throw other errors
⋮----
// Agent mode logic
⋮----
// All other modes (code, ask, architect)
⋮----
// Clear loading message after execution completes (success or failure)
⋮----
async reset()
⋮----
async restartAiderConnector()
⋮----
public async updateTask(updates: Partial<TaskData>): Promise<TaskData>
⋮----
// Handle worktree configuration changes
⋮----
// Check if currentMode changed and start aider if new mode requires it
⋮----
// setting a name will also save the task
⋮----
// no need to save if nothing changed
⋮----
private async sendTaskGitStatusUpdated()
⋮----
// These methods are often invoked fire-and-forget (void); never let them produce unhandled rejections
⋮----
public async sendUpdatedFilesUpdated()
⋮----
public async sendSkillsUpdated(): Promise<void>
⋮----
private async initWorktree(): Promise<void>
⋮----
private async runWorktreePostCreateCommand(command: string): Promise<void>
⋮----
private async applyWorkingMode(mode: WorkingMode)
⋮----
// Only remove the worktree if no other tasks share it
⋮----
public async mergeWorktreeToMain(squash: boolean, targetBranch?: string, commitMessage?: string): Promise<void>
⋮----
// For squash merge, we need a commit message
⋮----
// Get changes information for AI generation
⋮----
// Try to generate commit message using AI
⋮----
// Fallback to task name if AI generation fails
⋮----
// Store merge state for potential revert
⋮----
public async switchToLocalWorkingMode(options?: SwitchToLocalOptions): Promise<void>
⋮----
// When the merge was checkoutless (project directory is on a different branch than the merge
// target), the uncommitted changes were left in the worktree. Carry them over to the project
// directory before the worktree is removed below, otherwise they would be destroyed with it.
⋮----
// Abort the switch so the worktree is preserved together with its uncommitted changes
⋮----
public async switchToWorktreeWorkingMode(options?: SwitchToWorktreeOptions): Promise<void>
⋮----
public async getLocalUncommittedFiles(): Promise<UncommittedFiles>
⋮----
public async applyUncommittedChanges(): Promise<void>
⋮----
public async mergeWorktreeToWorktree(targetWorktreeDir: string, includeUncommitted = false): Promise<void>
⋮----
public async revertLastMerge(): Promise<void>
⋮----
// Clear merge state after successful revert
⋮----
public async addFileToGit(filePath: string): Promise<void>
⋮----
public async restoreFile(filePath: string): Promise<void>
⋮----
public async generateCommitMessage(filePaths?: string[]): Promise<string>
⋮----
// Get last 10 commit messages for context
⋮----
// Continue without commit history
⋮----
public async commitChanges(message: string, amend: boolean, skipGitHooks: boolean, filePaths?: string[]): Promise<void>
⋮----
public cancelCommitChanges(): void
⋮----
public async getTaskGitStatus(targetBranch?: string): Promise<TaskGitStatus | null>
⋮----
public async rebaseWorktreeFromBranch(fromBranch?: string): Promise<void>
⋮----
// Remember the intended source branch before starting so the conflict-resolution
// flow (continue/abort rebase) can rely on it instead of guessing it from git state
⋮----
// The rebase never started, so the pending marker is no longer needed
⋮----
// The rebase never started, so the pending marker is no longer needed
⋮----
// keep the marker if we cannot determine the rebase state
⋮----
public async abortRebase(): Promise<void>
⋮----
public async renameBranch(newBranchName: string): Promise<void>
⋮----
public async listGitBranches(includeRemote?: boolean): Promise<BranchInfo[]>
⋮----
public async isGitRepository(): Promise<boolean>
⋮----
public async initializeGitRepository(): Promise<void>
⋮----
public async getSyncCommits(targetBranch?: string): Promise<GitSyncCommits>
⋮----
public async createGitBranch(name: string, startPoint?: string, checkout?: boolean): Promise<void>
⋮----
public async checkoutGitBranch(branch: string, createTracking?: boolean, takeOver?: boolean): Promise<void>
⋮----
public async deleteGitBranch(branch: string, force?: boolean): Promise<void>
⋮----
public async mergeIntoCurrentBranch(branch: string): Promise<
⋮----
public async rebaseOntoBranch(branch: string): Promise<
⋮----
public async updateGitBranch(branchName: string): Promise<
⋮----
public async gitPull(rebase?: boolean): Promise<
⋮----
public async gitPush(force?: boolean, setUpstream?: boolean): Promise<
⋮----
public async renameGitBranch(newBranchName: string): Promise<void>
⋮----
public async renameWorktreeBranch(newBranchName: string): Promise<void>
⋮----
private async runGitAction<T>(action: string, gitAction: () => Promise<T>, shouldSkipLogging?: (error: unknown) => boolean): Promise<T>
⋮----
private async executeConflictResolution(directoryPath: string, directoryName: string): Promise<void>
⋮----
// Create temp directory structure for conflict files
⋮----
// Create version files using the relative path structure
⋮----
// Write version files
⋮----
// Update context based on whether it was interrupted or resolved
⋮----
// Stage the file
⋮----
// Clean up temp files
⋮----
// Clean up abort controller
⋮----
private isConflictError(error: unknown): boolean
⋮----
public async resolveConflictsWithAgent(): Promise<void>
⋮----
// Check worktree first
⋮----
// Check main repository
⋮----
// No conflicts found
⋮----
public reportGitActionError(action: string, error: unknown): void
⋮----
public async resolveGitErrorWithAgent(): Promise<void>
⋮----
public async continueRebase(): Promise<void>
⋮----
// Clear any remaining merge state after successful rebase continuation
⋮----
// The 'Continuing rebase...' loading message must be closed explicitly,
// otherwise it stays visible forever when the rebase pauses on conflicts
⋮----
public async duplicateFrom(sourceTask: Task): Promise<void>
⋮----
// Copy basic task data
⋮----
// Copy context files
⋮----
// Copy messages
⋮----
// Copy todos
⋮----
// Copy worktree if exists
⋮----
public async forkFrom(sourceTask: Task, messageId: string): Promise<void>
⋮----
// Copy basic task data
⋮----
// Copy ALL context files from source task (not just up to fork point)
⋮----
// Get messages up to and including the specified message
⋮----
// Save forked messages into new task
⋮----
// Copy worktree if exists
⋮----
async agentProfileUpdated(oldProfile: AgentProfile, newProfile: AgentProfile)
⋮----
public getProject(): Project
⋮----
public isInitialized()
⋮----
public async generateText(modelId: string, systemPrompt: string, prompt: string): Promise<string | undefined>
⋮----
public async generateObject<T>(modelId: string, systemPrompt: string, prompt: string, schema: z.ZodType<T>): Promise<T | undefined>
⋮----
async runCodeChangeRequests(requests: ChangeRequestItem[], contextSize: number = 5, createNewTask?: boolean): Promise<void>

================
File: src/main/telemetry/index.ts
================


================
File: src/main/telemetry/langfuse.ts
================
import { LangfuseExporter } from 'langfuse-vercel';
⋮----
import type { SpanExporter } from '@opentelemetry/sdk-trace-base';
import type { SettingsData } from '@common/types';
⋮----
import { getEffectiveEnvironmentVariable } from '@/utils/environment';
import logger from '@/logger';
⋮----
export const initializeLangfuseExporter = (): SpanExporter | undefined =>
⋮----
export const getLangfuseEnvironmentVariables = (baseDir: string, settings: SettingsData): Record<string, unknown> =>

================
File: src/main/telemetry/open-telemetry.ts
================
import { registerTelemetry } from 'ai';
import { OpenTelemetry } from '@ai-sdk/otel';
import { NodeSDK } from '@opentelemetry/sdk-node';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
⋮----
import { initializeLangfuseExporter } from './langfuse';
import { initializePostHogExporter } from './posthog';
⋮----
import type { SpanExporter } from '@opentelemetry/sdk-trace-base';
⋮----
import logger from '@/logger';
⋮----
// Graceful shutdown

================
File: src/main/telemetry/posthog.ts
================
import { PostHogTraceExporter } from '@posthog/ai/otel';
⋮----
import type { SpanExporter } from '@opentelemetry/sdk-trace-base';
import type { SettingsData } from '@common/types';
⋮----
import { getEffectiveEnvironmentVariable } from '@/utils/environment';
import logger from '@/logger';
⋮----
export const initializePostHogExporter = (): SpanExporter | undefined =>
⋮----
export const getPostHogAiderEnvironmentVariables = (baseDir: string, settings: SettingsData): Record<string, unknown> =>

================
File: src/main/telemetry/telemetry-manager.ts
================
import { PostHog } from 'posthog-node';
import { AgentProfile, Mode, SettingsData, TaskData } from '@common/types';
⋮----
import { Store } from '@/store';
import logger from '@/logger';
import { POSTHOG_PUBLIC_API_KEY, POSTHOG_HOST, APP_TYPE, PRODUCT_TELEMETRY_DISTINCT_ID } from '@/constants';
import { getElectronApp } from '@/app';
⋮----
export class TelemetryManager
⋮----
constructor(store: Store)
⋮----
settingsChanged(oldSettings: SettingsData, newSettings: SettingsData)
⋮----
async init(): Promise<void>
⋮----
async destroy(): Promise<void>
⋮----
captureProjectOpened(openedProjectsCount: number)
⋮----
captureProjectClosed(closedProjectsCount: number)
⋮----
captureRunPrompt(mode?: Mode)
⋮----
captureAgentRun(profile: AgentProfile, task?: TaskData, totalMcpServersCount = 0)
⋮----
captureCustomCommand(commandName: string, argsCount: number, mode: Mode)
⋮----
captureTerminalCreated()
⋮----
captureExtensionsLoaded(totalExtensions: number, globalExtensions: number, projectExtensions: number, enabledExtensions: number, disabledExtensions: number)
⋮----
captureExtensionInstalled(extensionName: string, location: 'global' | 'project')
⋮----
captureExtensionUninstalled(extensionName: string, location: 'global' | 'project')

================
File: src/main/terminal/index.ts
================


================
File: src/main/terminal/terminal-manager.ts
================
import { v4 as uuidv4 } from 'uuid';
⋮----
import logger from '@/logger';
import { TelemetryManager } from '@/telemetry';
import { EventManager } from '@/events';
⋮----
export interface TerminalInstance {
  id: string;
  baseDir: string;
  taskId: string;
  ptyProcess: pty.IPty;
  cols: number;
  rows: number;
  outputBuffer: string;
}
⋮----
export class TerminalManager
⋮----
constructor(
⋮----
private getShellCommand(): string
⋮----
// Try PowerShell first, fallback to cmd
⋮----
// Unix-like systems (Linux, macOS)
⋮----
private getShellArgs(): string[]
⋮----
public async createTerminal(baseDir: string, taskDir: string, taskId: string, cols: number = 80, rows: number = 24): Promise<string>
⋮----
// Handle data from terminal
⋮----
// Handle terminal exit
⋮----
// Capture telemetry event for terminal creation
⋮----
public writeToTerminal(terminalId: string, data: string): boolean
⋮----
public resizeTerminal(terminalId: string, cols: number, rows: number): boolean
⋮----
public closeTerminal(terminalId: string): boolean
⋮----
public getTerminalBuffer(terminalId: string):
⋮----
public closeTerminalForProject(baseDir: string): void
⋮----
public closeTerminalsForTask(taskId: string): void
⋮----
public getTerminalForTask(taskId: string): TerminalInstance | undefined
⋮----
public getTerminalsForTask(taskId: string): TerminalInstance[]
⋮----
public close(): void

================
File: src/main/utils/atomic-write.ts
================
export const atomicWriteJsonFile = async (filePath: string, data: unknown): Promise<void> =>
⋮----
export const cleanupStaleTempFiles = async (filePath: string): Promise<void> =>

================
File: src/main/utils/environment.ts
================
import fs from 'fs';
import path from 'path';
⋮----
import { parse } from '@dotenvx/dotenvx';
import YAML from 'yaml';
import { DEFAULT_AGENT_PROFILE, DEFAULT_AIDER_MAIN_MODEL, DEFAULT_PROVIDER_MODELS } from '@common/agent';
import { EnvironmentVariable, Model, ProjectSettings, ProviderProfile, SettingsData } from '@common/types';
⋮----
import logger from '@/logger';
import { getLangfuseEnvironmentVariables, getPostHogAiderEnvironmentVariables } from '@/telemetry';
import { Store } from '@/store';
⋮----
const readEnvFile = (filePath: string): Record<string, string> | null =>
⋮----
const readPropertyFromConfFile = (filePath: string, property: string): string | undefined =>
⋮----
const readApiKeyFromConfFile = (filePath: string, envVarName: string): string | undefined =>
⋮----
// Map environment variable names to provider names used in api-key property
// GOOGLE_API_KEY maps to 'gemini' in the api-key property
⋮----
// Normalize apiKeyValue to array for unified processing
⋮----
export const getEffectiveEnvironmentVariable = (key: string, settings?: SettingsData, projectDir?: string): EnvironmentVariable | undefined =>
⋮----
// 1. From settings.aider.environmentVariables
⋮----
// 2. From --env-file in settings.aider.options
⋮----
// 3. from `env-file` in $projectDir/.aider.conf.yml
⋮----
// 4. from $projectDir/.env
⋮----
// 5. from api-key property in $projectDir/.aider.conf.yml
⋮----
// 6. from kebab-case property in $projectDir/.aider.conf.yml
⋮----
// Home dir related checks
⋮----
// 7. from `env-file` in $HOME/.aider.conf.yml
⋮----
// 8. from $HOME/.env
⋮----
// 9. from api-key property in $HOME/.aider.conf.yml
⋮----
// 10. from kebab-case property in $HOME/.aider.conf.yml
⋮----
// 11. From process.env
⋮----
// Not found
⋮----
export const parseAiderEnv = (settings: SettingsData): Record<string, string> =>
⋮----
// Parse Aider environment variables from settings
⋮----
// Check for --env or --env-file in aider options
⋮----
...aiderEnvVars, // Start with settings env
...(fileEnv ?? {}), // Override with file env if it exists
⋮----
export const readAiderConfProperty = (baseDir: string, property: string): string | undefined =>
⋮----
export const determineWeakModel = (baseDir: string): string | undefined =>
⋮----
export const determineMainModel = (settings: SettingsData, providers: ProviderProfile[], providerModels: Model[], baseDir: string): string =>
⋮----
// Check for --model in aider options
⋮----
// Default model if no other condition is met
⋮----
export const getEnvironmentVariablesForAider = (settings: SettingsData, baseDir: string): Record<string, unknown> =>
⋮----
const getTelemetryEnvironmentVariablesForAider = (settings: SettingsData, baseDir: string): Record<string, unknown> =>
⋮----
export const getDefaultProjectSettings = (
  store: Store,
  providerModels: Model[],
  baseDir: string,
  defaultAgentProfileId = DEFAULT_AGENT_PROFILE.id,
  providers?: ProviderProfile[],
): ProjectSettings =>

================
File: src/main/utils/errors.ts
================
/**
 * Utility functions for error handling
 */
⋮----
/**
 * Checks if an error is an AbortError (operation was cancelled)
 * In Node.js, AbortError has name === 'AbortError' and code === 'ABORT_ERR'
 */
export const isAbortError = (error: unknown): boolean =>
⋮----
/**
 * Checks if an error is a file not found error (ENOENT)
 */
export const isFileNotFoundError = (error: unknown): boolean =>

================
File: src/main/utils/file-system.ts
================
import { simpleGit } from 'simple-git';
import filenamifyImport from 'filenamify';
import slugify from 'slugify';
import { glob } from 'glob';
⋮----
import logger from '@/logger';
import { AIDER_DESK_PROJECTS_DIR } from '@/constants';
// @ts-expect-error filenamify is not typed properly
⋮----
export const getFilePathSuggestions = async (currentPath: string, directoriesOnly = false): Promise<string[]> =>
⋮----
// Extract directory and search pattern
⋮----
// Fallback to parent directory if current doesn't exist
⋮----
// Ensure dirPath is a directory
⋮----
// Get directory contents
⋮----
// Filter entries based on type and search pattern
⋮----
export const isProjectPath = async (path: string): Promise<boolean> =>
⋮----
const getRepoNameFromPath = (repoPath: string): string | null =>
⋮----
export const parseRepositoryUrl = (repositoryUrl: string):
⋮----
export const cloneProjectRepository = async (repositoryUrl: string, targetDir?: string, signal?: AbortSignal): Promise<string> =>
⋮----
export const isValidPath = async (baseDir: string, filePath: string): Promise<boolean> =>
⋮----
export const isDirectory = async (path: string): Promise<boolean> =>
⋮----
export const isFileIgnored = async (projectBaseDir: string, filePath: string): Promise<boolean> =>
⋮----
// Make the path relative to the base directory for git check-ignore
⋮----
export const filterIgnoredFiles = async (projectBaseDir: string, filePaths: string[]): Promise<string[]> =>
⋮----
// Convert all file paths to relative paths for git check-ignore
⋮----
// Process files in chunks of max 100
⋮----
// Return only files that are not ignored
⋮----
// Return all files if git check fails (safer default)
⋮----
const isGitRepository = async (baseDir: string): Promise<boolean> =>
⋮----
const getAllFilesFromFS = async (baseDir: string, ignore: string[] = []): Promise<string[]> =>
⋮----
// Convert to relative paths
⋮----
const getIgnoreGlobPatterns = async (gitignorePath: string): Promise<string[]> =>
⋮----
// ignore
⋮----
export const getAllFiles = async (baseDir: string, useGit = true): Promise<string[]> =>
⋮----
// Try to use git first
⋮----
// use filesystem when useGit is false or git fails
⋮----
// If it's a git repo, filter out ignored files
⋮----
export const deriveDirName = (name: string, existingDirNames: Set<string>): string =>
⋮----
// First slugify the name to create a clean URL-friendly slug
⋮----
strict: true, // remove special characters except for hyphens and underscores
trim: true, // trim leading and trailing whitespace
⋮----
// Then filenamify to ensure it's safe as a directory name across all platforms
⋮----
replacement: '-', // use hyphens as replacement for invalid characters
maxLength: 100, // reasonable length limit
⋮----
/**
 * Validate if a file path is potentially valid for the current project
 *
 * @param filePath - File path to validate
 * @param taskDir - Task directory to check against
 * @returns True if file path could be valid for project
 */
export const isValidProjectFile = (filePath: string, taskDir: string): boolean =>
⋮----
// Convert to absolute path for validation
⋮----
// Check if resolved path is still within task directory

================
File: src/main/utils/file-watch.ts
================
import { execSync } from 'child_process';
⋮----
import { FileWatchMode } from '@common/types';
⋮----
import logger from '@/logger';
⋮----
const isDocker = (): boolean =>
⋮----
const isNetworkDrive = (drivePath: string): boolean =>
⋮----
export const shouldUsePolling = (watchPath: string, mode: FileWatchMode = FileWatchMode.Auto): boolean =>
⋮----
// FileWatchMode.Auto

================
File: src/main/utils/http.ts
================
export const fetchWithTimeout = async (url: string, timeoutMs: number = DEFAULT_FETCH_TIMEOUT_MS): Promise<Response> =>

================
File: src/main/utils/i18n.ts
================
import en from '@common/locales/en.json';
import ko from '@common/locales/ko.json';
import ru from '@common/locales/ru.json';
import zh from '@common/locales/zh.json';
⋮----
const getNested = (obj: Record<string, unknown>, key: string): unknown =>
⋮----
const interpolate = (template: string, params?: Record<string, string>): string =>
⋮----
export const translate = (language: string | undefined, key: string, params?: Record<string, string>): string =>

================
File: src/main/utils/index.ts
================


================
File: src/main/utils/mutex.ts
================
import logger from '@/logger';
⋮----
/**
 * A simple async mutex implementation for preventing race conditions
 * in critical sections of code. Supports named locks and timeouts.
 */
export class Mutex
⋮----
private defaultTimeout = 30000; // 30 seconds
⋮----
/**
   * Acquire a lock for the given resource name
   * @param resourceName - Unique name for the resource to lock
   * @param timeout - Optional timeout in milliseconds (default: 30 seconds)
   * @returns Promise<() => void> - Release function to unlock the resource
   */
async acquire(resourceName: string, timeout: number = this.defaultTimeout): Promise<() => void>
⋮----
// If there's already a lock for this resource, wait for it
⋮----
// Create a new lock promise
⋮----
// Store the lock
⋮----
// Return the release function
⋮----
/**
   * Execute a function with a mutex lock
   * @param resourceName - Unique name for the resource to lock
   * @param fn - Function to execute while holding the lock
   * @param timeout - Optional timeout in milliseconds
   * @returns Promise<T> - Result of the function execution
   */
async withLock<T>(resourceName: string, fn: () => Promise<T> | T, timeout?: number): Promise<T>
⋮----
/**
   * Check if a resource is currently locked
   * @param resourceName - Name of the resource to check
   * @returns boolean - True if the resource is locked
   */
isLocked(resourceName: string): boolean
⋮----
/**
   * Get the current number of active locks
   * @returns number - Number of active locks
   */
getActiveLockCount(): number
⋮----
/**
   * Get all currently locked resource names
   * @returns string[] - Array of locked resource names
   */
getLockedResources(): string[]
⋮----
/**
   * Force release all locks (use with caution)
   */
releaseAll(): void
⋮----
// Global mutex instance for the application
⋮----
/**
 * Convenience function to execute code with a named lock
 * @param resourceName - Unique name for the resource to lock
 * @param fn - Function to execute while holding the lock
 * @param timeout - Optional timeout in milliseconds
 * @returns Promise<T> - Result of the function execution
 */
export const withLock = async <T>(resourceName: string, fn: ()
⋮----
/**
 * Convenience function to acquire a named lock
 * @param resourceName - Unique name for the resource to lock
 * @param timeout - Optional timeout in milliseconds
 * @returns Promise<() => void> - Release function to unlock the resource
 */
export const acquireLock = async (resourceName: string, timeout?: number): Promise<()
⋮----
/**
 * Check if a resource is currently locked
 * @param resourceName - Name of the resource to check
 * @returns boolean - True if the resource is locked
 */
export const isLocked = (resourceName: string): boolean

================
File: src/main/utils/open-url.ts
================
import { execSync } from 'child_process';
import { join } from 'path';
import os from 'os';
⋮----
import icon from '../../../resources/icon.png?asset';
⋮----
import { isElectron } from '@/app';
import logger from '@/logger';
⋮----
/**
 * Opens a URL either in external browser or a new BrowserWindow.
 * In Node/Docker environments, 'window' falls back to external with a warning.
 *
 * @param url - URL to open
 * @param target - Where to open: 'external' (system browser) or 'window' (new Electron window)
 * @param title - Window title (only used when opening in a new window)
 */
export const openUrl = async (url: string, target: 'external' | 'window' = 'window', title?: string): Promise<Electron.BrowserWindow | null> =>
⋮----
// If URL starts with #/, it's an internal app route
⋮----
/**
 * Opens a URL in the system's default browser using platform-specific commands.
 * Works in Node.js environments without Electron.
 */
const openInExternalBrowser = (url: string): void =>

================
File: src/main/utils/probe.ts
================
import { PROBE_BINARY_PATH } from '@/constants';
import { execWithShellPath } from '@/utils/shell';
⋮----
interface ProbeSearchOptions {
  query: string;
  path?: string;
  json?: boolean;
  filesOnly?: boolean;
  ignore?: string;
  excludeFilenames?: string;
  reranker?: string;
  frequencySearch?: boolean;
  exact?: boolean;
  strictElasticSyntax?: boolean;
  maxResults?: number;
  maxBytes?: number;
  maxTokens?: number;
  allowTests?: boolean;
  noMerge?: boolean;
  mergeThreshold?: number;
  session?: string;
  timeout?: number;
  language?: string;
  format?: string;
}
⋮----
export const search = async (options: ProbeSearchOptions): Promise<string | unknown> =>

================
File: src/main/utils/python.ts
================
import { exec } from 'child_process';
⋮----
import { promisify } from 'util';
⋮----
import { AIDER_DESK_BIN_DIR, PYTHON_COMMAND, PYTHON_VENV_DIR, UV_EXECUTABLE } from '@/constants';
import logger from '@/logger';
⋮----
interface GetResolvedUvPathOptions {
  onDownloadStarted?: () => void;
}
⋮----
/**
 * Resolves and returns the path to a working uv executable.
 *
 * Resolution order:
 *  1. Previously cached result (immediate)
 *  2. Local bin directory download (UV_EXECUTABLE)
 *  3. System-installed uv (unless AIDER_DESK_DISABLE_SYSTEM_UV is set)
 *  4. Download from GitHub releases
 *
 * The in-flight promise is cached so that concurrent callers share the same
 * resolution — no duplicate downloads or checks.
 */
export const getResolvedUvPath = async (options?: GetResolvedUvPathOptions): Promise<string> =>
⋮----
// Return cached result immediately
⋮----
// If a resolution is already in flight, reuse its promise
⋮----
// Reset on failure so callers can retry
⋮----
const resolveUvPath = async (options?: GetResolvedUvPathOptions) =>
⋮----
// 1. Check local bin directory
⋮----
// 2. Check for system uv (unless disabled via env var)
⋮----
// 3. Download uv
⋮----
const findSystemUv = async (): Promise<string | null> =>
⋮----
const isUvWorking = async (uvPath: string): Promise<boolean> =>
⋮----
const downloadUv = async (): Promise<void> =>
⋮----
export const getPythonVenvBinPath = (): string =>
⋮----
/**
 * Gets the currently installed version of a Python library within the virtual environment.
 */
export const getCurrentPythonLibVersion = async (library: string): Promise<string | null> =>
⋮----
/**
 * Gets the latest available version of a Python library from the pip index.
 */
export const getLatestPythonLibVersion = async (library: string): Promise<string | null> =>

================
File: src/main/utils/ripgrep.ts
================
import fs from 'fs';
import path from 'path';
import { execSync } from 'child_process';
⋮----
import AdmZip from 'adm-zip';
⋮----
import logger from '@/logger';
import { AIDER_DESK_BIN_DIR, RIPGREP_BINARY_PATH } from '@/constants';
⋮----
const getRipgrepAssetName = (): string =>
⋮----
// linux - use musl for broader compatibility
⋮----
const isRipgrepAvailable = (): boolean =>
⋮----
const downloadRipgrep = async (): Promise<void> =>
⋮----
export const ensureRipgrepBinary = async (): Promise<boolean> =>

================
File: src/main/utils/shell.ts
================
import { exec, execSync } from 'child_process';
⋮----
import { promisify } from 'util';
⋮----
import logger from '@/logger';
import { getElectronApp } from '@/app';
⋮----
interface ShellInfo {
  path: string;
  name: string;
  args?: string[];
}
⋮----
class ShellDetector
⋮----
/**
   * Get the user's default shell
   * @param forceRefresh Force re-detection instead of using cache
   * @returns Shell information including path and name
   */
static getDefaultShell(forceRefresh = false): ShellInfo
⋮----
private static detectShell(): ShellInfo
⋮----
private static detectWindowsShell(): ShellInfo
⋮----
// Check for PowerShell Core first
⋮----
// Check for Windows PowerShell
⋮----
// Fall back to cmd.exe
⋮----
// Last resort
⋮----
private static detectUnixShell(): ShellInfo
⋮----
// First, try the SHELL environment variable
⋮----
// On macOS, try to get the default shell from Directory Services
⋮----
// Ignore errors and continue with fallback detection
⋮----
// Try to read from /etc/passwd
⋮----
// Ignore errors and continue with fallback detection
⋮----
// Try common shell paths in order of preference
⋮----
// Last resort - use sh
⋮----
private static findExecutable(name: string): string | null
⋮----
// Not executable, continue searching
⋮----
private static getShellArgs(shellName: string): string[]
⋮----
// Return appropriate arguments for interactive shell sessions
⋮----
return ['-i']; // Interactive mode
⋮----
return ['-NoExit']; // Keep PowerShell open
⋮----
/**
   * Get shell-specific command execution arguments
   * @param command The command to execute
   * @returns Array of arguments to pass to spawn/exec
   */
static getShellCommandArgs(command: string):
⋮----
// Unix shells
⋮----
/**
   * Check if a shell exists at the given path
   * @param shellPath Path to the shell executable
   * @returns true if the shell exists and is executable
   */
static isShellAvailable(shellPath: string): boolean
⋮----
/**
 * Get the path separator for the current platform
 */
const getPathSeparator = (): string
⋮----
/**
 * Extract the PATH value from shell command output by taking the last
 * non-empty line. This protects against login messages (MOTD, quota warnings,
 * etc.) that may be printed to stdout during shell initialization.
 */
export const extractPathFromOutput = (output: string): string =>
⋮----
/**
 * Get the user's shell PATH by executing their shell
 */
export const getShellPath = (): string =>
⋮----
// In packaged apps, always refresh PATH on first call to avoid cached restricted PATH
⋮----
// On Windows, use cmd.exe to get PATH
⋮----
// Also try to get PATH from PowerShell for more complete results
⋮----
// Combine both paths
⋮----
// PowerShell might not be available, continue with cmd.exe result
⋮----
// Unix/macOS logic - use ShellDetector to get the actual shell
⋮----
// For Linux, avoid slow interactive shell startup
// Use non-interactive mode for better performance
⋮----
? `${shell} -c 'echo $PATH'` // Fast non-interactive mode for Linux
: `${shell} -l -i -c 'echo $PATH'`; // Keep login shell for macOS
⋮----
// Execute the command to get the PATH
// For packaged apps, ALWAYS use login shell to get the user's real PATH
⋮----
// Use minimal base PATH - just enough to find the shell
⋮----
// Use login shell to load user's full environment
⋮----
// First try with explicit sourcing of shell config files
⋮----
timeout: isLinux ? 3000 : 10000, // Shorter timeout for Linux
⋮----
// Add ZDOTDIR for zsh users who might have custom config location
⋮----
// Try the standard login shell approach
⋮----
timeout: isLinux ? 3000 : 10000, // Shorter timeout for Linux
⋮----
// Fallback to current PATH + common locations
⋮----
// In development, try faster approach first
⋮----
timeout: isLinux ? 3000 : 10000, // Shorter timeout for Linux
⋮----
// Combine with current process PATH to ensure we don't lose anything
⋮----
// Also include npm global bin directories
⋮----
// Skip npm/yarn checks on Linux for better performance (they're usually in PATH already)
⋮----
// Try to get npm global bin directory
⋮----
// Ignore npm bin errors
⋮----
// Try to get yarn global bin directory
⋮----
// Ignore yarn bin errors
⋮----
// Windows-specific paths
⋮----
// Check for nvm-windows
⋮----
// Check for nvm-windows symlink
⋮----
// Unix/macOS-specific paths
⋮----
// Linux-specific common paths
⋮----
// Only add Linux paths that exist and aren't already in PATH
⋮----
// Check for nvm directories - look for all versions
⋮----
// Ignore nvm directory read errors
⋮----
// Try alternative method: read shell config files directly (Unix/macOS only)
⋮----
// Look for PATH exports
⋮----
// Expand $PATH references
⋮----
// Final fallback to process PATH
⋮----
/**
 * Find an executable in the shell PATH
 */
export const findExecutableInPath = (executable: string, checkOnly = false): string | null =>
⋮----
// On Windows, executables might have .exe, .cmd, or .bat extensions
⋮----
// On Windows, check if file exists
⋮----
// On Unix, check if the executable exists and is executable
⋮----
// Not found in this directory
⋮----
/**
 * Get a shell initialization prefix that sources the user's shell config files
 * and activates direnv if available. This ensures the bash tool has access to
 * the same environment (env vars, shell functions, direnv) as an interactive
 * terminal session.
 *
 * Only applies to Unix shells (bash, zsh, fish). Returns empty string for
 * Windows shells or unsupported shells.
 *
 * @returns Shell command prefix string to prepend before the actual command
 */
export const getShellInitCommand = (): string =>
⋮----
/**
 * Get shell and arguments to execute a command using the user's default shell.
 * This respects the $SHELL environment variable instead of always using /bin/sh.
 * @param command The command string to execute
 * @returns Object with shell path and args array (e.g. ['-c', command])
 */
export const getShellCommandArgs = (command: string):
⋮----
/**
 * Initialize the process PATH by loading it from the user's login shell.
 * Uses non-interactive mode (-lc) to avoid triggering prompts from interactive configs (e.g. .bashrc).
 */
export const initPath = (): void =>
⋮----
// Wrapper for execAsync that includes enhanced PATH
export const execWithShellPath = async (
  command: string,
  options?: { cwd?: string; env?: NodeJS.ProcessEnv; maxBuffer?: number; signal?: AbortSignal; killSignal?: NodeJS.Signals | number },
): Promise<

================
File: src/main/utils/web-scrapper.ts
================
import Turndown from 'turndown';
⋮----
import type { BrowserWindow as BrowserWindowType } from 'electron';
⋮----
import { isAbortError } from '@/utils/errors';
import { isElectron } from '@/app';
⋮----
type WebScrapeFormat = 'markdown' | 'html' | 'raw';
⋮----
export class WebScraper
⋮----
async scrape(url: string, timeout: number = 60000, abortSignal?: AbortSignal, format: WebScrapeFormat = 'markdown'): Promise<string>
⋮----
private async scrapeWithBrowserWindow(
    url: string,
    timeout: number = 60000,
    abortSignal?: AbortSignal,
    format: WebScrapeFormat = 'markdown',
): Promise<string>
⋮----
// eslint-disable-next-line @typescript-eslint/no-require-imports
⋮----
// Create hidden BrowserWindow for scraping
⋮----
// Create timeout promise
⋮----
// Create abort promise if signal is provided
⋮----
// Load the URL with timeout and abort signal
⋮----
// Wait for page to load completely with timeout and abort signal
⋮----
// Get page content with timeout and abort signal
⋮----
// Get content type from headers with timeout and abort signal
⋮----
// If it's HTML, convert to markdown-like text or return HTML based on format
⋮----
// Cleanup window
⋮----
private async waitForPageLoad(window: BrowserWindowType): Promise<void>
⋮----
const checkLoadState = () =>
⋮----
// Additional wait for dynamic content to load
⋮----
private async getContentType(window: BrowserWindowType): Promise<string>
⋮----
private async cleanupWindow(window: BrowserWindowType): Promise<void>
⋮----
private async scrapeWithFetch(url: string, timeout: number = 60000, abortSignal?: AbortSignal): Promise<string>
⋮----
// Return the bounded text content for HTML and other content types
⋮----
private async readBodyWithLimit(response: Response, maxBytes: number): Promise<string>
⋮----
// no streaming body available - fall back to full text with post-hoc cap
⋮----
private looksLikeHTML(content: string): boolean
⋮----
private cleanHtml(content: string): string
⋮----
// Remove comments
⋮----
private htmlToMarkDown(content: string): string
⋮----
export const scrapeWeb = async (url: string, timeout: number = 60000, abortSignal?: AbortSignal, format: WebScrapeFormat = 'markdown') =>

================
File: src/main/utils/window-load-retry.ts
================
import { delay } from '@common/utils';
⋮----
import logger from '@/logger';
⋮----
// Chromium network error codes that indicate transient failures where a retry can succeed
enum TransientLoadErrorCode {
  NetworkChanged = -21,
  ConnectionReset = -101,
  ConnectionRefused = -102,
  NameNotResolved = -105,
  InternetDisconnected = -106,
  TimedOut = -118,
}
⋮----
interface LoadRetryOptions {
  load: () => void | Promise<void>;
  isDestroyed: () => boolean;
  maxRetries?: number;
  baseDelayMs?: number;
}
⋮----
export interface LoadRetryHandler {
  onDidFailLoad: (errorCode: number, isMainFrame: boolean, validatedURL?: string) => Promise<void>;
  reset: () => void;
}
⋮----
export const createLoadRetryHandler = (
⋮----
const onDidFailLoad = async (errorCode: number, isMainFrame: boolean, validatedURL?: string): Promise<void> =>

================
File: src/main/versions/index.ts
================


================
File: src/main/versions/versions-manager.ts
================
import path from 'path';
⋮----
import { VersionsInfo } from '@common/types';
⋮----
import type { PythonDependenciesInstaller } from '@/python-dependencies-installer';
⋮----
import { getCurrentPythonLibVersion, getLatestPythonLibVersion } from '@/utils';
import logger from '@/logger';
import { Store } from '@/store';
import { EventManager } from '@/events';
import { getElectronApp, isDev } from '@/app';
⋮----
export class VersionsManager
⋮----
private readonly checkInterval = 10 * 60 * 1000; // 10 minutes
⋮----
constructor(
    private readonly eventManager: EventManager,
    private readonly store: Store,
    private readonly pythonInstaller?: PythonDependenciesInstaller,
)
⋮----
async getVersions(forceRefresh = false): Promise<VersionsInfo>
⋮----
// Get AiderDesk version using app.getVersion()
⋮----
// eslint-disable-next-line @typescript-eslint/no-require-imports
⋮----
// Wait for Python venv to be ready before checking library versions
⋮----
// Get current and available Aider versions using utility functions
⋮----
// Don't show error box for this common case
⋮----
// Check if auto-update is enabled and a new version was found
⋮----
return this.versionsInfo!; // versionsInfo is guaranteed to be non-null after updateVersionsInfo
⋮----
private updateVersionsInfo(partialInfo: Partial<VersionsInfo>): void
⋮----
private async init(): Promise<void>
⋮----
// Wait for Python venv to be ready before checking library versions
⋮----
// eslint-disable-next-line @typescript-eslint/no-require-imports
⋮----
autoUpdater.autoInstallOnAppQuit = true; // Install on quit after download
⋮----
// Schedule periodic checks
⋮----
// Initial check
⋮----
public destroy(): void
⋮----
public async downloadLatestAiderDesk(): Promise<void>
⋮----
// eslint-disable-next-line @typescript-eslint/no-require-imports
⋮----
// Check for updates first to ensure we have the latest info
⋮----
// Set autoDownload to true temporarily for this download action
⋮----
// Reset autoDownload back to false after initiating
⋮----
autoUpdater.autoDownload = false; // Ensure it's reset on error

================
File: src/main/app.ts
================
import type { App } from 'electron';
⋮----
export const isElectron = (): boolean =>
⋮----
export const getElectronApp = (): App | null =>
⋮----
// eslint-disable-next-line @typescript-eslint/no-require-imports
⋮----
export const isDev = (): boolean =>
⋮----
// eslint-disable-next-line @typescript-eslint/no-require-imports

================
File: src/main/constants.ts
================
import path from 'path';
import { homedir } from 'os';
⋮----
import { getDataDir, getResourceDir } from './paths';
⋮----
// constants for project directory files

================
File: src/main/events-handler.ts
================
import path from 'path';
import fs from 'fs/promises';
⋮----
import {
  AgentProfile,
  AutonomyMode,
  BranchInfo,
  CloudflareTunnelStatus,
  CommandsData,
  CreateTaskParams,
  EditFormat,
  EnvironmentVariable,
  FileEdit,
  GitSyncCommits,
  InstalledExtension,
  ExtensionToolInfo,
  McpOAuthStatusData,
  McpServerConfig,
  McpServersData,
  McpTool,
  MemoryEntry,
  Mode,
  Model,
  OpenDialogOptions,
  OpenDialogResult,
  OS,
  ProjectData,
  ProjectSettings,
  ProviderModelsData,
  ProviderProfile,
  QueuedPromptData,
  ResponseCompletedData,
  SettingsData,
  SystemLogLevel,
  SystemLogsResponse,
  TaskData,
  TaskStateData,
  TodoItem,
  UpdatedFile,
  UsageDataRow,
  VersionsInfo,
  VoiceSession,
  ChangeRequestItem,
} from '@common/types';
import { compareBaseDirs, normalizeBaseDir } from '@common/utils';
// @ts-expect-error istextorbinary is not typed properly
import { isBinary } from 'istextorbinary';
⋮----
import type { ModeDefinition, ExtensionConfigComponent, ExtensionOperationResult, ExtensionUIComponent } from '@common/types';
import type { WindowManager } from '@/window-manager';
import type { GitAskpassManager } from '@/git/git-askpass-manager';
⋮----
import { McpManager, McpConfigManager, AgentProfileManager } from '@/agent';
import { MemoryManager } from '@/memory/memory-manager';
import { ModelManager } from '@/models';
import { Project, ProjectManager } from '@/project';
import { CloudflareTunnelManager } from '@/server';
import { Store } from '@/store';
import { TelemetryManager } from '@/telemetry';
import { VersionsManager } from '@/versions';
import { DataManager } from '@/data-manager';
import { TerminalManager } from '@/terminal/terminal-manager';
import { ExtensionManager, LoadedExtension } from '@/extensions';
import { PromptsManager } from '@/prompts';
import logger, { eventTransport } from '@/logger';
import {
  cloneProjectRepository,
  getDefaultProjectSettings,
  getEffectiveEnvironmentVariable,
  getFilePathSuggestions,
  isProjectPath,
  isValidPath,
  scrapeWeb,
  openUrl,
} from '@/utils';
import { expandTilde } from '@/agent/utils';
import { AIDER_DESK_TMP_DIR, LOGS_DIR } from '@/constants';
import { EventManager } from '@/events';
import { isElectron } from '@/app';
import { NetworkManager } from '@/network-manager';
⋮----
export class EventsHandler
⋮----
constructor(
⋮----
loadSettings(): SettingsData
⋮----
getEventManager(): EventManager
⋮----
async saveSettings(newSettings: SettingsData): Promise<SettingsData>
⋮----
// Re-initialize network settings (proxy/TLS rules) if changed
⋮----
getProjectSettings(baseDir: string): ProjectSettings
⋮----
patchProjectSettings(baseDir: string, settings: Partial<ProjectSettings>): ProjectSettings
⋮----
async startProject(baseDir: string)
⋮----
async stopProject(baseDir: string): Promise<void>
⋮----
async restartProject(baseDir: string): Promise<void>
⋮----
async createVoiceSession(providerProfile: ProviderProfile): Promise<VoiceSession>
⋮----
async resetTask(baseDir: string, taskId: string): Promise<void>
⋮----
async restartAiderConnector(baseDir: string, taskId: string): Promise<void>
⋮----
getOpenProjects(): ProjectData[]
⋮----
async addOpenProject(baseDir: string): Promise<ProjectData[]>
⋮----
removeOpenProject(baseDir: string): ProjectData[]
⋮----
// Set the last project as active if the current active project was removed
⋮----
async setActiveProject(baseDir: string): Promise<ProjectData[]>
⋮----
updateOpenProjectsOrder(baseDirs: string[]): ProjectData[]
⋮----
getRecentProjects(): string[]
⋮----
addRecentProject(baseDir: string): void
⋮----
removeRecentProject(baseDir: string): void
⋮----
async interruptResponse(baseDir: string, taskId: string, interruptId?: string): Promise<void>
⋮----
clearContext(baseDir: string, taskId: string, includeLastMessage = true): void
⋮----
async removeLastMessage(baseDir: string, taskId: string): Promise<void>
⋮----
async removeMessage(baseDir: string, taskId: string, messageId: string): Promise<void>
⋮----
async removeMessagesUpTo(baseDir: string, taskId: string, messageId: string): Promise<void>
⋮----
async redoUserPrompt(baseDir: string, taskId: string, messageId: string, mode: Mode, updatedPrompt?: string, updatedImages?: string[]): Promise<void>
⋮----
async resumeTask(baseDir: string, taskId: string): Promise<void>
⋮----
async compactConversation(baseDir: string, taskId: string, mode: Mode, customInstructions?: string): Promise<void>
⋮----
async smartCompactConversation(baseDir: string, taskId: string): Promise<void>
⋮----
async undoContextChange(baseDir: string, taskId: string): Promise<boolean>
⋮----
async handoffConversation(baseDir: string, taskId: string, focus?: string): Promise<void>
⋮----
async runCodeChangeRequests(baseDir: string, taskId: string, requests: ChangeRequestItem[], createNewTask?: boolean): Promise<void>
⋮----
async loadInputHistory(baseDir: string): Promise<string[]>
⋮----
async getAddableFiles(baseDir: string, taskId: string, searchRegex?: string): Promise<string[]>
⋮----
async getAllFiles(baseDir: string, taskId: string, useGit = true): Promise<string[]>
⋮----
async refreshContextFiles(baseDir: string, taskId: string): Promise<void>
⋮----
async getUpdatedFiles(baseDir: string, taskId: string): Promise<UpdatedFile[]>
⋮----
async getUpdatedFileDiff(baseDir: string, taskId: string, filePath: string, commitHash?: string): Promise<string>
⋮----
async getUpdatedFileContents(baseDir: string, taskId: string, filePath: string, commitHash?: string): Promise<
⋮----
async addFile(baseDir: string, taskId: string, filePath: string, readOnly = false): Promise<void>
⋮----
dropFile(baseDir: string, taskId: string, filePath: string): void
⋮----
async pasteImage(baseDir: string, taskId: string, imageBuffer?: Buffer): Promise<void>
⋮----
applyEdits(baseDir: string, taskId: string, edits: FileEdit[]): void
⋮----
async runPrompt(baseDir: string, taskId: string, prompt: string, mode?: Mode, images?: string[]): Promise<ResponseCompletedData[]>
⋮----
async waitForTaskIdle(baseDir: string, taskId: string): Promise<void>
⋮----
async ensureProjectAndCreateTask(projectDir: string, params?: CreateTaskParams): Promise<
⋮----
async savePrompt(baseDir: string, taskId: string, prompt: string): Promise<void>
⋮----
async saveEditedPrompt(baseDir: string, taskId: string, messageId: string, prompt: string): Promise<void>
⋮----
async answerQuestion(baseDir: string, taskId: string, answer: string): Promise<void>
⋮----
respondInputPrompt(id: string, value: string | null, rememberSession?: boolean): void
⋮----
removeQueuedPrompt(baseDir: string, taskId: string, promptId: string): void
⋮----
reorderQueuedPrompts(baseDir: string, taskId: string, prompts: QueuedPromptData[]): void
⋮----
editQueuedPrompt(baseDir: string, taskId: string, promptId: string, newText: string): void
⋮----
async sendQueuedPromptNow(baseDir: string, taskId: string, promptId: string): Promise<void>
⋮----
runCommand(baseDir: string, taskId: string, command: string): void
⋮----
async getCommands(baseDir: string): Promise<CommandsData>
⋮----
async getCustomModes(baseDir?: string): Promise<ModeDefinition[]>
⋮----
async runCustomCommand(baseDir: string, taskId: string, commandName: string, args: string[], mode: Mode, images?: string[]): Promise<void>
⋮----
async updateMainModel(baseDir: string, taskId: string, mainModel: string): Promise<void>
⋮----
async updateWeakModel(baseDir: string, taskId: string, weakModel: string): Promise<void>
⋮----
async updateArchitectModel(baseDir: string, taskId: string, architectModel: string): Promise<void>
⋮----
updateEditFormats(baseDir: string, updatedFormats: Record<string, EditFormat>): void
⋮----
// Update just the current model's edit format while preserving others
⋮----
async loadMcpServerTools(serverName: string, config?: McpServerConfig, projectDir?: string): Promise<McpTool[] | string | null>
⋮----
async reloadMcpServers(projectDir?: string, force = false): Promise<void>
⋮----
async reloadMcpServer(serverName: string, config: McpServerConfig): Promise<McpTool[]>
⋮----
async getMcpOAuthStatus(serverName: string, config?: McpServerConfig, projectDir?: string): Promise<McpOAuthStatusData>
⋮----
async startMcpOAuth(serverName: string, config?: McpServerConfig, projectDir?: string): Promise<string>
⋮----
async disconnectMcpOAuth(serverName: string, config?: McpServerConfig, projectDir?: string): Promise<void>
⋮----
async completeMcpOAuth(code: string, state: string): Promise<void>
⋮----
async cancelMcpOAuth(state: string): Promise<void>
⋮----
async getMcpServers(): Promise<McpServersData>
⋮----
async addMcpServer(name: string, config: McpServerConfig, projectDir?: string): Promise<void>
⋮----
async updateMcpServer(oldName: string, name: string, config: McpServerConfig, projectDir?: string): Promise<void>
⋮----
async removeMcpServer(name: string, projectDir?: string): Promise<void>
⋮----
async replaceMcpServers(servers: Record<string, McpServerConfig>, projectDir?: string): Promise<void>
⋮----
async createTerminal(baseDir: string, taskId: string, cols?: number, rows?: number): Promise<string>
⋮----
writeToTerminal(terminalId: string, data: string): void
⋮----
resizeTerminal(terminalId: string, cols: number, rows: number): void
⋮----
closeTerminal(terminalId: string): void
⋮----
getTerminalForTask(taskId: string): string | null
⋮----
getTerminalBuffer(terminalId: string):
⋮----
getTerminalsForTask(taskId: string):
⋮----
private getTaskOrThrow(baseDir: string, taskId: string)
⋮----
async mergeWorktreeToMain(baseDir: string, taskId: string, squash: boolean, targetBranch?: string, commitMessage?: string): Promise<void>
⋮----
async switchToLocalWorkingMode(
    baseDir: string,
    taskId: string,
    options?: { mergeBeforeSwitch?: boolean; targetBranch?: string; switchAllInWorktree?: boolean; discardWorktreeChanges?: boolean },
): Promise<void>
⋮----
async switchToWorktreeWorkingMode(
    baseDir: string,
    taskId: string,
    options?: { carryOverUncommittedChanges?: boolean; dropSourceChanges?: boolean },
): Promise<void>
⋮----
async getLocalUncommittedFiles(baseDir: string, taskId: string): Promise<
⋮----
async applyUncommittedChanges(baseDir: string, taskId: string): Promise<void>
⋮----
async revertLastMerge(baseDir: string, taskId: string): Promise<void>
⋮----
async addFileToGit(baseDir: string, taskId: string, filePath: string): Promise<void>
⋮----
async restoreFile(baseDir: string, taskId: string, filePath: string): Promise<void>
⋮----
async readFile(baseDir: string, taskId: string, filePath: string): Promise<string>
⋮----
async saveFile(baseDir: string, taskId: string, filePath: string, content: string): Promise<void>
⋮----
async generateCommitMessage(baseDir: string, taskId: string, filePaths?: string[]): Promise<string>
⋮----
async commitChanges(baseDir: string, taskId: string, message: string, amend: boolean, skipGitHooks: boolean, filePaths?: string[]): Promise<void>
⋮----
cancelCommitChanges(baseDir: string, taskId: string): void
⋮----
async listBranches(baseDir: string): Promise<BranchInfo[]>
⋮----
async listGitBranches(baseDir: string, taskId: string, includeRemote?: boolean): Promise<BranchInfo[]>
⋮----
async isGitRepository(baseDir: string, taskId: string): Promise<boolean>
⋮----
async initializeGitRepository(baseDir: string, taskId: string): Promise<void>
⋮----
async getSyncCommits(baseDir: string, taskId: string, targetBranch?: string): Promise<GitSyncCommits>
⋮----
async createGitBranch(baseDir: string, taskId: string, name: string, startPoint?: string, checkout?: boolean): Promise<void>
⋮----
async checkoutGitBranch(baseDir: string, taskId: string, branch: string, createTracking?: boolean, takeOver?: boolean): Promise<void>
⋮----
async deleteGitBranch(baseDir: string, taskId: string, branch: string, force?: boolean): Promise<void>
⋮----
async mergeIntoCurrentBranch(baseDir: string, taskId: string, branch: string): Promise<
⋮----
async rebaseOntoBranch(baseDir: string, taskId: string, branch: string): Promise<
⋮----
async updateGitBranch(baseDir: string, taskId: string, branchName: string): Promise<
⋮----
async gitPull(baseDir: string, taskId: string, rebase?: boolean): Promise<
⋮----
async gitPush(baseDir: string, taskId: string, force?: boolean, setUpstream?: boolean): Promise<
⋮----
async getTaskGitStatus(baseDir: string, taskId: string, targetBranch?: string)
⋮----
async rebaseWorktreeFromBranch(baseDir: string, taskId: string, fromBranch?: string): Promise<void>
⋮----
async abortRebase(baseDir: string, taskId: string): Promise<void>
⋮----
async continueRebase(baseDir: string, taskId: string): Promise<void>
⋮----
async resolveConflictsWithAgent(baseDir: string, taskId: string): Promise<void>
⋮----
async resolveGitErrorWithAgent(baseDir: string, taskId: string): Promise<void>
⋮----
async renameGitBranch(baseDir: string, taskId: string, newBranchName: string): Promise<void>
⋮----
async renameWorktreeBranch(baseDir: string, taskId: string, newBranchName: string): Promise<void>
⋮----
async scrapeWeb(baseDir: string, taskId: string, url: string, filePath?: string): Promise<void>
⋮----
// Normalize URL for filename
⋮----
// Truncate if too long
⋮----
// Check if path looks like a directory (ends with separator)
⋮----
async createNewTask(baseDir: string, params?: CreateTaskParams): Promise<TaskData>
⋮----
async updateTask(baseDir: string, id: string, updates: Partial<TaskData>): Promise<TaskData | undefined>
⋮----
// Delegate to Task.updateTask method which handles worktree logic
⋮----
async deleteTask(baseDir: string, id: string): Promise<void>
⋮----
async duplicateTask(baseDir: string, taskId: string): Promise<TaskData>
⋮----
async forkTask(baseDir: string, taskId: string, messageId: string): Promise<TaskData>
⋮----
async getTasks(baseDir: string): Promise<TaskData[]>
⋮----
async reloadTasks(baseDir: string): Promise<TaskData[]>
⋮----
async loadTask(baseDir: string, taskId: string): Promise<TaskStateData>
⋮----
async generateTaskMarkdown(baseDir: string, taskId: string): Promise<string | null>
⋮----
async exportTaskToMarkdown(baseDir: string, taskId: string, copyOnly: boolean = false): Promise<string | null>
⋮----
async setZoomLevel(zoomLevel: number)
⋮----
// Apply zoom level to all open windows
⋮----
async getVersions(forceRefresh = false): Promise<VersionsInfo>
⋮----
async downloadLatestAiderDesk(): Promise<void>
⋮----
getReleaseNotes(): string | null
⋮----
clearReleaseNotes(): void
⋮----
getOS(): OS
⋮----
async queryUsageData(from: Date, to: Date): Promise<UsageDataRow[]>
⋮----
getEffectiveEnvironmentVariable(key: string, baseDir?: string): EnvironmentVariable | undefined
⋮----
async getProviderModels(reload = false): Promise<ProviderModelsData>
⋮----
getProviders(): ProviderProfile[]
⋮----
async updateProviders(providers: ProviderProfile[]): Promise<void>
⋮----
async upsertModel(providerId: string, modelId: string, model: Model): Promise<void>
⋮----
async deleteModel(providerId: string, modelId: string): Promise<void>
⋮----
async updateModels(modelUpdates: Array<
⋮----
async showOpenDialog(options: OpenDialogOptions): Promise<OpenDialogResult>
⋮----
async openLogsDirectory(): Promise<boolean>
⋮----
getSystemLogs(fromId?: number, limit?: number, levels?: SystemLogLevel[]): SystemLogsResponse
⋮----
clearSystemLogs(): void
⋮----
async openPath(path: string): Promise<boolean>
⋮----
async openUrlExternally(url: string): Promise<void>
⋮----
async openUrlInWindow(url: string, title?: string): Promise<void>
⋮----
async isProjectPath(path: string): Promise<boolean>
⋮----
async cloneProject(repositoryUrl: string, targetDir?: string): Promise<string>
⋮----
cancelCloneProject(): void
⋮----
async isValidPath(baseDir: string, path: string): Promise<boolean>
⋮----
async getFilePathSuggestions(currentPath: string, directoriesOnly = true): Promise<string[]>
⋮----
async getTodos(baseDir: string, taskId: string): Promise<TodoItem[]>
⋮----
async addTodo(baseDir: string, taskId: string, name: string): Promise<TodoItem[]>
⋮----
async updateTodo(baseDir: string, taskId: string, name: string, updates: Partial<TodoItem>): Promise<TodoItem[]>
⋮----
async deleteTodo(baseDir: string, taskId: string, name: string): Promise<TodoItem[]>
⋮----
async clearAllTodos(baseDir: string, taskId: string): Promise<TodoItem[]>
⋮----
async initProjectRulesFile(baseDir: string, taskId: string, args?: string): Promise<void>
⋮----
async getSkills(baseDir: string, taskId: string)
⋮----
async activateSkill(baseDir: string, taskId: string, skillName: string): Promise<boolean>
⋮----
async deactivateSkill(baseDir: string, taskId: string, skillName: string): Promise<void>
⋮----
async enableServer(username?: string, password?: string): Promise<SettingsData>
⋮----
async disableServer(): Promise<SettingsData>
⋮----
async startCloudflareTunnel(): Promise<CloudflareTunnelStatus | null>
⋮----
stopCloudflareTunnel(): void
⋮----
getCloudflareTunnelStatus(): CloudflareTunnelStatus
⋮----
async getAllAgentProfiles()
⋮----
// Extension agents are now included in getAllProfiles()
⋮----
async createAgentProfile(profile: AgentProfile, projectDir?: string)
⋮----
async updateAgentProfile(profile: AgentProfile)
⋮----
// First, check if this is an extension-provided agent
⋮----
// Extension handled it - notify projects of the change
⋮----
// Not an extension agent - use file-based storage
⋮----
async deleteAgentProfile(profileId: string)
⋮----
async updateAgentProfilesOrder(agentProfiles: AgentProfile[])
⋮----
async listAllMemories(): Promise<MemoryEntry[]>
⋮----
async deleteMemory(id: string): Promise<boolean>
⋮----
async deleteScopeMemories(scopeId: string): Promise<number>
⋮----
getMemoryEmbeddingProgress()
⋮----
getInstalledExtensions(projectDir?: string): InstalledExtension[]
⋮----
getExtensionToolsInfo(projectDir?: string): ExtensionToolInfo[]
⋮----
async getAvailableExtensions(repositories: string[], forceRefresh?: boolean, fetchOnly?: boolean)
⋮----
async installExtension(extensionId: string, repositoryUrl: string, projectDir?: string): Promise<ExtensionOperationResult>
⋮----
async uninstallExtension(extensionId: string, projectDir?: string)
⋮----
async reloadExtension(filePath: string, projectDir?: string): Promise<boolean>
⋮----
async updateExtension(extensionId: string, repositoryUrl: string, projectDir?: string): Promise<ExtensionOperationResult>
⋮----
getUIComponents(placement?: string, projectDir?: string, taskId?: string): ExtensionUIComponent[]
⋮----
async loadExtensionLibrary(librarySpec: string): Promise<string>
⋮----
async getUIExtensionData(extensionId: string, componentId: string, projectDir?: string, taskId?: string): Promise<unknown>
⋮----
async executeUIExtensionAction(
    extensionId: string,
    componentId: string,
    action: string,
    args: unknown[],
    projectDir?: string,
    taskId?: string,
): Promise<unknown>
⋮----
getExtensionConfigComponent(extensionId: string, projectDir?: string): ExtensionConfigComponent | null
⋮----
async getExtensionConfig(extensionId: string, projectDir?: string): Promise<unknown>
⋮----
async saveExtensionConfig(extensionId: string, configData: unknown, projectDir?: string): Promise<unknown>

================
File: src/main/file-watcher-manager.ts
================
import fs from 'fs/promises';
import path from 'path';
⋮----
import { FSWatcher, watch } from 'chokidar';
import debounce from 'lodash/debounce';
import { simpleGit } from 'simple-git';
⋮----
import type { Store } from '@/store';
⋮----
import { shouldUsePolling } from '@/utils/file-watch';
import logger from '@/logger';
⋮----
export type FileWatchOptions = {
  ignored?: (string | RegExp)[] | ((matchingPath: string) => boolean);
  debounceMs?: number;
};
⋮----
export interface WatchHandle {
  unwatch: () => void;
}
⋮----
type WatcherEntry = {
  dir: string;
  watcher: FSWatcher | null;
  handles: Set<() => Promise<void>>;
  notify: (() => void) & { cancel: () => void };
  loggedEnospc?: boolean;
};
⋮----
// Heavy directories excluded from watching by default
⋮----
const getErrorMessage = (error: unknown): string
⋮----
const tryCatchAsync = async (fn: () => void | Promise<void>, context: string) =>
⋮----
/**
 * Lists paths ignored by git rules (collapsed to top-level entries), e.g. `out/`, `.aider-desk/tmp/`.
 * Returns paths relative to `dir` (dirs end with `/`), or an empty list for non-git dirs / failures.
 */
const getGitIgnoredEntries = async (dir: string): Promise<string[]> =>
⋮----
const countWatchedEntries = async (dir: string, isIgnored: (watcherPath: string) => boolean, isCancelled: () => boolean): Promise<number> =>
⋮----
export class FileWatcherManager
⋮----
constructor(private readonly store: Store)
⋮----
/**
   * Starts watching `dir` (one shared chokidar watcher per dir + options combination,
   * handles are ref-counted) and returns a handle that must be released with `unwatch()`.
   * The debounced `onChange` is invoked for file changes (add/change/unlink).
   *
   * Watching is skipped (no-op handle) when the directory tree is too large to watch safely.
   */
async watch(dir: string, onChange: () => void | Promise<void>, options: FileWatchOptions =
⋮----
const handler = () => tryCatchAsync(onChange, `watch '$
⋮----
const isCancelled = ()
⋮----
async dispose(): Promise<void>
⋮----
private createIgnoredList(dir: string, gitIgnoredEntries: string[], userIgnored?: FileWatchOptions['ignored']): ((watcherPath: string) => boolean)[]
⋮----
const matcher = (relativePath: string): boolean =>
⋮----
// path itself or one of its ancestors matches a gitignored entry
⋮----
const matchFn = (watcherPath: string): boolean =>
⋮----
private getWatchKey(dir: string, options: FileWatchOptions): string
⋮----
private createEntry(dir: string, options: FileWatchOptions, ignoredForWatcher: ((watcherPath: string) => boolean)[]): WatcherEntry
⋮----
private bindWatcherEvents(entry: WatcherEntry)
⋮----
private handleWatcherError(entry: WatcherEntry, error: unknown): void
⋮----
// Log once per watcher instead of spamming an error for every unwatchable path
⋮----
private async closeEntry(entry: WatcherEntry): Promise<void>
⋮----
private deleteEntry(entry: WatcherEntry): void
⋮----
private async closeWatcher(entry: WatcherEntry): Promise<void>

================
File: src/main/index.ts
================
import { join } from 'path';
import { existsSync, statSync } from 'fs';
⋮----
import { compareBaseDirs, delay } from '@common/utils';
import { ContextMenuParams } from '@common/types';
import { electronApp, is, optimizer } from '@electron-toolkit/utils';
import { app, BrowserWindow, dialog, Menu, session, shell } from 'electron';
⋮----
import icon from '../../build/icon.png?asset';
import splashImage from '../../build/splash.mp4?asset';
⋮----
import { AIDER_DESK_DATA_DIR, DISABLE_MENU, HEADLESS_MODE } from '@/constants';
import { ProgressWindow } from '@/progress-window';
import { setupIpcHandlers } from '@/ipc-handlers';
import { performStartUp, UpdateProgressData } from '@/start-up';
import { Store } from '@/store';
import logger, { eventTransport } from '@/logger';
import { initManagers } from '@/managers';
import { createLoadRetryHandler, getDefaultProjectSettings, initPath } from '@/utils';
import { WindowManager } from '@/window-manager';
⋮----
// Global instances shared across all windows
⋮----
// Prevent unhandled promise rejections from crashing the process
⋮----
const setupCustomMenu = (createWindowFn: () => void): void =>
⋮----
// File menu
⋮----
// Edit menu
⋮----
// View menu
⋮----
// Settings menu
⋮----
const initStore = async (): Promise<Store> =>
⋮----
const initWindow = async (windowMgr: WindowManager, storeInstance: Store, projectToActivate?: string): Promise<BrowserWindow> =>
⋮----
// Register window with window manager
⋮----
// Retry transient network failures (e.g. ERR_NETWORK_CHANGED during dev server loading)
// so a network change doesn't leave a blank white window
⋮----
// Renderer crash recovery: reload from the main process side, since a crashed/hung
// renderer cannot process user-triggered reloads (e.g. Ctrl+R)
⋮----
const saveWindowState = (): void =>
⋮----
// Only save state if this is the main window
⋮----
// Handle window close
⋮----
// HMR for renderer base on electron-vite cli.
// Load the remote URL for development or the local html file for production.
⋮----
// For HashRouter, query params must come after the hash: #/home?project=...
⋮----
// transient failures (e.g. ERR_NETWORK_CHANGED) are retried by the did-fail-load handler
⋮----
// For production with HashRouter, append hash with query params
⋮----
// transient failures (e.g. ERR_NETWORK_CHANGED) are retried by the did-fail-load handler
⋮----
// Apply saved zoom level
⋮----
// Function to create a new window
const createNewWindow = async (projectToActivate?: string) =>
⋮----
// Export getter for IPC handlers
export const getCreateNewWindow = ()
⋮----
// Setup custom menu only in GUI mode - pass createNewWindow function
⋮----
updateProgress = (
⋮----
// Forward logger.info messages to splash screen
⋮----
// In headless mode, use a no-op updateProgress
updateProgress = () =>
⋮----
// Initialize store globally
⋮----
// Initialize window manager
⋮----
// Initialize managers (shared across all windows) — creates pythonInstaller
⋮----
// performStartUp is now non-blocking — it kicks off background installation
// and returns immediately. The progress bar acts as a splash screen.
⋮----
// performStartUp no longer throws for install errors (handled async)
// but guard against unexpected issues
⋮----
// Setup global cleanup
const globalCleanup = async () =>
⋮----
// Handle CTRL+C (SIGINT)
⋮----
// Allow renderer getUserMedia() microphone access.
// Without this, Electron may never surface the macOS TCC permission prompt and the mic stays unavailable.
⋮----
// Initialize IPC handlers
⋮----
// Create the first window
⋮----
// Show ready state before closing
⋮----
// Stop forwarding logs to splash before closing

================
File: src/main/ipc-handlers.ts
================
import {
  EditFormat,
  FileEdit,
  McpServerConfig,
  Mode,
  Model,
  OpenDialogOptions,
  ProjectSettings,
  ProviderProfile,
  QueuedPromptData,
  SettingsData,
  SystemLogLevel,
  TaskData,
  CreateTaskParams,
  TodoItem,
  AgentProfile,
  ChangeRequestItem,
} from '@common/types';
import { ipcMain, clipboard, ClipboardItem } from 'electron';
⋮----
import { EventsHandler } from './events-handler';
⋮----
import { PythonDependenciesInstaller } from '@/python-dependencies-installer';
import { ServerController } from '@/server';
⋮----
export const setupIpcHandlers = (eventsHandler: EventsHandler, serverController: ServerController, pythonInstaller: PythonDependenciesInstaller) =>
⋮----
// Voice handlers
⋮----
// MCP server config handlers (file-based, global + per-project)
⋮----
// Extension handlers
⋮----
// Extension config handlers (per-extension settings)
⋮----
// Terminal handlers
⋮----
// Worktree merge handlers
⋮----
// Git branch operations
⋮----
// Server control handlers
⋮----
// Cloudflare tunnel handlers
⋮----
// Agent profile handlers
⋮----
// Memory handlers
⋮----
// System logs handlers
⋮----
// Aider connector status (Python install + per-task connector lifecycle)

================
File: src/main/logger.ts
================
import winston from 'winston';
import DailyRotateFile from 'winston-daily-rotate-file';
⋮----
import { EventTransport } from './logger/event-transport';
⋮----
import { LOGS_DIR } from '@/constants';
import { EventManager } from '@/events';
⋮----
// Create event transport immediately to capture all logs since startup
⋮----
// Add event transport immediately to capture all logs from startup
⋮----
// If we're not in production OR running in headless mode (Docker), also log to the console
⋮----
/**
 * Initialize event-based logging (call this after EventManager is available)
 */
export const initEventLogging = (eventManager: EventManager): void =>

================
File: src/main/managers.ts
================
import { createServer } from 'http';
⋮----
import type { WindowManager } from '@/window-manager';
⋮----
import { AgentProfileManager, McpConfigManager, McpManager } from '@/agent';
import { CloudflareTunnelManager, ServerController } from '@/server';
import { ConnectorManager } from '@/connector';
import { ProjectManager } from '@/project';
import { EventManager } from '@/events';
import { ModelManager } from '@/models';
import { DataManager } from '@/data-manager';
import { TerminalManager } from '@/terminal';
import { VersionsManager } from '@/versions';
import { TelemetryManager } from '@/telemetry';
import { GitManager, GitAskpassManager } from '@/git';
import { MemoryManager } from '@/memory/memory-manager';
import { ExtensionManager } from '@/extensions/extension-manager';
import { Store } from '@/store';
import { SERVER_PORT } from '@/constants';
import logger, { initEventLogging } from '@/logger';
import { NetworkManager } from '@/network-manager';
import { EventsHandler } from '@/events-handler';
import { PromptsManager } from '@/prompts';
import { PythonDependenciesInstaller } from '@/python-dependencies-installer';
⋮----
export interface ManagersResult {
  eventsHandler: EventsHandler;
  serverController: ServerController;
  cleanup: () => Promise<void>;
  modelManager: ModelManager;
  agentProfileManager: AgentProfileManager;
  extensionManager: ExtensionManager;
  pythonInstaller: PythonDependenciesInstaller;
  projectManager: ProjectManager;
}
⋮----
export const initManagers = async (store: Store, windowManager?: WindowManager): Promise<ManagersResult> =>
⋮----
// Prevent unhandled promise rejections from crashing the process
⋮----
// Initialize network manager FIRST — must be before any network calls
⋮----
// Initialize telemetry manager (non-blocking - analytics not critical for startup)
⋮----
// Initialize MCP manager
⋮----
// Initialize event manager with window manager
⋮----
// Initialize MCP config manager (file-based global + per-project server configuration)
⋮----
// Initialize event-based logging (adds transport to logger)
⋮----
// Create Python dependencies installer (self-wires status events via EventManager)
⋮----
// Initialize model manager
⋮----
// Initialize data manager
⋮----
// Initialize memory manager (non-blocking - heavy operation with lazy loading)
⋮----
// Initialize extension manager (non-blocking - errors should not crash app)
⋮----
// Initialize prompts manager (non-blocking - templates compile lazily)
⋮----
// Initialize agent profile manager with extension manager for unified profile access
⋮----
// Initialize project manager
⋮----
// Initialize terminal manager
⋮----
// Initialize Versions Manager
⋮----
// Create HTTP server
⋮----
// Initialize Cloudflare tunnel manager
⋮----
// Initialize events handler with window manager
⋮----
// Create and initialize REST API controller with the server
⋮----
// Initialize connector manager with the server
⋮----
// Start listening
⋮----
const cleanup = async (): Promise<void> =>
⋮----
// Handle process signals

================
File: src/main/messages.ts
================
import {
  AiderRunOptions,
  ContextFile,
  ContextFileSourceType,
  EditFormat,
  FileEdit,
  LogLevel,
  MessageRole,
  Mode,
  ModelInfo,
  PromptContext,
  RawModelInfo,
  TokensCost,
  UsageReportData,
} from '@common/types';
⋮----
export type MessageAction =
  | 'init'
  | 'prompt'
  | 'prompt-finished'
  | 'response'
  | 'add-file'
  | 'drop-file'
  | 'update-autocompletion'
  | 'ask-question'
  | 'answer-question'
  | 'set-models'
  | 'update-context-files'
  | 'use-command-output'
  | 'run-command'
  | 'tokens-info'
  | 'add-message'
  | 'interrupt-response'
  | 'apply-edits'
  | 'compact-conversation'
  | 'update-repo-map'
  | 'update-env-vars'
  | 'request-context-info'
  | 'subscribe-events'
  | 'unsubscribe-events'
  | 'readonly-subscribe-events'
  | 'readonly-unsubscribe-events'
  | 'update-models-info'
  | 'write-to-terminal';
⋮----
export interface Message {
  action: MessageAction;
}
⋮----
export interface LogMessage {
  message: string;
  level: LogLevel;
  finished?: boolean;
  promptContext?: PromptContext;
}
⋮----
export interface InitMessage {
  action: 'init';
  baseDir: string;
  taskId?: string;
  source?: string;
  contextFiles?: ContextFile[];
  listenTo?: MessageAction[];
  inputHistoryFile?: string;
}
⋮----
export const isInitMessage = (message: Message): message is InitMessage =>
⋮----
export interface PromptMessage extends Message {
  action: 'prompt';
  prompt: string;
  mode: Mode | null;
  architectModel: string | null;
  promptContext: PromptContext;
  messages?: { role: MessageRole; content: string }[];
  files?: ContextFile[];
  options?: AiderRunOptions;
}
⋮----
export interface ResponseMessage extends Message {
  id: string;
  action: 'response';
  content: string;
  reasoning?: string;
  reflectedMessage?: string;
  finished: boolean;
  usageReport?: string | UsageReportData;
  editedFiles?: string[];
  commitHash?: string;
  commitMessage?: string;
  diff?: string;
  sequenceNumber?: number;
  promptContext?: PromptContext;
}
⋮----
export const isResponseMessage = (message: Message): message is ResponseMessage =>
⋮----
export interface AddFileMessage extends Message {
  action: 'add-file';
  path: string;
  sourceType?: ContextFileSourceType;
  readOnly?: boolean;
  noUpdate?: boolean;
}
⋮----
export const isAddFileMessage = (message: Message): message is AddFileMessage =>
⋮----
export interface DropFileMessage extends Message {
  action: 'drop-file';
  path: string;
  readOnly?: boolean;
  noUpdate?: boolean;
}
⋮----
export const isDropFileMessage = (message: Message): message is DropFileMessage =>
⋮----
export interface RunCommandMessage extends Message {
  action: 'run-command';
  command: string;
  messages?: { role: MessageRole; content: string }[];
  files?: ContextFile[];
}
⋮----
export interface UpdateAutocompletionMessage extends Message {
  action: 'update-autocompletion';
  words: string[];
}
⋮----
export const isUpdateAutocompletionMessage = (message: Message): message is UpdateAutocompletionMessage =>
⋮----
export interface AskQuestionMessage extends Message {
  action: 'ask-question';
  question: string;
  subject?: string;
  defaultAnswer: string;
  isGroupQuestion?: boolean;
}
⋮----
export const isAskQuestionMessage = (message: Message): message is AskQuestionMessage =>
⋮----
export interface AnswerQuestionMessage extends Message {
  action: 'answer-question';
  answer: string;
}
⋮----
export interface SetModelsMessage extends Message {
  action: 'set-models';
  mainModel: string;
  weakModel?: string | null;
  editFormat?: EditFormat;
  environmentVariables?: Record<string, string>;
  info?: RawModelInfo;
  hasError?: boolean;
}
⋮----
export const isSetModelsMessage = (message: Message): message is SetModelsMessage =>
⋮----
export interface UpdateContextFilesMessage extends Message {
  action: 'update-context-files';
  files: ContextFile[];
}
⋮----
export const isUpdateContextFilesMessage = (message: Message): message is UpdateContextFilesMessage =>
⋮----
export interface UseCommandOutputMessage extends Message {
  action: 'use-command-output';
  command: string;
  addToContext?: boolean;
  finished: boolean;
}
⋮----
export const isUseCommandOutputMessage = (message: Message): message is UseCommandOutputMessage =>
⋮----
export interface TokensInfoMessage extends Message {
  action: 'tokens-info';
  info: {
    files: Record<string, TokensCost>;
    systemMessages: TokensCost;
    chatHistory: TokensCost;
    repoMap: TokensCost;
  };
}
⋮----
export const isTokensInfoMessage = (message: Message): message is TokensInfoMessage =>
⋮----
export interface AddMessageMessage extends Message {
  action: 'add-message';
  content: string;
  role: MessageRole;
  acknowledge: boolean;
  usageReport?: UsageReportData;
}
⋮----
export const isAddMessageMessage = (message: Message): message is AddMessageMessage =>
⋮----
export interface InterruptResponseMessage extends Message {
  action: 'interrupt-response';
}
⋮----
export interface PromptFinishedMessage extends Message {
  action: 'prompt-finished';
  promptId: string;
}
⋮----
export const isPromptFinishedMessage = (message: Message): message is PromptFinishedMessage =>
⋮----
export interface ApplyEditsMessage extends Message {
  action: 'apply-edits';
  edits: FileEdit[];
}
⋮----
export interface CompactConversationMessage extends Message {
  action: 'compact-conversation';
  customInstructions?: string;
}
⋮----
export interface UpdateRepoMapMessage extends Message {
  action: 'update-repo-map';
  repoMap: string;
}
⋮----
export const isUpdateRepoMapMessage = (message: Message): message is UpdateRepoMapMessage =>
⋮----
export interface UpdateEnvVarsMessage extends Message {
  action: 'update-env-vars';
  environmentVariables: Record<string, unknown>;
}
⋮----
export const isUpdateEnvVarsMessage = (message: Message): message is UpdateEnvVarsMessage =>
⋮----
export interface UpdateModelsInfoMessage extends Message {
  action: 'update-models-info';
  modelsInfo: Record<string, ModelInfo>;
}
⋮----
export const isUpdateModelsInfoMessage = (message: Message): message is UpdateModelsInfoMessage =>
⋮----
export interface RequestContextInfoMessage extends Message {
  action: 'request-context-info';
  messages?: { role: MessageRole; content: string }[];
  files?: ContextFile[];
}
⋮----
export const isRequestContextInfoMessage = (message: Message): message is RequestContextInfoMessage =>
⋮----
export interface WriteToTerminalMessage extends Message {
  action: 'write-to-terminal';
  terminalId: string;
  data: string;
}
⋮----
export const isWriteToTerminalMessage = (message: Message): message is WriteToTerminalMessage =>
⋮----
export interface SubscribeEventsMessage extends Message {
  action: 'subscribe-events';
  eventTypes?: string[];
  baseDirs?: string[];
}
⋮----
export const isSubscribeEventsMessage = (message: Message): message is SubscribeEventsMessage =>
⋮----
export interface UnsubscribeEventsMessage extends Message {
  action: 'unsubscribe-events';
}
⋮----
export const isUnsubscribeEventsMessage = (message: Message): message is UnsubscribeEventsMessage =>
⋮----
export interface ReadonlySubscribeEventsMessage extends Message {
  action: 'readonly-subscribe-events';
  projectDir: string;
  eventTypes: string[];
}
⋮----
export const isReadonlySubscribeEventsMessage = (message: Message): message is ReadonlySubscribeEventsMessage =>
⋮----
export interface ReadonlyUnsubscribeEventsMessage extends Message {
  action: 'readonly-unsubscribe-events';
}
⋮----
export const isReadonlyUnsubscribeEventsMessage = (message: Message): message is ReadonlyUnsubscribeEventsMessage =>

================
File: src/main/network-manager.ts
================
import { readFileSync } from 'fs';
⋮----
import { setGlobalDispatcher, EnvHttpProxyAgent, Agent as UndiciAgent, Dispatcher } from 'undici';
import { bootstrap } from 'global-agent';
⋮----
import type { ProviderProfile, SettingsData, TlsPolicyOptions, TlsPolicyRegistrar } from '@common/types';
⋮----
import logger from '@/logger';
⋮----
const HEADERS_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
const BODY_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
const CONNECT_TIMEOUT_MS = 30 * 1000; // 30 seconds
⋮----
type TlsConnectOptions = {
  rejectUnauthorized?: false;
  ca?: string[];
};
⋮----
class TlsRoutingDispatcher extends Dispatcher
⋮----
constructor(
    private readonly resolveDispatcher: (origin: string | undefined) => Dispatcher,
    private readonly getAllDispatchers: () => Dispatcher[],
)
⋮----
dispatch(options: Dispatcher.DispatchOptions, handler: Dispatcher.DispatchHandler): boolean
⋮----
close(): Promise<void>
⋮----
destroy(): Promise<void>
⋮----
export const getProxyEnvVars = (settings: SettingsData): Record<string, string> =>
⋮----
export const getTlsEnvVars = (providers: ProviderProfile[]): Record<string, string> =>
⋮----
export const getNetworkEnvVars = (settings: SettingsData, providers: ProviderProfile[]): Record<string, string> => (
⋮----
export class NetworkManager implements TlsPolicyRegistrar
⋮----
/**
   * Initialize network settings from settings. Called once during app startup (first in managers init).
   */
init(settings: SettingsData): void
⋮----
/**
   * React to settings changes. Called on every saveSettings().
   * Re-initializes the proxy if the proxy config changed.
   */
settingsChanged(oldSettings: SettingsData, newSettings: SettingsData): void
⋮----
setTlsPolicy(origin: string, options: TlsPolicyOptions): void
⋮----
hasTlsPolicy(origin: string): boolean
⋮----
clearTlsPolicy(origin: string): void
⋮----
private resolveDispatcher(origin: string | undefined): Dispatcher
⋮----
private getAllDispatchers(): Dispatcher[]
⋮----
private applyProxy(url: string, noProxy?: string): void
⋮----
// Cover Axios, Got, and legacy HTTP libs via global-agent
⋮----
private clearProxy(): void
⋮----
private rebuildDispatchers(): void
⋮----
private buildDispatcher(connectOptions?: TlsConnectOptions): Dispatcher
⋮----
// `connect` applies to plain Agents, `requestTls` to origins tunneled through ProxyAgent CONNECT

================
File: src/main/paths.ts
================
import path from 'path';
⋮----
import { getElectronApp, isDev, isElectron } from '@/app';
⋮----
export const getDataDir = (): string =>
⋮----
// eslint-disable-next-line @typescript-eslint/no-require-imports
⋮----
export const getResourceDir = (): string =>

================
File: src/main/progress-window.ts
================
import { join } from 'path';
⋮----
import { BrowserWindow } from 'electron';
⋮----
import { isDev } from '@/app';
⋮----
export class ProgressWindow
⋮----
constructor(options:
⋮----
on(event: 'ready', callback: () => void): void
⋮----
setDetail(value: string): void
⋮----
setCompleted(): void
⋮----
addLog(message: string): void
⋮----
close(): void

================
File: src/main/python-dependencies-installer.ts
================
import { exec } from 'child_process';
⋮----
import { promisify } from 'util';
⋮----
import logger from '@/logger';
import { EventManager } from '@/events';
import { getCurrentPythonLibVersion, getLatestPythonLibVersion, getPythonVenvBinPath, getResolvedUvPath } from '@/utils';
import { AIDER_DESK_CONNECTOR_DIR, AIDER_DESK_DATA_DIR, PYTHON_VENV_DIR, RESOURCES_DIR, SETUP_COMPLETE_FILENAME } from '@/constants';
⋮----
export type PythonInstallStatus =
  | { state: 'idle' }
  | { state: 'checking-uv' }
  | { state: 'downloading-uv' }
  | { state: 'creating-venv' }
  | {
      state: 'installing-packages';
      package: string;
      current: number;
      total: number;
    }
  | { state: 'setting-up-connector' }
  | { state: 'starting-connector' }
  | { state: 'ready' }
  | { state: 'failed'; error: string };
⋮----
type StatusListener = (status: PythonInstallStatus) => void;
⋮----
/**
 * Manages Python dependency installation in the background.
 *
 * Handles:
 * - Creating the Python virtual environment
 * - Installing aider-chat and connector dependencies
 * - Exposing installation status to the UI via EventManager
 *
 * The installation runs asynchronously (fire-and-forget). Consumers call
 * `waitForReady()` to gate on completion before spawning Python processes.
 */
export class PythonDependenciesInstaller
⋮----
constructor(private readonly eventManager: EventManager)
⋮----
/**
   * Start the installation process in the background (fire-and-forget).
   * Safe to call multiple times — only one install runs at a time.
   */
ensureInstalled(): void
⋮----
/**
   * Wait until the installation is complete and the environment is ready.
   * Resolves immediately if already ready. Rejects if installation failed.
   */
async waitForReady(): Promise<void>
⋮----
// Not started yet — kick off and wait
⋮----
/** Check if the environment is ready to use. */
isReady(): boolean
⋮----
/** Get the current installation status. */
getStatus(): PythonInstallStatus
⋮----
/**
   * Subscribe to status changes. Returns an unsubscribe function.
   */
onStatusChange(listener: StatusListener): () => void
⋮----
// --- Private implementation ---
⋮----
private setStatus(status: PythonInstallStatus): void
⋮----
// Ignore listener errors
⋮----
private async runInstallation(): Promise<void>
⋮----
// If setup was previously completed and venv exists, do an update check
⋮----
// Full first-time setup
⋮----
// Create data dir if needed
⋮----
// Mark setup as complete
⋮----
private async performUpdateCheck(): Promise<void>
⋮----
// Run update check (install/upgrade packages)
⋮----
private async checkUvAvailableInternal(): Promise<void>
⋮----
private async createVirtualEnvInternal(): Promise<void>
⋮----
private async setupAiderConnectorInternal(cleanInstall: boolean): Promise<void>
⋮----
// Copy connector.py from resources
⋮----
private async installAiderConnectorRequirements(cleanInstall: boolean): Promise<void>
⋮----
private async checkNetworkConnectivity(): Promise<boolean>

================
File: src/main/start-up.ts
================
import { PythonDependenciesInstaller } from '@/python-dependencies-installer';
import { AIDER_DESK_DATA_DIR, SETUP_COMPLETE_FILENAME, PYTHON_VENV_DIR } from '@/constants';
import logger from '@/logger';
⋮----
export type UpdateProgressData = {
  step: string;
  message: string;
  info?: string;
};
⋮----
export type UpdateProgressFunction = (data: UpdateProgressData) => void;
⋮----
/**
 * Performs a fast startup check and kicks off background Python dependency installation.
 *
 * This function no longer blocks on pip installs. It returns quickly after checking
 * whether setup has been completed previously. The actual installation happens
 * asynchronously in the PythonDependenciesInstaller singleton.
 *
 * @param pythonInstaller The singleton PythonDependenciesInstaller instance
 * @param updateProgress Optional callback for progress UI (splash screen etc.)
 * @returns true if the app should proceed (always true now — errors are handled async)
 */
export const performStartUp = async (pythonInstaller: PythonDependenciesInstaller, updateProgress?: UpdateProgressFunction): Promise<boolean> =>
⋮----
// Kick off background update check — don't await it
⋮----
// First-time setup needed — kick off in background
⋮----
// Ensure data dir exists (fast, synchronous)
⋮----
// Kick off full installation — fire and forget

================
File: src/main/window-manager.ts
================
import type { BrowserWindow } from 'electron';
⋮----
import logger from '@/logger';
⋮----
/**
 * WindowManager manages all open BrowserWindow instances in the application.
 * It provides a central registry for windows and tracks which is the main window.
 */
export class WindowManager
⋮----
/**
   * Add a window to the manager.
   * The first window added is automatically set as the main window.
   * @param window The BrowserWindow to add
   */
addWindow(window: BrowserWindow): void
⋮----
// Set the first window as the main window
⋮----
/**
   * Remove a window from the manager.
   * @param window The BrowserWindow to remove
   */
removeWindow(window: BrowserWindow): void
⋮----
// If the main window is closed, don't set a new one
// The main window designation is only used for window state persistence
⋮----
/**
   * Get all currently open windows (non-destroyed).
   * @returns Array of BrowserWindow instances
   */
getAllWindows(): BrowserWindow[]
⋮----
/**
   * Get the main window (first window created).
   * @returns The main BrowserWindow or null if it's been closed
   */
getMainWindow(): BrowserWindow | null
⋮----
/**
   * Check if a given window is the main window.
   * @param window The window to check
   * @returns True if the window is the main window
   */
isMainWindow(window: BrowserWindow): boolean
⋮----
/**
   * Get the number of open windows.
   * @returns Number of windows
   */
getWindowCount(): number

================
File: src/preload/index.d.ts
================
import { ApplicationAPI } from '@common/api';
⋮----
import type { ElectronAPI } from '@electron-toolkit/preload';
⋮----
interface Window {
    electron: ElectronAPI;
    api: ApplicationAPI;
  }

================
File: src/preload/index.ts
================
import {
  AutocompletionData,
  ClearTaskData,
  CommandOutputData,
  CommandsData,
  ContextFilesUpdatedData,
  ContextInfoData,
  ContextMenuParams,
  CreateTaskParams,
  ExtensionUIRefreshData,
  ExtensionsUpdatedData,
  FileEdit,
  InputHistoryData,
  LogData,
  McpServerConfig,
  McpServersData,
  MessageRemovedData,
  ModalOverlayUrlData,
  ModelsData,
  OpenDialogOptions,
  OS,
  ProjectSettings,
  ProjectStartedData,
  ProviderModelsData,
  ProvidersUpdatedData,
  QuestionAnsweredData,
  QuestionData,
  QueuedPromptsUpdatedData,
  ResponseChunkData,
  ResponseCompletedData,
  SettingsData,
  SystemLogLevel,
  TaskCreatedData,
  TaskData,
  TerminalData,
  TerminalExitData,
  TokensInfoData,
  ToolData,
  ToolInputChunkData,
  UpdatedFilesUpdatedData,
  SkillsUpdatedData,
  UserMessageData,
  VersionsInfo,
  TaskGitStatusUpdatedData,
  AiderConnectorStatus,
  InputPromptData,
} from '@common/types';
import { electronAPI } from '@electron-toolkit/preload';
import { contextBridge, ipcRenderer, webUtils } from 'electron';
import { ApplicationAPI } from '@common/api';
import { compareBaseDirs } from '@common/utils';
⋮----
// Extension operations
⋮----
const listener = (_: Electron.IpcRendererEvent, data: ExtensionUIRefreshData) =>
⋮----
// Extension config operations (per-extension settings)
⋮----
// Voice API
⋮----
// Task lifecycle event listeners
⋮----
// Terminal operations
⋮----
// Worktree merge operations
⋮----
// Git branch operations
⋮----
// Agent profile operations
⋮----
// Memory operations
⋮----
// System logs
⋮----
// notifications in Electron app are handled by the main process
⋮----
// Aider connector status (Python install + per-task connector lifecycle)
⋮----
// eslint-disable-next-line no-console

================
File: src/renderer/public/manifest.json
================
{
  "name": "AiderDesk",
  "short_name": "AiderDesk",
  "description": "AI-powered coding assistant desktop application",
  "start_url": "/",
  "scope": "/",
  "display": "standalone",
  "background_color": "#1c2025",
  "theme_color": "#3b82f6",
  "orientation": "any",
  "categories": ["development", "productivity"],
  "prefer_related_applications": false,
  "icons": [
    {
      "src": "/icon-192.png",
      "sizes": "192x192",
      "type": "image/png",
      "purpose": "any"
    },
    {
      "src": "/icon-192.png",
      "sizes": "192x192",
      "type": "image/png",
      "purpose": "maskable"
    },
    {
      "src": "/icon-512.png",
      "sizes": "512x512",
      "type": "image/png",
      "purpose": "any"
    },
    {
      "src": "/icon-512.png",
      "sizes": "512x512",
      "type": "image/png",
      "purpose": "maskable"
    }
  ]
}

================
File: src/renderer/public/sw.js
================
// Minimal service worker for PWA recognition.
// No caching and no fetch interception - just exists to enable PWA features.
⋮----
// Skip waiting to activate immediately
⋮----
// Claim clients immediately
⋮----
// Do not intercept fetch requests. A pass-through respondWith(fetch()) here adds a
// service worker round-trip to every module load and turns transient network failures
// into hard ERR_FAILED errors. Leaving fetches untouched keeps PWA install eligibility
// without the failure amplification.

================
File: src/renderer/src/api/browser-api.ts
================
import {
  AgentProfilesUpdatedData,
  McpServersData,
  AutocompletionData,
  ClearTaskData,
  CloudflareTunnelStatus,
  CommandOutputData,
  ContextFilesUpdatedData,
  ContextInfoData,
  ContextMenuParams,
  CommandsData,
  EditFormat,
  EnvironmentVariable,
  ExtensionUIRefreshData,
  FileEdit,
  InputHistoryData,
  LogData,
  SystemLogData,
  SystemLogLevel,
  SystemLogsResponse,
  McpOAuthStatusData,
  McpServerConfig,
  McpTool,
  MessageRemovedData,
  Mode,
  ModeDefinition,
  Model,
  ModelsData,
  NotificationData,
  OpenDialogOptions,
  OpenDialogResult,
  OS,
  ProjectData,
  ProjectSettings,
  ProjectStartedData,
  ProviderModelsData,
  ProviderProfile,
  ProvidersUpdatedData,
  QuestionAnsweredData,
  QuestionData,
  QueuedPromptData,
  ResponseChunkData,
  ResponseCompletedData,
  SettingsData,
  TaskData,
  CreateTaskParams,
  TaskStateData,
  TerminalData,
  TerminalExitData,
  TodoItem,
  TokensInfoData,
  ToolData,
  ToolInputChunkData,
  UsageDataRow,
  UserMessageData,
  VersionsInfo,
  VoiceSession,
  AgentProfile,
  MemoryEntry,
  MemoryEmbeddingProgress,
  BranchInfo,
  GitSyncCommits,
  SwitchToLocalOptions,
  SwitchToWorktreeOptions,
  TaskGitStatus,
  TaskGitStatusUpdatedData,
  UncommittedFiles,
  TaskCreatedData,
  UpdatedFilesUpdatedData,
  QueuedPromptsUpdatedData,
  InstalledExtension,
  AvailableExtension,
  ExtensionConfigComponent,
  ExtensionToolInfo,
  ExtensionUIComponent,
  ExtensionsUpdatedData,
  ModalOverlayUrlData,
  InputPromptData,
  AiderConnectorStatus,
  ChangeRequestItem,
  SkillDefinition,
  SkillsUpdatedData,
  ExtensionOperationResult,
} from '@common/types';
import { ApplicationAPI, RendererProcessMemoryInfo } from '@common/api';
import { type AxiosInstance, create } from 'axios';
import { io, Socket } from 'socket.io-client';
import { compareBaseDirs } from '@common/utils';
import { v4 as uuidv4 } from 'uuid';
⋮----
type EventDataMap = {
  'settings-updated': SettingsData;
  'response-chunk': ResponseChunkData;
  'response-completed': ResponseCompletedData;
  log: LogData;
  'system-log': SystemLogData;
  'context-files-updated': ContextFilesUpdatedData;
  'context-info-updated': ContextInfoData;
  'commands-updated': CommandsData;
  'update-autocompletion': AutocompletionData;
  'ask-question': QuestionData;
  'question-answered': QuestionAnsweredData;
  'queued-prompts-updated': QueuedPromptsUpdatedData;
  'update-aider-models': ModelsData;
  'command-output': CommandOutputData;
  'update-tokens-info': TokensInfoData;
  tool: ToolData;
  'tool-input-chunk': ToolInputChunkData;
  'user-message': UserMessageData;
  'input-history-updated': InputHistoryData;
  'clear-task': ClearTaskData;
  'project-started': ProjectStartedData;
  'provider-models-updated': ProviderModelsData;
  'providers-updated': ProvidersUpdatedData;
  'project-settings-updated': { baseDir: string; settings: ProjectSettings };
  'task-git-status-updated': TaskGitStatusUpdatedData;
  'agent-profiles-updated': AgentProfilesUpdatedData;
  'mcp-servers-updated': McpServersData;
  'extensions-updated': ExtensionsUpdatedData;
  'updated-files-updated': UpdatedFilesUpdatedData;
  'skills-updated': SkillsUpdatedData;
  notification: NotificationData;
  'task-created': TaskCreatedData;
  'task-initialized': TaskData;
  'task-updated': TaskData;
  'task-deleted': TaskData;
  'task-started': TaskData;
  'task-completed': TaskData;
  'task-cancelled': TaskData;
  'message-removed': MessageRemovedData;
  'terminal-data': TerminalData;
  'terminal-exit': TerminalExitData;
  'extension-ui-refresh': ExtensionUIRefreshData;
  'modal-overlay-url': ModalOverlayUrlData;
  'input-prompt': InputPromptData;
  'aider-connector-status': { baseDir?: string; taskId?: string; status: AiderConnectorStatus };
};
⋮----
type EventCallback<T> = (data: T) => void;
⋮----
interface ListenerEntry<T> {
  callback: EventCallback<T>;
  baseDir?: string;
  taskId?: string;
  taskDir?: string;
}
⋮----
class UnsupportedError extends Error
⋮----
constructor(message: string)
⋮----
export class BrowserApi implements ApplicationAPI
⋮----
constructor()
⋮----
// Allow overriding the API port via query param (e.g. when opening the dev renderer in a browser against a dev server)
⋮----
// eslint-disable-next-line no-console
⋮----
// 'reconnect' is only emitted on the Manager (socket.io does not forward it to the Socket)
⋮----
// eslint-disable-next-line no-console
⋮----
// Filter by baseDir
⋮----
// Filter by taskId for task-level events
⋮----
// Filter by taskDir for directory-scoped events (paths compared cross-platform)
⋮----
private ensureSocketConnected(): Promise<void>
⋮----
const cleanup = () =>
⋮----
const onConnect = () =>
⋮----
const onDisconnect = () =>
⋮----
private addListener<T extends keyof EventDataMap>(
    eventType: T,
    callback: EventCallback<EventDataMap[T]>,
    baseDir?: string,
    taskId?: string,
    taskDir?: string,
): () => void
⋮----
private async post<B, R>(endpoint: string, body: B): Promise<R>
⋮----
private async get<T>(endpoint: string, params?: Record<string, unknown>): Promise<T>
⋮----
private async patch<B, R>(endpoint: string, body: B): Promise<R>
⋮----
private async delete<T>(endpoint: string, params?: Record<string, unknown>): Promise<T>
⋮----
private async deleteWithBody<B, T>(endpoint: string, body: B): Promise<T>
⋮----
private async put<B, R>(endpoint: string, body: B, params?: Record<string, unknown>): Promise<R>
⋮----
getRendererProcessMemoryInfo(): Promise<RendererProcessMemoryInfo | null>
isOpenLogsDirectorySupported(): boolean
openLogsDirectory(): Promise<boolean>
loadSettings(): Promise<SettingsData>
saveSettings(settings: SettingsData): Promise<SettingsData>
startProject(baseDir: string): Promise<void>
stopProject(baseDir: string): void
⋮----
restartProject(baseDir: string): void
resetTask(baseDir: string, taskId: string): void
restartAiderConnector(baseDir: string, taskId: string): void
runPrompt(baseDir: string, taskId: string, prompt: string, mode?: Mode, images?: string[]): void
savePrompt(baseDir: string, taskId: string, prompt: string): Promise<void>
saveEditedPrompt(baseDir: string, taskId: string, messageId: string, prompt: string): Promise<void>
redoUserPrompt(baseDir: string, taskId: string, messageId: string, mode: Mode, updatedPrompt?: string, updatedImages?: string[]): void
resumeTask(baseDir: string, taskId: string): void
answerQuestion(baseDir: string, taskId: string, answer: string): void
removeQueuedPrompt(baseDir: string, taskId: string, promptId: string): void
sendQueuedPromptNow(baseDir: string, taskId: string, promptId: string): void
reorderQueuedPrompts(baseDir: string, taskId: string, prompts: QueuedPromptData[]): void
editQueuedPrompt(baseDir: string, taskId: string, promptId: string, newText: string): void
loadInputHistory(baseDir: string): Promise<string[]>
isOpenDialogSupported(): boolean
showOpenDialog(options: OpenDialogOptions): Promise<OpenDialogResult>
getPathForFile(file: File): string
getOpenProjects(): Promise<ProjectData[]>
addOpenProject(baseDir: string): Promise<ProjectData[]>
setActiveProject(baseDir: string): Promise<ProjectData[]>
removeOpenProject(baseDir: string): Promise<ProjectData[]>
openNewWindow(): Promise<void>
⋮----
// In browser mode, open a new tab with the home page
⋮----
openProjectInNewWindow(baseDir: string): Promise<void>
⋮----
// In browser mode, open a new tab with the specific project
⋮----
updateOpenProjectsOrder(baseDirs: string[]): Promise<ProjectData[]>
updateMainModel(baseDir: string, taskId: string, model: string): void
updateWeakModel(baseDir: string, taskId: string, model: string): void
updateArchitectModel(baseDir: string, taskId: string, model: string): void
updateEditFormats(baseDir: string, editFormats: Record<string, EditFormat>): void
getProjectSettings(baseDir: string): Promise<ProjectSettings>
patchProjectSettings(baseDir: string, settings: Partial<ProjectSettings>): Promise<ProjectSettings>
getFilePathSuggestions(currentPath: string, directoriesOnly?: boolean): Promise<string[]>
getAddableFiles(baseDir: string, taskId: string): Promise<string[]>
getAllFiles(baseDir: string, taskId: string, useGit?: boolean): Promise<string[]>
refreshContextFiles(baseDir: string, taskId: string): Promise<void>
getUpdatedFiles(baseDir: string, taskId: string): Promise<
getUpdatedFileDiff(baseDir: string, taskId: string, filePath: string, commitHash?: string): Promise<string>
getUpdatedFileContents(baseDir: string, taskId: string, filePath: string, commitHash?: string): Promise<
async generateCommitMessage(baseDir: string, taskId: string, filePaths?: string[]): Promise<string>
async commitChanges(baseDir: string, taskId: string, message: string, amend: boolean, skipGitHooks: boolean, filePaths?: string[]): Promise<void>
async cancelCommitChanges(baseDir: string, taskId: string): Promise<void>
addFile(baseDir: string, taskId: string, filePath: string, readOnly?: boolean): void
async isValidPath(baseDir: string, path: string): Promise<boolean>
async isProjectPath(path: string): Promise<boolean>
async cloneProject(repositoryUrl: string, targetDir?: string): Promise<string>
async cancelCloneProject(): Promise<void>
dropFile(baseDir: string, taskId: string, path: string): void
runCommand(baseDir: string, taskId: string, command: string): void
async pasteImage(baseDir: string, taskId: string, imageBuffer?: ArrayBuffer): Promise<void>
scrapeWeb(baseDir: string, taskId: string, url: string, filePath?: string): Promise<void>
initProjectRulesFile(baseDir: string, taskId: string, args?: string): Promise<void>
getSkills(baseDir: string, taskId: string): Promise<SkillDefinition[]>
async activateSkill(baseDir: string, taskId: string, skillName: string): Promise<boolean>
deactivateSkill(baseDir: string, taskId: string, skillName: string): Promise<void>
getTodos(baseDir: string, taskId: string): Promise<TodoItem[]>
addTodo(baseDir: string, taskId: string, name: string): Promise<TodoItem[]>
updateTodo(baseDir: string, taskId: string, name: string, updates: Partial<TodoItem>): Promise<TodoItem[]>
deleteTodo(baseDir: string, taskId: string, name: string): Promise<TodoItem[]>
clearAllTodos(baseDir: string, taskId: string): Promise<TodoItem[]>
loadMcpServerTools(serverName: string, config?: McpServerConfig, projectDir?: string): Promise<McpTool[] | null>
reloadMcpServers(projectDir?: string, force = false): Promise<void>
reloadMcpServer(serverName: string, config: McpServerConfig): Promise<McpTool[]>
getMcpOAuthStatus(serverName: string, config?: McpServerConfig, projectDir?: string): Promise<McpOAuthStatusData>
async startMcpOAuth(serverName: string, config?: McpServerConfig, projectDir?: string): Promise<string>
disconnectMcpOAuth(serverName: string, config?: McpServerConfig, projectDir?: string): Promise<void>
getMcpServers(): Promise<McpServersData>
addMcpServer(name: string, config: McpServerConfig, projectDir?: string): Promise<McpServersData>
updateMcpServer(oldName: string, name: string, config: McpServerConfig, projectDir?: string): Promise<McpServersData>
removeMcpServer(name: string, projectDir?: string): Promise<McpServersData>
replaceMcpServers(servers: Record<string, McpServerConfig>, projectDir?: string): Promise<McpServersData>
createNewTask(baseDir: string, params?: CreateTaskParams): Promise<TaskData>
updateTask(baseDir: string, id: string, updates: Partial<TaskData>): Promise<boolean>
deleteTask(baseDir: string, id: string): Promise<boolean>
duplicateTask(baseDir: string, taskId: string): Promise<TaskData>
forkTask(baseDir: string, taskId: string, messageId: string): Promise<TaskData>
getTasks(baseDir: string): Promise<TaskData[]>
loadTask(baseDir: string, id: string): Promise<TaskStateData>
⋮----
async exportTaskToMarkdown(baseDir: string, taskId: string, copyOnly: boolean = false): Promise<string | void>
⋮----
getRecentProjects(): Promise<string[]>
addRecentProject(baseDir: string): Promise<void>
removeRecentProject(baseDir: string): Promise<void>
interruptResponse(baseDir: string, taskId: string, interruptId?: string): void
applyEdits(baseDir: string, taskId: string, edits: FileEdit[]): void
⋮----
clearContext(baseDir: string, taskId: string): void
removeLastMessage(baseDir: string, taskId: string): void
⋮----
async removeMessage(baseDir: string, taskId: string, messageId: string): Promise<void>
⋮----
async removeMessagesUpTo(baseDir: string, taskId: string, messageId: string): Promise<void>
compactConversation(baseDir: string, taskId: string, mode: Mode, customInstructions?: string): void
⋮----
async smartCompactConversation(baseDir: string, taskId: string): Promise<void>
⋮----
async undoContextChange(baseDir: string, taskId: string): Promise<boolean>
⋮----
async handoffConversation(baseDir: string, taskId: string, focus?: string): Promise<void>
⋮----
runCodeChangeRequests(baseDir: string, taskId: string, requests: ChangeRequestItem[], createNewTask?: boolean): void
⋮----
setZoomLevel(level: number): Promise<void>
⋮----
// eslint-disable-next-line no-console
⋮----
getVersions(forceRefresh = false): Promise<VersionsInfo | null>
downloadLatestAiderDesk(): Promise<void>
async getReleaseNotes(): Promise<string | null>
clearReleaseNotes(): Promise<void>
async getOS(): Promise<OS>
getProviderModels(reload?: boolean): Promise<ProviderModelsData>
getProviders(): Promise<ProviderProfile[]>
updateProviders(providers: ProviderProfile[]): Promise<ProviderProfile[]>
upsertModel(providerId: string, modelId: string, model: Model): Promise<ProviderModelsData>
deleteModel(providerId: string, modelId: string): Promise<ProviderModelsData>
updateModels(modelUpdates: Array<
queryUsageData(from: string, to: string): Promise<UsageDataRow[]>
getEffectiveEnvironmentVariable(key: string, baseDir?: string): Promise<EnvironmentVariable | undefined>
⋮----
// Voice API
createVoiceSession(provider: ProviderProfile): Promise<VoiceSession>
⋮----
addSettingsUpdatedListener(callback: (data: SettingsData) => void): () => void
addResponseChunkListener(baseDir: string, taskId: string, callback: (data: ResponseChunkData) => void): () => void
addResponseCompletedListener(baseDir: string, taskId: string, callback: (data: ResponseCompletedData) => void): () => void
addLogListener(baseDir: string, taskId: string, callback: (data: LogData) => void): () => void
addContextFilesUpdatedListener(baseDir: string, taskId: string, callback: (data: ContextFilesUpdatedData) => void): () => void
addContextInfoUpdatedListener(baseDir: string, taskId: string, callback: (data: ContextInfoData) => void): () => void
addUpdatedFilesUpdatedListener(baseDir: string, taskDir: string, callback: (data: UpdatedFilesUpdatedData) => void): () => void
addSkillsUpdatedListener(baseDir: string, taskId: string, callback: (data: SkillsUpdatedData) => void): () => void
addCommandsUpdatedListener(baseDir: string, callback: (data: CommandsData) => void): () => void
addUpdateAutocompletionListener(baseDir: string, taskId: string, callback: (data: AutocompletionData) => void): () => void
addAskQuestionListener(baseDir: string, taskId: string, callback: (data: QuestionData) => void): () => void
⋮----
addQuestionAnsweredListener(baseDir: string, taskId: string, callback: (data: QuestionAnsweredData) => void): () => void
addQueuedPromptsUpdatedListener(baseDir: string, taskId: string, callback: (data: QueuedPromptsUpdatedData) => void): () => void
addUpdateAiderModelsListener(baseDir: string, taskId: string, callback: (data: ModelsData) => void): () => void
addCommandOutputListener(baseDir: string, taskId: string, callback: (data: CommandOutputData) => void): () => void
addTokensInfoListener(baseDir: string, taskId: string, callback: (data: TokensInfoData) => void): () => void
addToolListener(baseDir: string, taskId: string, callback: (data: ToolData) => void): () => void
addToolInputChunkListener(baseDir: string, taskId: string, callback: (data: ToolInputChunkData) => void): () => void
addUserMessageListener(baseDir: string, taskId: string, callback: (data: UserMessageData) => void): () => void
addInputHistoryUpdatedListener(baseDir: string, callback: (data: InputHistoryData) => void): () => void
addClearTaskListener(baseDir: string, taskId: string, callback: (data: ClearTaskData) => void): () => void
⋮----
addMessageRemovedListener(baseDir: string, taskId: string, callback: (data: MessageRemovedData) => void): () => void
addProjectStartedListener(baseDir: string, callback: (data: ProjectStartedData) => void): () => void
addVersionsInfoUpdatedListener(callback: (data: VersionsInfo) => void): () => void
⋮----
addSocketReconnectListener(callback: () => void): () => void
⋮----
addProviderModelsUpdatedListener(callback: (data: ProviderModelsData) => void): () => void
⋮----
addProvidersUpdatedListener(callback: (data: ProvidersUpdatedData) => void): () => void
⋮----
addAgentProfilesUpdatedListener(callback: (data: AgentProfilesUpdatedData) => void): () => void
⋮----
addMcpServersUpdatedListener(callback: (data: McpServersData) => void): () => void
⋮----
addExtensionsUpdatedListener(callback: (data: ExtensionsUpdatedData) => void): () => void
⋮----
addProjectSettingsUpdatedListener(baseDir: string, callback: (data:
⋮----
addTaskGitStatusUpdatedListener(baseDir: string, taskId: string, callback: (data: TaskGitStatusUpdatedData) => void): () => void
⋮----
// Task lifecycle event listeners
addTaskCreatedListener(baseDir: string, callback: (data: TaskCreatedData) => void): () => void
⋮----
addTaskInitializedListener(baseDir: string, callback: (data: TaskData) => void): () => void
⋮----
addTaskUpdatedListener(baseDir: string, callback: (data: TaskData) => void): () => void
⋮----
addTaskStartedListener(baseDir: string, callback: (data: TaskData) => void): () => void
⋮----
addTaskCompletedListener(baseDir: string, callback: (data: TaskData) => void): () => void
⋮----
addTaskCancelledListener(baseDir: string, callback: (data: TaskData) => void): () => void
⋮----
addTaskDeletedListener(baseDir: string, callback: (data: TaskData) => void): () => void
addTerminalDataListener(baseDir: string, callback: (data: TerminalData) => void): () => void
addTerminalExitListener(baseDir: string, callback: (data: TerminalExitData) => void): () => void
addContextMenuListener(callback: (params: ContextMenuParams) => void): () => void
addShowViewListener(callback: (viewId: string) => void): () => void
async getCommands(baseDir: string): Promise<CommandsData>
getCustomModes(baseDir?: string): Promise<ModeDefinition[]>
runCustomCommand(baseDir: string, taskId: string, commandName: string, args: string[], mode: Mode, images?: string[]): Promise<void>
isTerminalSupported(): boolean
async createTerminal(baseDir: string, taskId: string, cols?: number, rows?: number): Promise<string>
async writeToTerminal(terminalId: string, data: string): Promise<boolean>
⋮----
private flushTerminalWrite(terminalId: string): Promise<boolean>
async resizeTerminal(terminalId: string, cols: number, rows: number): Promise<boolean>
async closeTerminal(terminalId: string): Promise<boolean>
async getTerminalForTask(taskId: string): Promise<string | null>
async getAllTerminalsForTask(taskId: string): Promise<Array<
async getTerminalBuffer(terminalId: string): Promise<
isManageServerSupported(): boolean
⋮----
startServer(username?: string, password?: string): Promise<boolean>
⋮----
// Server control not supported in browser mode
⋮----
stopServer(): Promise<boolean>
⋮----
// Server control not supported in browser mode
⋮----
startCloudflareTunnel(): Promise<boolean>
⋮----
stopCloudflareTunnel(): Promise<void>
⋮----
getCloudflareTunnelStatus(): Promise<CloudflareTunnelStatus>
⋮----
// Worktree merge operations
mergeWorktreeToMain(baseDir: string, taskId: string, squash: boolean, targetBranch?: string, commitMessage?: string): Promise<void>
⋮----
switchToLocalWorkingMode(baseDir: string, taskId: string, options?: SwitchToLocalOptions): Promise<void>
⋮----
switchToWorktreeWorkingMode(baseDir: string, taskId: string, options?: SwitchToWorktreeOptions): Promise<void>
⋮----
getLocalUncommittedFiles(baseDir: string, taskId: string): Promise<UncommittedFiles>
⋮----
applyUncommittedChanges(baseDir: string, taskId: string): Promise<void>
⋮----
revertLastMerge(baseDir: string, taskId: string): Promise<void>
⋮----
addFileToGit(baseDir: string, taskId: string, filePath: string): Promise<void>
⋮----
restoreFile(baseDir: string, taskId: string, filePath: string): Promise<void>
⋮----
async readFile(baseDir: string, taskId: string, filePath: string): Promise<string>
⋮----
async saveFile(baseDir: string, taskId: string, filePath: string, content: string): Promise<void>
⋮----
listBranches(baseDir: string): Promise<Array<
⋮----
listGitBranches(baseDir: string, taskId: string, includeRemote?: boolean): Promise<BranchInfo[]>
⋮----
getSyncCommits(baseDir: string, taskId: string, targetBranch?: string): Promise<GitSyncCommits>
⋮----
async createGitBranch(baseDir: string, taskId: string, name: string, startPoint?: string, checkout?: boolean): Promise<void>
⋮----
async checkoutGitBranch(baseDir: string, taskId: string, branch: string, createTracking?: boolean, takeOver?: boolean): Promise<void>
⋮----
async deleteGitBranch(baseDir: string, taskId: string, branch: string, force?: boolean): Promise<void>
⋮----
mergeIntoCurrentBranch(baseDir: string, taskId: string, branch: string): Promise<
⋮----
rebaseOntoBranch(baseDir: string, taskId: string, branch: string): Promise<
⋮----
updateGitBranch(baseDir: string, taskId: string, branchName: string): Promise<
⋮----
isGitRepository(baseDir: string, taskId: string): Promise<boolean>
⋮----
initializeGitRepository(baseDir: string, taskId: string): Promise<void>
⋮----
gitPull(baseDir: string, taskId: string, rebase?: boolean): Promise<
⋮----
gitPush(baseDir: string, taskId: string, force?: boolean): Promise<
⋮----
resolveGitErrorWithAgent(baseDir: string, taskId: string): Promise<void>
⋮----
getTaskGitStatus(baseDir: string, taskId: string, targetBranch?: string): Promise<TaskGitStatus>
⋮----
rebaseWorktreeFromBranch(baseDir: string, taskId: string, fromBranch?: string): Promise<void>
⋮----
abortRebase(baseDir: string, taskId: string): Promise<void>
⋮----
continueRebase(baseDir: string, taskId: string): Promise<void>
⋮----
resolveConflictsWithAgent(baseDir: string, taskId: string): Promise<void>
⋮----
renameGitBranch(baseDir: string, taskId: string, newBranchName: string): Promise<void>
⋮----
renameWorktreeBranch(baseDir: string, taskId: string, newBranchName: string): Promise<void>
⋮----
// Memory operations
listAllMemories(): Promise<MemoryEntry[]>
⋮----
async deleteMemory(id: string): Promise<boolean>
⋮----
getMemoryEmbeddingProgress(): Promise<MemoryEmbeddingProgress>
⋮----
async deleteScopeMemories(scopeId: string): Promise<number>
⋮----
async writeToClipboard(text: string): Promise<void>
⋮----
// Fallback for mobile browsers and non-secure contexts
⋮----
async writeImageToClipboard(dataUrl: string): Promise<void>
⋮----
async openPath(): Promise<boolean>
⋮----
// Not available in browser context
⋮----
async openUrlInWindow(url: string): Promise<void>
⋮----
async openUrlExternally(url: string): Promise<void>
⋮----
// Agent profile operations
getAllAgentProfiles(baseDir?: string): Promise<AgentProfile[]>
⋮----
createAgentProfile(profile: AgentProfile, projectDir?: string): Promise<AgentProfile[]>
⋮----
updateAgentProfile(profile: AgentProfile, baseDir?: string): Promise<AgentProfile[]>
⋮----
deleteAgentProfile(profileId: string, baseDir?: string): Promise<AgentProfile[]>
⋮----
updateAgentProfilesOrder(agentProfiles: AgentProfile[]): Promise<void>
⋮----
addNotificationListener(baseDir: string, callback: (data: NotificationData) => void): () => void
⋮----
// System logs
getSystemLogs(fromId?: number, limit?: number, levels?: SystemLogLevel[]): Promise<SystemLogsResponse>
⋮----
clearSystemLogs(): Promise<void>
⋮----
addSystemLogListener(callback: (data: SystemLogData) => void): () => void
⋮----
getInstalledExtensions(projectDir?: string): Promise<InstalledExtension[]>
⋮----
getExtensionToolsInfo(projectDir?: string): Promise<ExtensionToolInfo[]>
⋮----
getAvailableExtensions(repositories: string[], forceRefresh?: boolean, fetchOnly?: boolean): Promise<AvailableExtension[]>
⋮----
installExtension(extensionId: string, repositoryUrl: string, projectDir?: string): Promise<ExtensionOperationResult>
⋮----
uninstallExtension(extensionId: string, projectDir?: string): Promise<boolean>
⋮----
updateExtension(extensionId: string, repositoryUrl: string, projectDir?: string): Promise<ExtensionOperationResult>
⋮----
reloadExtension(filePath: string, projectDir?: string): Promise<boolean>
⋮----
getExtensionUIComponents(placement?: string, projectDir?: string, taskId?: string): Promise<ExtensionUIComponent[]>
⋮----
getUIExtensionData(extensionId: string, componentId: string, projectDir?: string, taskId?: string): Promise<unknown>
⋮----
executeUIExtensionAction(extensionId: string, componentId: string, action: string, args: unknown[], projectDir?: string, taskId?: string): Promise<unknown>
⋮----
// Extension config operations (per-extension settings)
getExtensionConfigComponent(extensionId: string, projectDir?: string): Promise<ExtensionConfigComponent | null>
⋮----
getExtensionConfig(extensionId: string, projectDir?: string): Promise<unknown>
⋮----
saveExtensionConfig(extensionId: string, configData: unknown, projectDir?: string): Promise<unknown>
⋮----
onExtensionUIRefresh(callback: (data: ExtensionUIRefreshData) => void): () => void
⋮----
onModalOverlayUrl(callback: (data: ModalOverlayUrlData) => void): () => void
⋮----
onInputPrompt(callback: (data: InputPromptData) => void): () => void
⋮----
respondInputPrompt(id: string, value: string | null, rememberSession?: boolean): Promise<void>
⋮----
loadExtensionLibrary(librarySpec: string): Promise<string>
⋮----
isWebViewSupported(): boolean
⋮----
addAiderConnectorStatusListener(
    callback: (data: { baseDir?: string; taskId?: string; status: AiderConnectorStatus }) => void,
    baseDir?: string,
    taskId?: string,
): () => void
⋮----
async getAiderConnectorStatus(): Promise<AiderConnectorStatus>
⋮----
destroy(): void

================
File: src/renderer/src/api/readonly-browser-api.ts
================
import { ExtensionDisplayAPI } from '@common/api';
import { BrowserBootstrap, ExtensionUIComponent, ExtensionUIRefreshData, TaskData, TaskStateData } from '@common/types';
import { type AxiosInstance, create } from 'axios';
import { io, Socket } from 'socket.io-client';
⋮----
export type ReadonlyEvent = {
  type: string;
  data: unknown;
};
⋮----
type EventListener = (event: ReadonlyEvent) => void;
⋮----
const getBaseUrl = (): string =>
⋮----
// Allow overriding the API port via query param (e.g. when opening the dev renderer in a browser against a dev server)
⋮----
export const loadBrowserBootstrap = async (): Promise<BrowserBootstrap> =>
⋮----
export class ReadonlyBrowserApi implements ExtensionDisplayAPI
⋮----
constructor(private readonly projectDir: string)
⋮----
// 'reconnect' is only emitted on the Manager (socket.io does not forward it to the Socket)
⋮----
destroy(): void
⋮----
onReconnect(listener: () => void): () => void
⋮----
onEvent(callback: EventListener): () => void
⋮----
async getTasks(): Promise<TaskData[]>
⋮----
async loadTask(taskId: string): Promise<TaskStateData>
⋮----
async getExtensionUIComponents(placement?: string, projectDir?: string, taskId?: string): Promise<ExtensionUIComponent[]>
⋮----
async getUIExtensionData(extensionId: string, componentId: string, projectDir?: string, taskId?: string): Promise<unknown>
⋮----
async executeUIExtensionAction(
    extensionId: string,
    componentId: string,
    action: string,
    args: unknown[],
    projectDir?: string,
    taskId?: string,
): Promise<unknown>
⋮----
onExtensionUIRefresh(callback: (data: ExtensionUIRefreshData) => void): () => void
⋮----
async loadExtensionLibrary(librarySpec: string): Promise<string>

================
File: src/renderer/src/components/AgentSelector/AgentSelector.tsx
================
import { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { MdCheck, MdFlashOn, MdOutlineChecklist, MdOutlineFileCopy, MdOutlineHdrAuto, MdOutlineMap, MdPsychology } from 'react-icons/md';
import { HiUserGroup } from 'react-icons/hi';
import { RiToolsFill } from 'react-icons/ri';
import { clsx } from 'clsx';
import { AgentProfile, InvocationMode, TaskData, ToolApprovalState } from '@common/types';
import { isSubagentEnabled } from '@common/agent';
import { BiCog } from 'react-icons/bi';
import { TOOL_GROUP_NAME_SEPARATOR } from '@common/tools';
import { useHotkeys } from 'react-hotkeys-hook';
import { LuBrain, LuClipboardList } from 'react-icons/lu';
⋮----
import { Checkbox } from '../common/Checkbox';
⋮----
import { McpServerSelectorItem } from './McpServerSelectorItem';
import { ExtensionToolsSelectorItem } from './ExtensionToolsSelectorItem';
⋮----
import { useClickOutside } from '@/hooks/useClickOutside';
import { useExtensionTools } from '@/hooks/useExtensionTools';
import { IconButton } from '@/components/common/IconButton';
import { Accordion } from '@/components/common/Accordion';
import { Tooltip } from '@/components/ui/Tooltip';
import { useMcpServers } from '@/contexts/McpServersContext';
import { useProjectSettings } from '@/contexts/ProjectSettingsContext';
import { useApi } from '@/contexts/ApiContext';
import { useAgents } from '@/contexts/AgentsContext';
import { useTask } from '@/contexts/TasksContext';
import { useActiveAgentProfile } from '@/utils/agents';
import { openSettingsPage } from '@/stores/settingsNavigationStore';
⋮----
export type AgentSelectorRef = {
  open: () => void;
};
⋮----
type Props = {
  projectDir: string;
  task: TaskData;
  isActive: boolean;
};
⋮----
const calculateEnabledTools = async () =>
⋮----
// eslint-disable-next-line no-console
⋮----
// eslint-disable-next-line no-console
⋮----
// Remove tool approvals for this server
⋮----
const handleToggleSubagent = (subagentProfile: AgentProfile) =>
⋮----
if (activeGlobalProfile)

================
File: src/renderer/src/components/AgentSelector/ExtensionToolsSelectorItem.tsx
================
import { ExtensionToolInfo, ToolApprovalState } from '@common/types';
import { TOOL_GROUP_NAME_SEPARATOR } from '@common/tools';
import { useTranslation } from 'react-i18next';
⋮----
import { Checkbox } from '../common/Checkbox';
⋮----
type Props = {
  extensionInfo: ExtensionToolInfo;
  disabled: boolean;
  toolApprovals: Record<string, ToolApprovalState>;
  onToggle: (extensionId: string) => void;
};
⋮----
export const ExtensionToolsSelectorItem = (
⋮----
const handleToggle = ()

================
File: src/renderer/src/components/AgentSelector/index.ts
================


================
File: src/renderer/src/components/AgentSelector/McpServerSelectorItem.tsx
================
import { useEffect, useState } from 'react';
import { CgSpinner } from 'react-icons/cg';
import { McpServerConfig, ToolApprovalState } from '@common/types';
import { TOOL_GROUP_NAME_SEPARATOR } from '@common/tools';
import { useTranslation } from 'react-i18next';
⋮----
import { Checkbox } from '../common/Checkbox';
⋮----
import { useApi } from '@/contexts/ApiContext';
⋮----
type Props = {
  serverName: string;
  config?: McpServerConfig;
  projectDir?: string;
  disabled: boolean;
  toolApprovals: Record<string, ToolApprovalState>;
  onToggle: (serverName: string) => void;
};
⋮----
export const McpServerSelectorItem = (
⋮----
const loadTools = async () =>
⋮----
// eslint-disable-next-line no-console

================
File: src/renderer/src/components/common/CommandPalette/CommandPalette.tsx
================
import { CommandPaletteContent } from './CommandPaletteContent';
⋮----
import { useOverlayRegistration } from '@/hooks/useOverlayFocusRestore';
import { useCommandPaletteStore } from '@/stores/commandPaletteStore';
⋮----
export const CommandPalette = () =>

================
File: src/renderer/src/components/common/CommandPalette/CommandPaletteContent.tsx
================
import { ChangeEvent, KeyboardEvent, MouseEvent, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { clsx } from 'clsx';
import { useLocalStorage } from '@reactuses/core';
⋮----
import { TaskStateChip } from '@/components/common/TaskStateChip';
import { PaletteItem, PaletteItemType, useCommandPaletteStore } from '@/stores/commandPaletteStore';
⋮----
enum PaletteTab {
  All = 'all',
  Files = 'file',
  Actions = 'action',
  Tasks = 'task',
  Projects = 'project',
}
⋮----
const fuzzyMatch = (query: string, text: string):
⋮----
const getTabForItem = (item: PaletteItem): PaletteTab =>
⋮----
const getMatches = (itemsToSearch: PaletteItem[])
⋮----
const handleEscape = (event: globalThis.KeyboardEvent) =>
⋮----
const handleListScroll = () =>
⋮----
const handleSearchChange = (event: ChangeEvent<HTMLInputElement>) =>
⋮----
const handleBackdropClick = () =>
⋮----
const handleContentClick = (event: MouseEvent<HTMLDivElement>) =>
⋮----
onClick=

================
File: src/renderer/src/components/common/CommandPalette/index.ts
================


================
File: src/renderer/src/components/common/DiffViewer/CompactDiffViewer.tsx
================
import { useMemo, type TableHTMLAttributes } from 'react';
import { diffLines, formatLines } from 'unidiff';
import { getLanguageFromPath } from '@common/utils';
import { clsx } from 'clsx';
import { HiOutlineSelector } from 'react-icons/hi';
⋮----
import { parseDiff, File, Hunk as HunkType, SkipBlock, Line as LineType } from './utils';
⋮----
import { highlightWithRefractor } from '@/utils/highlighter';
import { useRefractorLanguage } from '@/hooks/useRefractorLanguage';
import { DiffContextProvider, useDiffContext } from '@/contexts/DiffViewerContext';
⋮----
export interface DiffProps extends TableHTMLAttributes<HTMLTableElement>, Pick<File, 'hunks' | 'type'> {
  fileName?: string;
  language?: string;
}
⋮----
type HunkProps = { hunk: HunkType | SkipBlock };
⋮----
<tr className=
⋮----
className=

================
File: src/renderer/src/components/common/DiffViewer/DiffLineCommentPanel.tsx
================
import { useState, useRef, KeyboardEvent } from 'react';
import { createPortal } from 'react-dom';
import { useTranslation } from 'react-i18next';
⋮----
import { Button } from '@/components/common/Button';
import { useClickOutside } from '@/hooks/useClickOutside';
⋮----
type Props = {
  onSubmit: (comment: string) => void;
  onCancel: () => void;
  anchorRect: { top: number; left: number };
  initialText?: string;
};
⋮----
const handleSubmit = () =>
⋮----
const handleKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) =>

================
File: src/renderer/src/components/common/DiffViewer/DiffViewer.scss
================
.diff-viewer {
  .diff-gutter {
    color: var(--color-text-muted);
    text-align: center; // Better alignment for line numbers

    &.diff-gutter-omit {
      &:before {
        background-color: var(--color-bg-tertiary);
      }
    }
  }

  .diff-code {
    padding: 0;
    font-family: 'Sono', monospace;
  }

  .diff-code-insert {
    background-color: var(--color-bg-diff-viewer-new-primary);

    .diff-code-edit {
      background-color: var(--color-bg-diff-viewer-new-secondary);
    }
  }

  .diff-code-delete {
    background-color: var(--color-bg-diff-viewer-old-primary);

    .diff-code-edit {
      background-color: var(--color-bg-diff-viewer-old-secondary);
    }
  }

  .diff-gutter-insert {
    background-color: var(--color-bg-diff-viewer-new-primary);
    width: 16px;
    min-width: 16px;
    max-width: 16px;
  }

  .diff-gutter-delete {
    background-color: var(--color-bg-diff-viewer-old-primary);
  }

  ::selection {
    background-color: var(--color-bg-tertiary);
    color: var(--color-text-primary);
  }

  &.selectable {
    .diff-line-hover-new,
    .diff-line-hover-old,
    .diff-gutter-selected,
    .diff-code-selected {
      cursor: pointer;
      background-color: var(--color-bg-tertiary) !important;
    }

    .diff-code-insert:hover,
    .diff-code-normal:hover{
      cursor: pointer;
      background-color: var(--color-bg-tertiary) !important;
    }
  }
}

================
File: src/renderer/src/components/common/DiffViewer/DiffViewer.tsx
================
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { Diff, Hunk, parseDiff } from 'react-diff-view';
import { diffLines, formatLines } from 'unidiff';
import { useDebounce } from '@reactuses/core';
import { DiffViewMode } from '@common/types';
⋮----
import { createTokens } from './utils';
⋮----
import { useRefractorLanguage } from '@/hooks/useRefractorLanguage';
import { useResponsive } from '@/hooks/useResponsive';
⋮----
type Props = {
  oldValue: string;
  newValue: string;
  language: string;
  isComplete?: boolean;
  viewMode?: DiffViewMode;
};
⋮----
// eslint-disable-next-line no-console
⋮----
// No error, but no diff file (e.g., identical content)
// While streaming, args may be partial/identical - show nothing until completed
if (!isComplete)

================
File: src/renderer/src/components/common/DiffViewer/index.tsx
================


================
File: src/renderer/src/components/common/DiffViewer/PierreDiffViewer.scss
================
diffs-container {
  --diffs-bg: var(--color-bg-code-block);
  --diffs-bg-separator: var(--color-bg-primary);
  --diffs-fg-number: var(--color-text-muted);
  --diffs-font-family:'Sono', monospace;
  --diffs-header-font-family: 'Sono', monospace;
  --diffs-modified-base: var(--color-text-tertiary);
  font-size: 12px;
  line-height: 16px;

  background-color: transparent !important;
}

================
File: src/renderer/src/components/common/DiffViewer/PierreDiffViewer.tsx
================
import { type MouseEvent, useCallback, useMemo } from 'react';
import { HiPencil, HiTrash } from 'react-icons/hi';
import { DiffViewMode } from '@common/types';
import { MultiFileDiff, type MultiFileDiffProps, PatchDiff } from '@pierre/diffs/react';
⋮----
import type { DiffLineAnnotation, OnDiffLineClickProps, SelectedLineRange } from '@pierre/diffs';
⋮----
export type PierreLineClickInfo = {
  lineNumber: number;
  side: 'deletions' | 'additions';
  viewportRect: { top: number; left: number };
};
⋮----
export type DiffComment = {
  id: string;
  lineNumber: number;
  comment: string;
};
⋮----
type Props = {
  oldValue?: string;
  newValue?: string;
  udiff?: string;
  fileName?: string;
  showFilename?: boolean;
  viewMode?: DiffViewMode;
  selectedLineNumber?: number | null;
  onLineClick?: (lineInfo: PierreLineClickInfo) => void;
  comments?: DiffComment[];
  onEditComment?: (info: { commentId: string; viewportRect: { top: number; left: number } }) => void;
  onRemoveComment?: (commentId: string) => void;
  /** Full file contents; when provided together with expandContext, renders the whole-file diff with expandable context */
  contents?: { oldContent: string; newContent: string } | null;
  expandContext?: boolean;
};
⋮----
/** Full file contents; when provided together with expandContext, renders the whole-file diff with expandable context */
⋮----
const handleEdit = (e: MouseEvent<HTMLButtonElement>) =>
const handleRemove = ()
⋮----
const ensureTrailingNewline = (s: string)

================
File: src/renderer/src/components/common/DiffViewer/UDiffViewer.tsx
================
import { useMemo, MouseEvent, useCallback } from 'react';
import { Diff, Hunk, parseDiff, getChangeKey } from 'react-diff-view';
import { DiffViewMode } from '@common/types';
import { clsx } from 'clsx';
⋮----
import { createTokens } from './utils';
⋮----
import type { NormalChange } from 'gitdiff-parser';
import type { ChangeData, HunkData } from 'react-diff-view';
⋮----
import { useRefractorLanguage } from '@/hooks/useRefractorLanguage';
import { useResponsive } from '@/hooks/useResponsive';
⋮----
export type LineClickInfo = {
  lineKey: string;
  change: ChangeData;
  lineNumber: number;
  content: string;
};
⋮----
type Props = {
  udiff: string;
  language: string;
  viewMode?: DiffViewMode;
  showFilename?: boolean;
  onLineClick?: (lineInfo: LineClickInfo, event: MouseEvent) => void;
  activeLineKey?: string | null;
};
⋮----
// eslint-disable-next-line no-console

================
File: src/renderer/src/components/common/DiffViewer/utils.ts
================
import { tokenize } from 'react-diff-view';
import { refractor } from 'refractor/all';
import { HunkData } from 'react-diff-view/types/utils';
import gitDiffParser, { Hunk as _Hunk, File as _File, Change as _Change, DeleteChange, InsertChange } from 'gitdiff-parser';
import { diffChars, diffWords } from 'diff';
⋮----
/**
 * Refractor v5 returns a Root node with a children array,
 * but react-diff-view (v3) expects the highlight function to return the array directly.
 */
⋮----
// eslint-disable-next-line no-console
⋮----
// eslint-disable-next-line no-console
⋮----
export const createTokens = (hunks: HunkData[], language: string) =>
⋮----
// eslint-disable-next-line no-console
⋮----
export interface LineSegment {
  value: string;
  type: 'insert' | 'delete' | 'normal';
}
⋮----
type ReplaceKey<T, K extends PropertyKey, V> = T extends unknown ? Omit<T, K> & Record<K, V> : never;
⋮----
export type Line = ReplaceKey<_Change, 'content', LineSegment[]>;
⋮----
export interface Hunk extends Omit<_Hunk, 'changes'> {
  type: 'hunk';
  lines: Line[];
}
⋮----
export interface SkipBlock {
  count: number;
  type: 'skip';
  content: string;
}
⋮----
export interface File extends Omit<_File, 'hunks'> {
  hunks: (Hunk | SkipBlock)[];
}
⋮----
export interface ParseOptions {
  maxDiffDistance: number;
  maxChangeRatio: number;
  mergeModifiedLines: boolean;
  inlineMaxCharEdits: number;
  wordDiff: boolean;
}
⋮----
const calculateChangeRatio = (a: string, b: string): number =>
⋮----
const isSimilarEnough = (a: string, b: string, maxChangeRatio: number): boolean =>
⋮----
const changeToLine = (change: _Change): Line => (
⋮----
const diffCharsIfWithinEditLimit = (
  a: string,
  b: string,
  maxEdits = 4,
):
  | {
      exceededLimit: true;
    }
  | {
      exceededLimit: false;
      diffs: LineSegment[];
    } => {
  const diffs = diffChars(a, b);
⋮----
const buildInlineDiffSegments = (current: _Change, next: _Change, options: ParseOptions): Line['content'] =>
⋮----
const mergeIntoResult = (segment: LineSegment) =>
⋮----
const mergeAdjacentLines = (changes: _Change[], options: ParseOptions): Line[] =>
⋮----
const buildChangeIndices = (changes: _Change[]) =>
⋮----
// TODO: slight penalty for distance?
// TODO: improve performance w binary search?
const findBestInsertForDelete = (changes: _Change[], delIdx: number, insertIdxs: number[], pairOfAdd: Int32Array, options: ParseOptions): number =>
⋮----
const buildInitialPairs = (changes: _Change[], insertIdxs: number[], deleteIdxs: number[], options: ParseOptions) =>
⋮----
const buildUnpairedDeletePrefix = (changes: _Change[], pairOfDel: Int32Array) =>
⋮----
const hasUnpairedDeleteBetween = (unpairedDelPrefix: Int32Array, deleteIdx: number, insertIdx: number) =>
⋮----
const emitNormal = (out: Line[], c: _Change) =>
⋮----
const emitModified = (out: Line[], del: DeleteChange, add: InsertChange, options: ParseOptions) =>
⋮----
const emitLines = (changes: _Change[], pairOfDel: Int32Array, pairOfAdd: Int32Array, unpairedDelPrefix: Int32Array, options: ParseOptions): Line[] =>
⋮----
// Defer emission to paired insert
⋮----
export const mergeModifiedLines = (changes: _Change[], options: ParseOptions): Line[] =>
⋮----
const parseHunk = (hunk: _Hunk, options: ParseOptions): Hunk =>
⋮----
const extractHunkContext = (header: string): string
⋮----
const insertSkipBlocks = (hunks: Hunk[]): (Hunk | SkipBlock)[] =>
⋮----
export const parseDiff = (diff: string, options?: Partial<ParseOptions>): File[] =>

================
File: src/renderer/src/components/common/Accordion.tsx
================
import { ReactNode, useEffect, useRef, useState, Activity } from 'react';
import { FaChevronDown } from 'react-icons/fa';
import { clsx } from 'clsx';
import { useTranslation } from 'react-i18next';
import { twMerge } from 'tailwind-merge';
⋮----
type Props = {
  title: ReactNode;
  children: ReactNode;
  className?: string;
  buttonClassName?: string;
  defaultOpen?: boolean;
  chevronPosition?: 'left' | 'right';
  isOpen?: boolean;
  onOpenChange?: (isOpen: boolean) => void;
  noMaxHeight?: boolean;
  scrollToVisibleWhenExpanded?: boolean;
  showCollapseButton?: boolean;
};
⋮----
const handleOpenChange = () =>
⋮----
handleOpenChange();
⋮----
className=

================
File: src/renderer/src/components/common/AppCrashFallback.tsx
================
import { MouseEvent, useState } from 'react';
import { Trans, useTranslation } from 'react-i18next';
import { BiCheck, BiCopy, BiErrorCircle } from 'react-icons/bi';
⋮----
import { Button } from '@/components/common/Button';
⋮----
type Props = {
  error: Error | null;
};
⋮----
const handleReload = () =>
⋮----
const handleCopy = async (e: MouseEvent<HTMLButtonElement>) =>
⋮----
// eslint-disable-next-line no-console

================
File: src/renderer/src/components/common/AppErrorBoundary.tsx
================
import { Component, ErrorInfo, ReactNode } from 'react';
⋮----
import { AppCrashFallback } from '@/components/common/AppCrashFallback';
⋮----
type Props = {
  children: ReactNode;
};
⋮----
type State = {
  hasError: boolean;
  error: Error | null;
};
⋮----
export class AppErrorBoundary extends Component<Props, State>
⋮----
constructor(props: Props)
⋮----
static getDerivedStateFromError(error: Error): State
⋮----
componentDidCatch(error: Error, errorInfo: ErrorInfo): void
⋮----
// eslint-disable-next-line no-console
⋮----
render(): ReactNode

================
File: src/renderer/src/components/common/BaseDialog.tsx
================
import { FocusTrap } from 'focus-trap-react';
import { ReactNode, useRef } from 'react';
import { createPortal } from 'react-dom';
import { useTranslation } from 'react-i18next';
import { twMerge } from 'tailwind-merge';
import { useHotkeys } from 'react-hotkeys-hook';
⋮----
type Props = {
  title: string;
  onClose?: () => void;
  children: ReactNode;
  contentClass?: string;
  footer?: ReactNode;
  width?: number;
  closeOnEscape?: boolean;
  closeButtonText?: string;
  enableFocusTrap?: boolean;
};
⋮----
className=

================
File: src/renderer/src/components/common/Button.tsx
================
import { ReactNode, MouseEvent } from 'react';
import { twMerge } from 'tailwind-merge';
⋮----
import { Tooltip } from '@/components/ui/Tooltip';
⋮----
export type ButtonVariant = 'contained' | 'text' | 'outline';
export type ButtonColor = 'primary' | 'secondary' | 'tertiary' | 'danger';
export type ButtonSize = 'sm' | 'md' | 'xs';
⋮----
type Props = {
  children: ReactNode;
  onClick?: (e: MouseEvent<HTMLButtonElement>) => void;
  variant?: ButtonVariant;
  color?: ButtonColor;
  className?: string;
  disabled?: boolean;
  autoFocus?: boolean;
  size?: ButtonSize;
  type?: 'button' | 'submit' | 'reset';
  title?: string;
  tooltip?: string;
};
⋮----
export const Button = ({
  children,
  onClick,
  variant = 'contained',
  color = 'primary',
  className = '',
  disabled = false,
  autoFocus = false,
  size = 'md',
  type = 'button',
  title,
  tooltip,
}: Props) =>
⋮----
const renderButton = () =>
⋮----
className=

================
File: src/renderer/src/components/common/Checkbox.tsx
================
import { InputHTMLAttributes, ReactNode } from 'react';
import { clsx } from 'clsx';
⋮----
import { Tooltip } from '@/components/ui/Tooltip';
⋮----
type Props = Omit<InputHTMLAttributes<HTMLInputElement>, 'size' | 'onChange'> & {
  label?: ReactNode;
  checked: boolean;
  onChange: (checked: boolean) => void;
  className?: string;
  size?: 'xs' | 'sm' | 'md';
  tooltip?: string;
};
⋮----
className=
⋮----
className="sr-only" // Hide the actual input but keep it accessible

================
File: src/renderer/src/components/common/Chip.tsx
================
import { IoClose } from 'react-icons/io5';
import { twMerge } from 'tailwind-merge';
⋮----
import { IconButton } from '@/components/common/IconButton';
⋮----
type Props = {
  label: string;
  onRemove?: () => void;
  removeTooltip?: string;
  className?: string;
};
⋮----
className=

================
File: src/renderer/src/components/common/ChipListInput.tsx
================
import { useState, useRef, KeyboardEvent, ReactNode } from 'react';
import { useTranslation } from 'react-i18next';
⋮----
import { Chip } from '@/components/common/Chip';
import { Button } from '@/components/common/Button';
⋮----
type Props = {
  label: ReactNode;
  items: string[];
  onAdd: (item: string) => void;
  onRemove: (item: string) => void;
  placeholder?: string;
  addLabel?: string;
  removeTooltip?: string;
  emptyLabel?: string;
};
⋮----
const handleAdd = () =>
⋮----
const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) =>
⋮----
onRemove=

================
File: src/renderer/src/components/common/CodeBlock.tsx
================
import { useEffect, useMemo, useState, type ReactNode } from 'react';
import { useTranslation } from 'react-i18next';
import { twMerge } from 'tailwind-merge';
import { AiOutlineLoading3Quarters } from 'react-icons/ai';
import { MdKeyboardArrowDown, MdUndo } from 'react-icons/md';
import { VscCode } from 'react-icons/vsc';
import { DiffViewMode } from '@common/types';
⋮----
import { CopyMessageButton } from '../message/CopyMessageButton';
⋮----
import { MermaidDiagram } from './MermaidDiagram';
import { IconButton } from './IconButton';
⋮----
import { DiffViewer, UDiffViewer, CompactDiffViewer } from '@/components/common/DiffViewer';
import { useApi } from '@/contexts/ApiContext';
import { useSettingsStore } from '@/stores/settingsStore';
import { highlightWithLowlight } from '@/utils/highlighter';
⋮----
const isCustomDiffContent = (content: string): boolean =>
⋮----
const isUdiffContent = (content: string): boolean =>
⋮----
const parseDiffContent = (content: string):
⋮----
// We have old value complete and new value being streamed
⋮----
// We have complete diff
⋮----
type Props = {
  baseDir: string;
  taskId?: string;
  language: string;
  children?: string;
  file?: string;
  isComplete?: boolean;
  oldValue?: string;
  newValue?: string;
  className?: string;
  hideCopyButton?: boolean;
};
⋮----
diffOldValue = oldValue!; // Known to be string
diffNewValue = newValue!; // Known to be string
⋮----
const parsed = parseDiffContent(children!); // children is non-null and a diff
⋮----
// Not a diff, display children as plain code (if it exists)
⋮----
// Defer syntax highlighting off the critical render path: the plain-text version renders
// immediately (cheap) and is upgraded to the highlighted version once the browser is idle.
// This keeps mounting large conversation histories fast. The result is cached against its
// source code so a stale highlight from a previous value is never shown.
⋮----
const handleRevertChanges = () =>
⋮----
className=

================
File: src/renderer/src/components/common/CodeInline.tsx
================
import { ReactNode } from 'react';
import { twMerge } from 'tailwind-merge';
⋮----
type Props = {
  className?: string;
  children?: ReactNode;
};
⋮----
export const CodeInline = (
⋮----
className=

================
File: src/renderer/src/components/common/CompactSelect.tsx
================
import { Listbox, ListboxButton, ListboxOption, ListboxOptions } from '@headlessui/react';
import { ReactNode } from 'react';
import { HiCheck, HiChevronDown } from 'react-icons/hi2';
import { clsx } from 'clsx';
⋮----
export type CompactOption = {
  label: ReactNode;
  value: string;
};
⋮----
type Props = {
  options: CompactOption[];
  value: string;
  onChange: (value: string) => void;
  className?: string;
};
⋮----
<div className=
⋮----
clsx(

================
File: src/renderer/src/components/common/ConfirmDialog.tsx
================
import { ReactNode } from 'react';
import { useTranslation } from 'react-i18next';
⋮----
import { BaseDialog } from './BaseDialog';
import { Button, ButtonColor } from './Button';
⋮----
type AdditionalAction = {
  label: string;
  onClick: () => void;
  color?: ButtonColor;
  disabled?: boolean;
};
⋮----
type Props = {
  title: string;
  onConfirm: () => void;
  onCancel: () => void;
  confirmButtonText?: string;
  cancelButtonText?: string;
  contentClass?: string;
  children: ReactNode;
  disabled?: boolean;
  confirmButtonClass?: string;
  confirmButtonColor?: ButtonColor;
  width?: number;
  closeOnEscape?: boolean;
  footerAdditionalComponents?: ReactNode;
  additionalAction?: AdditionalAction;
  enableFocusTrap?: boolean;
};

================
File: src/renderer/src/components/common/DatePicker.css
================
.react-datepicker {
⋮----
.react-datepicker__header {
⋮----
.react-datepicker__triangle {
⋮----
@apply hidden;
⋮----
.react-datepicker__current-month,
⋮----
@apply text-text-primary;
⋮----
.react-datepicker__day-name,
⋮----
.react-datepicker__day:hover {
⋮----
@apply bg-bg-tertiary;
⋮----
.react-datepicker__day--selected,
⋮----
@apply bg-info-light;
⋮----
.react-datepicker__day--keyboard-selected {

================
File: src/renderer/src/components/common/DatePicker.tsx
================
import { forwardRef, ReactNode } from 'react';
import ReactDatePicker, { type DatePickerProps } from 'react-datepicker';
⋮----
import { FaCalendarAlt } from 'react-icons/fa';
import { clsx } from 'clsx';
⋮----
import { Input } from './Input';
⋮----
// eslint-disable-next-line @typescript-eslint/no-explicit-any
⋮----
type Props = DatePickerProps & {
  label?: ReactNode;
};
⋮----
className=

================
File: src/renderer/src/components/common/DevelopmentPerformanceCleanup.tsx
================
import { useEffect } from 'react';
⋮----
export const DevelopmentPerformanceCleanup = () =>
⋮----
// eslint-disable-next-line no-console
⋮----
const checkAndCleanup = async () =>

================
File: src/renderer/src/components/common/FileChip.tsx
================
import { useTranslation } from 'react-i18next';
⋮----
import { Chip } from '@/components/common/Chip';
⋮----
type Props = {
  path: string;
  onRemove: (path: string) => void;
};
⋮----
export const FileChip = (
⋮----
return <Chip key=

================
File: src/renderer/src/components/common/FloatingPanel.tsx
================
import { ReactNode, useCallback, useState } from 'react';
import { clsx } from 'clsx';
import { Rnd } from 'react-rnd';
import { IoMdClose } from 'react-icons/io';
import { VscChevronRight } from 'react-icons/vsc';
import { useLocalStorage } from '@reactuses/core';
⋮----
import { IconButton } from '@/components/common/IconButton';
⋮----
type Props = {
  title: string;
  children: ReactNode;
  onClose?: () => void;
  storageKey?: string;
  defaultPosition?: { x: number; y: number };
  defaultSize?: { width: number; height: number };
  minWidth?: number;
  minHeight?: number;
  className?: string;
};
⋮----
icon=
⋮----
className=

================
File: src/renderer/src/components/common/HtmlInfoDialog.tsx
================
import { useTranslation } from 'react-i18next';
⋮----
import { BaseDialog } from '@/components/common/BaseDialog';
⋮----
type Props = {
  title: string;
  text: string;
  onClose: () => void;
};

================
File: src/renderer/src/components/common/IconButton.tsx
================
import { MouseEvent, ReactNode } from 'react';
import { twMerge } from 'tailwind-merge';
⋮----
import { Tooltip } from '@/components/ui/Tooltip';
⋮----
type Props = {
  icon: ReactNode;
  onClick?: () => void;
  tooltip?: ReactNode;
  tooltipMaxWidth?: number | string;
  className?: string;
  disabled?: boolean;
  'data-testid'?: string;
};
⋮----
export const IconButton = (
⋮----
const handleClick = (e: MouseEvent<HTMLDivElement>) =>

================
File: src/renderer/src/components/common/InfoIcon.tsx
================
import { FaInfoCircle } from 'react-icons/fa';
import { ReactNode } from 'react';
import { clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
⋮----
import { IconButton } from './IconButton';
⋮----
type Props = {
  tooltip: ReactNode;
  className?: string;
  size?: 'sm' | 'md' | 'lg';
  tooltipMaxWidth?: number | string;
};
⋮----
export const InfoIcon = (
⋮----
className=

================
File: src/renderer/src/components/common/InlineEditPanel.tsx
================
import { KeyboardEvent } from 'react';
import { useTranslation } from 'react-i18next';
⋮----
import { Input } from '@/components/common/Input';
import { Button } from '@/components/common/Button';
⋮----
type Props = {
  value: string;
  onChange: (value: string) => void;
  onConfirm: () => void;
  onCancel: () => void;
  placeholder?: string;
  autoFocus?: boolean;
};
⋮----
const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) =>

================
File: src/renderer/src/components/common/Input.tsx
================
import { forwardRef, InputHTMLAttributes, ReactNode } from 'react';
import { twMerge } from 'tailwind-merge';
⋮----
type InputSize = 'md' | 'sm';
⋮----
export type Props = Omit<InputHTMLAttributes<HTMLInputElement>, 'size'> & {
  wrapperClassName?: string;
  label?: ReactNode;
  size?: InputSize;
};

================
File: src/renderer/src/components/common/InputPromptDialog.tsx
================
import { useCallback, useEffect, useState } from 'react';
import { InputPromptData } from '@common/types';
⋮----
import { useApi } from '@/contexts/ApiContext';
import { InputPromptModal } from '@/components/common/InputPromptModal';
⋮----
type Props = {
  promptData?: InputPromptData | null;
  onRespond?: (id: string, value: string | null, rememberSession?: boolean) => void;
};

================
File: src/renderer/src/components/common/InputPromptModal.tsx
================
import { ChangeEvent, FormEvent, KeyboardEvent, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { RiEyeLine, RiEyeOffLine } from 'react-icons/ri';
import { InputPromptData } from '@common/types';
⋮----
import { BaseDialog } from '@/components/common/BaseDialog';
import { Button } from '@/components/common/Button';
import { Checkbox } from '@/components/common/Checkbox';
import { Input } from '@/components/common/Input';
⋮----
type Props = {
  promptData: InputPromptData;
  onRespond: (id: string, value: string | null, rememberSession?: boolean) => void;
};
⋮----
const handleClose = () =>
⋮----
const handleSubmit = (e?: FormEvent<HTMLFormElement>) =>
⋮----
const handleConfirmChoice = (answer: string) =>
⋮----
const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) =>
⋮----
const handleChange = (e: ChangeEvent<HTMLInputElement>) =>
⋮----
const handleToggleShowPassword = () =>
⋮----
const handleToggleRemember = (checked: boolean) =>

================
File: src/renderer/src/components/common/ItemSelector.tsx
================
import { ElementType, memo, useRef, useState } from 'react';
import { MdKeyboardArrowDown, MdKeyboardArrowUp } from 'react-icons/md';
import { useTranslation } from 'react-i18next';
⋮----
import { Tooltip } from '@/components/ui/Tooltip';
import { useClickOutside } from '@/hooks/useClickOutside';
⋮----
export type ItemConfig<T extends string = string> = {
  value: T;
  icon: ElementType;
  labelKey: string;
  tooltipKey?: string;
};
⋮----
type PopupPlacement = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
⋮----
const getPopupPositionClasses = (placement: PopupPlacement): string =>
⋮----
type Props<T extends string = string> = {
  items: ItemConfig<T>[];
  selectedValue: T;
  onChange: (value: T) => void;
  className?: string;
  disabled?: boolean;
  popupPlacement?: PopupPlacement;
  minWidth?: number;
  iconOnly?: boolean;
};
⋮----
const ItemSelectorInner = <T extends string = string>({
  items,
  selectedValue,
  onChange,
  className = '',
  disabled = false,
  popupPlacement = 'top-left',
  minWidth = 150,
  iconOnly = false,
}: Props<T>) =>
⋮----
const toggleOpen = () =>
⋮----
const handleItemSelect = (value: T) =>
⋮----
const renderItem = (item: ItemConfig<T>) =>

================
File: src/renderer/src/components/common/LoadingOverlay.tsx
================
import { AnimatePresence, motion } from 'framer-motion';
import { CgSpinner } from 'react-icons/cg';
import { clsx } from 'clsx';
⋮----
type Props = {
  message: string;
  spinnerSize?: 'sm' | 'md' | 'lg';
  animateOpacity?: boolean;
  transparent?: boolean;
};
⋮----
export const LoadingOverlay = (
⋮----
className=
⋮----
<div className=

================
File: src/renderer/src/components/common/LoadingText.tsx
================
import { ReactNode } from 'react';
⋮----
import { useTypingAnimation } from '@/hooks/useTypingAnimation';
⋮----
type Props = {
  label: string;
  typingSpeed?: number;
  pauseDuration?: number;
  icon?: ReactNode;
  className?: string;
};
⋮----
export const LoadingText = (

================
File: src/renderer/src/components/common/MarkdownInfoDialog.tsx
================
import ReactMarkdown from 'react-markdown';
import { useTranslation } from 'react-i18next';
⋮----
import { BaseDialog } from '@/components/common/BaseDialog';
⋮----
type Props = {
  title: string;
  text: string;
  onClose: () => void;
};

================
File: src/renderer/src/components/common/MermaidDiagram.tsx
================
import { useEffect, useId, useState } from 'react';
import mermaid from 'mermaid';
import { CgSpinner } from 'react-icons/cg';
import { useTranslation } from 'react-i18next';
⋮----
type Props = {
  code: string;
  className?: string;
};
⋮----
const renderDiagram = async () =>

================
File: src/renderer/src/components/common/ModalOverlayLayout.tsx
================
import { ReactNode, useEffect, useId } from 'react';
import { HiOutlineExternalLink } from 'react-icons/hi';
import { IoMdClose } from 'react-icons/io';
import { useHotkeys } from 'react-hotkeys-hook';
⋮----
import { IconButton } from './IconButton';
⋮----
import { useApi } from '@/contexts/ApiContext';
import { useOverlayStore } from '@/stores/overlayStore';
⋮----
type Props = {
  title: string;
  children: ReactNode;
  onClose?: () => void;
  closeOnEscape?: boolean;
  openInWindowUrl?: string;
  openInWindowTitle?: string;
};
⋮----
export const ModalOverlayLayout = (
⋮----
const handleOpenUrl = async () =>

================
File: src/renderer/src/components/common/ModalOverlayUrlViewer.tsx
================
import { useEffect, useRef } from 'react';
⋮----
import { ModalOverlayLayout } from './ModalOverlayLayout';
⋮----
import { useApi } from '@/contexts/ApiContext';
⋮----
type Props = {
  url: string;
  onClose: () => void;
};
⋮----
const handleClose = () =>

================
File: src/renderer/src/components/common/ModelSelectorWrapper.tsx
================
import { forwardRef, useCallback, useMemo } from 'react';
import { Model } from '@common/types';
import { getProviderModelId } from '@common/agent';
⋮----
import { ModelSelector, ModelSelectorRef } from '@/components/ModelSelector';
import { useModelProviders } from '@/contexts/ModelProviderContext';
import { useSaveSettings, useSettingsStore } from '@/stores/settingsStore';
⋮----
type Props = {
  className?: string;
  selectedModelId?: string | null;
  onChange: (model: Model | null) => void;
  popupPlacement?: 'top' | 'bottom';
  labelOnNull?: string;
  skipPreferredModelsUpdate?: boolean;
  usePortal?: boolean;
};

================
File: src/renderer/src/components/common/MultiSelect.tsx
================
import { useState, useRef, ReactNode } from 'react';
import { createPortal } from 'react-dom';
import { HiChevronUpDown } from 'react-icons/hi2';
import { useTranslation } from 'react-i18next';
⋮----
import { Checkbox } from './Checkbox';
⋮----
import { useClickOutside } from '@/hooks/useClickOutside';
⋮----
type Option = {
  value: string;
  label: string;
};
⋮----
type Props = {
  options: Option[];
  selected: string[];
  onChange: (selected: string[]) => void;
  label?: ReactNode;
  className?: string;
  size?: 'sm' | 'md' | 'lg';
  noneSelectedLabel?: string;
  filterInput?: boolean;
};
⋮----
const handleToggle = () =>
⋮----
const handleCheckboxChange = (value: string) =>
⋮----
const handleSelectAll = () =>
⋮----
? noneSelectedLabel || t('multiselect.noneSelected')
? t('multiselect.allSelected')
⋮----
<Checkbox label=

================
File: src/renderer/src/components/common/RadioButton.tsx
================
import { ReactNode } from 'react';
⋮----
type Props = {
  id: string;
  name: string;
  value: string;
  checked: boolean;
  onChange: (value: string) => void;
  label?: ReactNode;
  className?: string;
};
⋮----
export const RadioButton = (
⋮----
const handleChange = () =>
⋮----
className="sr-only" // Hide the actual input but keep it accessible

================
File: src/renderer/src/components/common/SearchableSelect.tsx
================
import { KeyboardEvent, MouseEvent, ReactNode, useEffect, useMemo, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import { HiChevronUpDown, HiCheck } from 'react-icons/hi2';
import { useTranslation } from 'react-i18next';
⋮----
import { useClickOutside } from '@/hooks/useClickOutside';
import { useDropdownState } from '@/hooks/useDropdownState';
import { KeyboardKeys } from '@/constants/keyboardKeys';
import { Option } from '@/components/common/Select';
⋮----
type Props = {
  label?: ReactNode;
  options?: Option[];
  value?: string;
  onChange?: (value: string) => void;
  className?: string;
  size?: 'sm' | 'md' | 'lg';
  disabled?: boolean;
  placement?: 'bottom' | 'top';
  placeholder?: string;
};
⋮----
const getOptionText = (opt: Option): string
⋮----
const handleOpen = () =>
⋮----
const handleSearchChange = (search: string) =>
⋮----
const handleOptionSelect = (option: Option) =>
⋮----
const handleInputKeyDown = (e: KeyboardEvent) =>
⋮----
const handleBlur = () =>
⋮----
const handleChevronMouseDown = (e: MouseEvent<SVGSVGElement>) =>
⋮----
onMouseDown=
⋮----
onChange=

================
File: src/renderer/src/components/common/Section.tsx
================
import { ReactNode } from 'react';
import { clsx } from 'clsx';
⋮----
type Props = {
  id?: string;
  title?: string;
  children: ReactNode;
  className?: string;
};
⋮----
export const Section = (

================
File: src/renderer/src/components/common/Select.tsx
================
import { KeyboardEvent, ReactNode, useRef, useCallback, useState } from 'react';
import { createPortal } from 'react-dom';
import { HiChevronUpDown, HiCheck } from 'react-icons/hi2';
import { useTranslation } from 'react-i18next';
⋮----
import { useClickOutside } from '@/hooks/useClickOutside';
import { useDropdownState } from '@/hooks/useDropdownState';
import { KeyboardKeys } from '@/constants/keyboardKeys';
⋮----
export type Option = {
  label: ReactNode;
  value: string;
  style?: React.CSSProperties;
};
⋮----
type Props = {
  label?: ReactNode;
  options?: Option[];
  value?: string;
  onChange?: (value: string) => void;
  className?: string;
  size?: 'sm' | 'md' | 'lg';
  disabled?: boolean;
  placement?: 'bottom' | 'top';
  notFoundLabel?: string;
};
⋮----
const handleKeyDown = (e: KeyboardEvent) =>
⋮----
const handleOptionSelect = (option: Option) =>
⋮----
{/* Button container */}
⋮----
{/* Portal for Dropdown */}
⋮----
document.body, // Render into the body

================
File: src/renderer/src/components/common/Slider.tsx
================
import { ChangeEvent, CSSProperties, ReactNode } from 'react';
⋮----
type SliderSize = 'md' | 'sm';
⋮----
type Props = {
  label?: ReactNode;
  min: number;
  max: number;
  step?: number;
  value: number;
  onChange: (value: number) => void;
  className?: string;
  showValue?: boolean;
  size?: SliderSize;
  formatValue?: (value: number) => string;
};
⋮----
const handleChange = (e: ChangeEvent<HTMLInputElement>) =>

================
File: src/renderer/src/components/common/StyledTooltip.tsx
================
/**
 * @deprecated Use the new Tooltip component directly instead. This is a no-op shim kept for backwards compatibility.
 */
export const StyledTooltip = ()

================
File: src/renderer/src/components/common/Table.tsx
================
import { ReactNode, useMemo, useState } from 'react';
import { MdArrowUpward, MdArrowDownward } from 'react-icons/md';
import { useTranslation } from 'react-i18next';
⋮----
export type Column<T> = {
  accessor?: keyof T;
  header: ReactNode;
  cell?: (value: T[keyof T] | null, row: T) => ReactNode;
  headerClassName?: string;
  cellClassName?: string;
  align?: 'left' | 'center' | 'right';
  maxWidth?: string | number;
  sort?: (a: T, b: T) => number;
};
⋮----
export type FooterColumn = {
  cell: ReactNode;
  colSpan?: number;
  className?: string;
};
⋮----
type SortState = {
  columnKey: string;
  direction: 'asc' | 'desc' | null;
};
⋮----
type Props<T> = {
  data: T[];
  columns: Column<T>[];
  footerColumns?: FooterColumn[];
};
⋮----
const handleSort = (column: Column<T>) =>
⋮----
onClick=

================
File: src/renderer/src/components/common/Tabs.tsx
================
import { ReactNode } from 'react';
import { clsx } from 'clsx';
⋮----
export type TabItem = {
  id: string;
  label: ReactNode;
};
⋮----
type Props = {
  tabs: TabItem[];
  activeTabId: string;
  onTabChange: (tabId: string) => void;
  className?: string;
};
⋮----
<div className=
⋮----
const handleClick = () =>
⋮----
className=

================
File: src/renderer/src/components/common/TaskStateChip.tsx
================
import { useTranslation } from 'react-i18next';
import { twMerge } from 'tailwind-merge';
import { DefaultTaskState } from '@common/types';
⋮----
import type { TFunction } from 'i18next';
⋮----
export const getStateTextClass = (state: string): string =>
⋮----
export const getTaskStateLabel = (t: TFunction, state: string) =>
⋮----
type Props = {
  state: string;
  className?: string;
};
⋮----
export const TaskStateChip = (
⋮----
<span className=

================
File: src/renderer/src/components/common/TextArea.tsx
================
import { forwardRef, ReactNode, TextareaHTMLAttributes } from 'react';
import { twMerge } from 'tailwind-merge';
⋮----
export type Props = TextareaHTMLAttributes<HTMLTextAreaElement> & {
  label?: ReactNode;
  wrapperClassName?: string;
  error?: string | null;
};
⋮----
className={twMerge(
          `w-full p-2 bg-bg-secondary-light border-2 ${error ? 'border-error-emphasis' : 'border-border-default'} rounded focus:outline-none ${error ? 'focus:border-error-emphasis' : 'focus:border-border-light'} text-text-primary text-sm placeholder-text-muted
          scrollbar-thin
          scrollbar-track-bg-secondary-light
          scrollbar-thumb-bg-fourth`,
          className,
        )}
      />
      {error && <p className="text-xs text-error mt-1">{error}</p>}
    </div>
  );

================
File: src/renderer/src/components/common/Toggle.tsx
================
import { clsx } from 'clsx';
⋮----
export type ToggleColor = 'primary' | 'secondary' | 'tertiary' | 'danger';
export type ToggleSize = 'sm' | 'md';
⋮----
type Props = {
  checked: boolean;
  onChange: (checked: boolean) => void;
  disabled?: boolean;
  'aria-label'?: string;
  color?: ToggleColor;
  size?: ToggleSize;
};
⋮----
export const Toggle = (
⋮----
const handleChange = () =>
⋮----
<label className=
⋮----
className=

================
File: src/renderer/src/components/common/TriStateCheckbox.tsx
================
import { InputHTMLAttributes } from 'react';
⋮----
export type TriState = 'checked' | 'unchecked' | 'indeterminate';
⋮----
type Props = InputHTMLAttributes<HTMLInputElement> & {
  label?: string;
  state: TriState;
  onChange: () => void;
  className?: string;
};
⋮----
onClick=

================
File: src/renderer/src/components/common/VerticalDotsMenu.tsx
================
import { ReactNode, useRef, useState } from 'react';
import { FaEllipsisVertical } from 'react-icons/fa6';
import { clsx } from 'clsx';
⋮----
import { useClickOutside } from '@/hooks/useClickOutside';
⋮----
export type MenuOption = {
  label: string;
  action: () => void;
  icon?: ReactNode;
  disabled?: boolean;
};
⋮----
type Props = {
  options: MenuOption[];
  className?: string;
};
⋮----
const handleMenuClick = () =>
⋮----
const handleOptionClick = (option: MenuOption) =>
⋮----
<div className=
⋮----
className={clsx(
            'transition-opacity p-1.5 rounded-md hover:bg-bg-tertiary text-text-muted hover:text-text-primary',
            !isMenuOpen && 'opacity-0 group-hover:opacity-100',
          )}
onClick=

================
File: src/renderer/src/components/common/VirtualTable.tsx
================
import { ReactNode, useMemo, useRef, useState, useCallback } from 'react';
import { useVirtualizer } from '@tanstack/react-virtual';
import { twMerge } from 'tailwind-merge';
import { MdArrowUpward, MdArrowDownward } from 'react-icons/md';
⋮----
import { Column, FooterColumn } from './Table';
⋮----
type SortState = {
  columnKey: string;
  direction: 'asc' | 'desc' | null;
};
⋮----
type Props<T> = {
  data: T[];
  columns: Column<T>[];
  footerColumns?: FooterColumn[];
  rowHeight?: number | ((params: { index: number }) => number);
  overscanCount?: number;
  disableHeader?: boolean;
  getRowClassName?: (row: T, index: number) => string | undefined;
};
⋮----
// Apply manual sorting to data
⋮----
// Generate row model for virtualizer
⋮----
// Parent ref for virtualizer
⋮----
// Create virtualizer
// eslint-disable-next-line react-hooks/incompatible-library
⋮----
{/* Header - Outside virtualized area */}
⋮----
{/* Virtualized body */}
⋮----
{/* Footer - Outside virtualized area */}

================
File: src/renderer/src/components/common/WarningIcon.tsx
================
import { FaExclamationTriangle } from 'react-icons/fa';
import { ReactNode } from 'react';
import { clsx } from 'clsx';
⋮----
import { IconButton } from './IconButton';
⋮----
type Props = {
  tooltip: ReactNode;
  className?: string;
  size?: 'sm' | 'md' | 'lg';
};
⋮----
export const WarningIcon = (
⋮----
className=

================
File: src/renderer/src/components/extensions/ExtensionComponentRenderer.tsx
================
import { memo, useCallback, useMemo } from 'react';
import { ExtensionDisplayAPI } from '@common/api';
import { UIComponentProps } from '@common/extensions';
import { ExtensionUIComponent } from '@common/types';
⋮----
import StringToReactComponent from './StringToReactComponent';
⋮----
import { ExtensionUIErrorBoundary } from '@/components/extensions/ExtensionUIErrorBoundary';
import { useExtensionComponentData } from '@/stores/extensionUIStore';
⋮----
type Props = {
  component: ExtensionUIComponent;
  componentProps: UIComponentProps;
  additionalProps?: Record<string, unknown>;
  libraries: Record<string, Record<string, unknown>>;
  currentProjectDir?: string;
  currentTaskId?: string;
  currentActionProjectDir?: string;
  currentActionTaskId?: string;
  api: ExtensionDisplayAPI;
};
⋮----
const ExtensionComponentRendererInner = ({
  component,
  componentProps,
  additionalProps,
  libraries,
  currentProjectDir,
  currentTaskId,
  currentActionProjectDir,
  currentActionTaskId,
  api,
}: Props) =>

================
File: src/renderer/src/components/extensions/ExtensionComponentWrapper.tsx
================
import { memo } from 'react';
import { twMerge } from 'tailwind-merge';
⋮----
import { useExtensionComponentsWrapper } from '@/components/extensions/useExtensionComponentsWrapper';
import { useSettingsStore } from '@/stores/settingsStore';
⋮----
type Props = {
  placement: string;
  className?: string;
  direction?: 'horizontal' | 'vertical';
  additionalProps?: Record<string, unknown>;
  renderNullOnEmpty?: boolean;
  projectDir?: string;
  taskId?: string;
  actionProjectDir?: string;
  actionTaskId?: string;
};
⋮----
const ExtensionComponentWrapperInner = ({
  placement,
  className,
  direction = 'horizontal',
  additionalProps,
  renderNullOnEmpty = false,
  projectDir,
  taskId,
  actionProjectDir,
  actionTaskId,
}: Props) =>
⋮----
return <div className=

================
File: src/renderer/src/components/extensions/ExtensionUIErrorBoundary.tsx
================
import { Component, ErrorInfo, ReactNode } from 'react';
import { useTranslation } from 'react-i18next';
⋮----
type Props = {
  extensionId: string;
  componentId: string;
  children: ReactNode;
};
⋮----
type State = {
  hasError: boolean;
  error: Error | null;
};
⋮----
export class ExtensionUIErrorBoundary extends Component<Props, State>
⋮----
constructor(props: Props)
⋮----
static getDerivedStateFromError(error: Error): State
⋮----
componentDidCatch(error: Error, errorInfo: ErrorInfo): void
⋮----
// eslint-disable-next-line no-console
⋮----
render(): ReactNode
⋮----
type ErrorMessageProps = {
  extensionId: string;
  componentId: string;
};

================
File: src/renderer/src/components/extensions/FloatingExtensionPanels.tsx
================
import { createPortal } from 'react-dom';
import { useEffect, useState } from 'react';
⋮----
import { FloatingPanel } from '@/components/common/FloatingPanel';
import { useExtensionComponentsWrapper } from '@/components/extensions/useExtensionComponentsWrapper';
⋮----
type Props = {
  placement?: string;
  portalRootId?: string;
};
⋮----
// eslint-disable-next-line react-hooks/set-state-in-effect -- portal target is external DOM, not available on first render

================
File: src/renderer/src/components/extensions/StringToReactComponent.tsx
================
// eslint-disable-next-line no-restricted-imports -- needs the React runtime object itself, injected into sandboxed transpiled user components
⋮----
import { type ComponentType, useEffect, useRef, useState } from 'react';
import { transpileJsxString } from '@common/jsx-transpiler';
⋮----
type SucraseOptions = {
  transforms?: Array<'jsx' | 'typescript' | 'imports' | 'flow' | 'react-hot-loader' | 'jest'>;
  production?: boolean;
  [key: string]: unknown;
};
⋮----
export type StringToReactComponentProps = {
  data?: object;
  babelOptions?: SucraseOptions;
  children?: string;
};
⋮----
const createBlobUrl = (code: string): string =>
⋮----
const loadModule = async (blobUrl: string, react: typeof React): Promise<ComponentType> =>
⋮----
const module = await import(/* @vite-ignore */ blobUrl);
⋮----
const StringToReactComponent = (
⋮----
// eslint-disable-next-line react-hooks/refs
⋮----
// eslint-disable-next-line react-hooks/refs

================
File: src/renderer/src/components/extensions/useExtensionComponentsWrapper.tsx
================
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
⋮----
import { ExtensionComponentRenderer } from './ExtensionComponentRenderer';
⋮----
import { useExtensionApi } from '@/contexts/ExtensionApiContext';
import { useExtensions } from '@/contexts/ExtensionsContext';
import {
  handleExtensionUIRefreshEvent,
  isExtensionUIDataLoaded,
  loadExtensionComponentData,
  loadExtensionUIComponents,
  useExtensionComponents,
} from '@/stores/extensionUIStore';
import { useReactIcons } from '@/utils/extension-icons';
import { loadAllLibraries } from '@/utils/extension-library-loader';
⋮----
type UseExtensionComponentsWrapperProps = {
  placement: string;
  additionalProps?: Record<string, unknown>;
  projectDir?: string;
  taskId?: string;
  actionProjectDir?: string;
  actionTaskId?: string;
};
⋮----
// Track which components have been loaded in this mount to prevent infinite loops with noDataCache
⋮----
// Load libraries for all components that declare them
⋮----
// eslint-disable-next-line no-console
⋮----
// Load components list if not yet loaded
⋮----
// Reset loaded components tracking when components list changes
⋮----
// Load data for each component individually
⋮----
// For noDataCache components, only load once per mount (unless explicitly refreshed)
// For normal components, only load if not already loaded
⋮----
// Remove from tracking so it can be loaded again
⋮----
true, // forceRefresh

================
File: src/renderer/src/components/extensions/useExtensionMessageComponent.tsx
================
import { useCallback, useMemo } from 'react';
import { ExtensionUIComponent, Message, isToolMessage } from '@common/types';
⋮----
import { useExtensionComponentsWrapper } from '@/components/extensions/useExtensionComponentsWrapper';
⋮----
const matchesMessage = (message: Message, messageFilter: ExtensionUIComponent['messageFilter']): boolean =>
⋮----
type UseExtensionMessageComponentProps = {
  message: Message;
  projectDir?: string;
  taskId?: string;
};
⋮----
export const useExtensionMessageComponent = (

================
File: src/renderer/src/components/logs/LogsPage.tsx
================
import { UIEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { SystemLogEntry, SystemLogLevel } from '@common/types';
import { clsx } from 'clsx';
import { FaSearch, FaTrash, FaChevronDown } from 'react-icons/fa';
import { CgSpinner } from 'react-icons/cg';
⋮----
import { ModalOverlayLayout } from '@/components/common/ModalOverlayLayout';
import { useApi } from '@/contexts/ApiContext';
import { Button } from '@/components/common/Button';
import { Input } from '@/components/common/Input';
import { Select } from '@/components/common/Select';
import { Checkbox } from '@/components/common/Checkbox';
import { ConfirmDialog } from '@/components/common/ConfirmDialog';
⋮----
// Track expanded log entries
const useExpandedLogs = () =>
⋮----
// Helper function to deduplicate logs by ID
const deduplicateLogs = (logs: SystemLogEntry[]): SystemLogEntry[] =>
⋮----
return true; // Keep logs without ID
⋮----
return false; // Skip duplicates
⋮----
type Props = {
  openInWindowUrl?: string;
  onClose: () => void;
};
⋮----
const shouldAutoScrollRef = useRef(true); // Track if we should auto-scroll
⋮----
// Load initial logs
⋮----
const loadLogs = async () =>
⋮----
shouldAutoScrollRef.current = true; // Enable auto-scroll for initial load
⋮----
// eslint-disable-next-line no-console
⋮----
// Listen for new logs, batching high-frequency arrivals into a single update per animation frame
⋮----
const flushPendingEntries = () =>
⋮----
shouldAutoScrollRef.current = true; // Enable auto-scroll for new real-time logs
⋮----
// Auto-scroll to bottom when new logs arrive
⋮----
shouldAutoScrollRef.current = false; // Reset flag after scrolling
⋮----
}, [logs, autoScroll]); // eslint-disable-line react-hooks/exhaustive-deps
⋮----
// Extract unique extensions from logs
⋮----
// Filter logs
⋮----
// Filter by level
⋮----
// Filter by extension
⋮----
// Filter by search text
⋮----
const handleLevelToggle = (level: SystemLogLevel) =>
⋮----
// eslint-disable-next-line no-console
⋮----
// Load more logs (older logs - prepend to the beginning)
⋮----
const oldestLogId = logs[0].id; // First log is now the oldest
⋮----
setLogs((prev) => deduplicateLogs([...response.logs, ...prev])); // Prepend older logs and deduplicate
⋮----
// eslint-disable-next-line no-console
⋮----
// Handle scroll event for infinite scroll
⋮----
// Check if scrolled to top (scroll up to load more older logs)
⋮----
<div className=
⋮----
<span className=
⋮----
<ModalOverlayLayout title=
⋮----
{/* Filters and Controls */}
⋮----
{/* Search field on the left */}
⋮----
onChange=
⋮----
{/* Filter section on the right */}
⋮----
{/* Level checkboxes */}
⋮----
checked=
⋮----
{/* Extension filter */}
⋮----
{/* Logs list */}
⋮----
{/* Auto-scroll */}
⋮----
{/* Clear logs button */}
⋮----
{/* Clear confirmation dialog */}
⋮----
title=
⋮----
confirmButtonText=

================
File: src/renderer/src/components/message/ActivateSkillToolMessage.tsx
================
import { MdPsychology } from 'react-icons/md';
import { CgSpinner } from 'react-icons/cg';
import { useTranslation } from 'react-i18next';
import { VscError } from 'react-icons/vsc';
import { clsx } from 'clsx';
import { ToolMessage } from '@common/types';
⋮----
import { CopyMessageButton } from './CopyMessageButton';
import { ExpandableMessageBlock } from './ExpandableMessageBlock';
import { parseToolContent } from './utils';
⋮----
type Props = {
  message: ToolMessage;
  onRemove?: () => void;
  compact?: boolean;
  onFork?: () => void;
  onRemoveUpTo?: () => void;
  hideMessageBar?: boolean;
};
⋮----
<div className=

================
File: src/renderer/src/components/message/AssistantMessageBlock.tsx
================
import { memo, useCallback, useRef } from 'react';
import { clsx } from 'clsx';
import { Message, UsageReportData, AssistantGroupMessage, isResponseMessage, isToolMessage } from '@common/types';
⋮----
import { MessageBar } from './MessageBar';
import { MessageBlockWrapper } from './MessageBlockWrapper';
import { areMessagesEqual } from './utils';
⋮----
import { useParsedContent } from '@/hooks/useParsedContent';
⋮----
type Props = {
  baseDir: string;
  taskId: string;
  message: AssistantGroupMessage;
  allFiles: string[];
  renderMarkdown: boolean;
  remove?: (message: Message) => void;
  onFork?: (message: Message) => void;
  onRemoveUpTo?: (message: Message) => void;
};
⋮----
const aggregateUsage = (): UsageReportData | undefined =>
⋮----
{/* Response content */}
⋮----
<div className=
⋮----
{/* Single MessageBar for the entire group */}
⋮----
const arePropsEqual = (prevProps: Props, nextProps: Props): boolean =>

================
File: src/renderer/src/components/message/BashToolMessage.tsx
================
import { useEffect, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { RiTerminalLine, RiErrorWarningFill, RiCheckboxCircleFill, RiCloseCircleFill } from 'react-icons/ri';
import { CgSpinner } from 'react-icons/cg';
import { MdKeyboardDoubleArrowDown } from 'react-icons/md';
import { ToolMessage } from '@common/types';
⋮----
import { CodeBlock } from '@/components/common/CodeBlock';
import { CodeInline } from '@/components/common/CodeInline';
import { ExpandableMessageBlock } from '@/components/message/ExpandableMessageBlock';
import { Tooltip } from '@/components/ui/Tooltip';
import { CopyMessageButton } from '@/components/message/CopyMessageButton';
import { IconButton } from '@/components/common/IconButton';
import { useScrollingPaused } from '@/hooks/useScrollingPaused';
⋮----
type Props = {
  message: ToolMessage;
  onRemove?: () => void;
  compact?: boolean;
  onFork?: () => void;
  onRemoveUpTo?: () => void;
  hideMessageBar?: boolean;
};
⋮----
const isStandardBashOutput = (obj: unknown): obj is
⋮----
const hasErrorProperty = (obj: unknown): obj is
⋮----
const hasDeniedProperty = (obj: unknown): obj is
⋮----
const handleScrollToBottom = () =>
⋮----
const handleExpandedChange = (open: boolean) =>
⋮----
aria-label=

================
File: src/renderer/src/components/message/CommandOutputMessageBlock.tsx
================
import { BiTerminal } from 'react-icons/bi';
import { useTranslation } from 'react-i18next';
import { CommandOutputMessage } from '@common/types';
⋮----
import { CopyMessageButton } from './CopyMessageButton';
⋮----
type Props = {
  message: CommandOutputMessage;
  compact?: boolean;
};

================
File: src/renderer/src/components/message/CompactionSnippetBlock.tsx
================
import { RiFileEditLine } from 'react-icons/ri';
import { useTranslation } from 'react-i18next';
⋮----
import { CodeInline } from '@/components/common/CodeInline';
import { Tooltip } from '@/components/ui/Tooltip';
⋮----
type Props = {
  filePath: string;
};

================
File: src/renderer/src/components/message/CompactToolMessage.tsx
================
import { ReactNode, memo } from 'react';
import { CgSpinner } from 'react-icons/cg';
import { RiToolsFill } from 'react-icons/ri';
import { useTranslation } from 'react-i18next';
import { VscError } from 'react-icons/vsc';
import { IoChevronDown, IoChevronForward } from 'react-icons/io5';
import { clsx } from 'clsx';
import {
  AIDER_TOOL_ADD_CONTEXT_FILES,
  AIDER_TOOL_DROP_CONTEXT_FILES,
  AIDER_TOOL_GROUP_NAME,
  AIDER_TOOL_RUN_PROMPT,
  POWER_TOOL_BASH,
  POWER_TOOL_FETCH,
  POWER_TOOL_FILE_READ,
  POWER_TOOL_GLOB,
  POWER_TOOL_GREP,
  POWER_TOOL_GROUP_NAME,
  POWER_TOOL_SEMANTIC_SEARCH,
} from '@common/tools';
import { ToolMessage } from '@common/types';
⋮----
import { CopyMessageButton } from './CopyMessageButton';
import { formatName, parseToolContent } from './utils';
import { ToolResultImages } from './ToolResultImages';
⋮----
import { CodeInline } from '@/components/common/CodeInline';
⋮----
type Props = {
  message: ToolMessage;
  isExpanded: boolean;
  onToggle: () => void;
};
⋮----
const defaultLabel = () => t('toolMessage.toolLabel',
⋮----
if (!parsedResult)
⋮----

⋮----
className=
⋮----
{/* Header - clickable to toggle */}
⋮----
<div className=
⋮----
{/* Expanded content */}

================
File: src/renderer/src/components/message/CopyMessageButton.tsx
================
import { BiCopy } from 'react-icons/bi';
import { useTranslation } from 'react-i18next';
import { twMerge } from 'tailwind-merge';
⋮----
import { IconButton } from '../common/IconButton';
⋮----
import { showInfoNotification } from '@/utils/notifications';
import { useApi } from '@/contexts/ApiContext';
⋮----
type Props = {
  content: string;
  className?: string;
  alwaysShow?: boolean;
};
⋮----
const copyToClipboard = async () =>
⋮----
// eslint-disable-next-line no-console
⋮----
icon=

================
File: src/renderer/src/components/message/CreateTaskToolMessage.tsx
================
import { useTranslation } from 'react-i18next';
import { RiCheckboxCircleFill, RiCloseCircleFill, RiErrorWarningFill } from 'react-icons/ri';
import { CgSpinner } from 'react-icons/cg';
import { MdAssignmentAdd } from 'react-icons/md';
import { AutonomyMode, TaskExecutionMode, ToolMessage } from '@common/types';
⋮----
import { CodeInline } from '@/components/common/CodeInline';
import { ExpandableMessageBlock } from '@/components/message/ExpandableMessageBlock';
import { Tooltip } from '@/components/ui/Tooltip';
⋮----
type Props = {
  message: ToolMessage;
  onRemove?: () => void;
  compact?: boolean;
  onFork?: () => void;
  onRemoveUpTo?: () => void;
  hideMessageBar?: boolean;
};
⋮----
const getExecutionModeLabel = (mode: TaskExecutionMode) =>
⋮----
{/* Task Details */}

================
File: src/renderer/src/components/message/CustomCommandBashBlock.tsx
================
import { useState } from 'react';
import { FaChevronDown, FaChevronRight } from 'react-icons/fa';
import { BiTerminal } from 'react-icons/bi';
⋮----
import { CodeBlock } from '@/components/common/CodeBlock';
⋮----
type Props = {
  baseDir: string;
  command: string;
  output: string;
};
⋮----
const toggleAccordion = () =>

================
File: src/renderer/src/components/message/DefaultTaskStateActions.tsx
================
import { IoPlayOutline } from 'react-icons/io5';
import { RiAlertLine, RiCheckFill, RiCheckLine, RiPlayLine } from 'react-icons/ri';
import { BiArchive, BiArchiveIn } from 'react-icons/bi';
import { MdDeleteForever, MdOutlineDifference } from 'react-icons/md';
import { useTranslation } from 'react-i18next';
import { ReactNode, useCallback, useEffect, useMemo, useState } from 'react';
import { DefaultTaskState, Mode, TaskData, UpdatedFile } from '@common/types';
⋮----
import type { DiffModalGroup } from '@/components/Workspace/UpdatedFilesDiffModal';
⋮----
import { UpdatedFilesDiffModal } from '@/components/Workspace/UpdatedFilesDiffModal';
import { Button } from '@/components/common/Button';
import { ExtensionComponentWrapper } from '@/components/extensions/ExtensionComponentWrapper';
import { useApi } from '@/contexts/ApiContext';
import { groupFilesByCommit } from '@/components/Workspace/group-files';
import { encodeBaseDir, ROUTES, URL_PARAMS } from '@/utils/routes';
⋮----
type Props = {
  projectDir: string;
  taskId: string;
  state: string | undefined;
  mode?: Mode;
  isArchived: boolean | undefined;
  task?: TaskData | null;
  onResumeTask?: () => void;
  onMarkAsDone?: () => void;
  onRunPrompt?: (prompt: string) => void;
  onArchiveTask?: () => void;
  onUnarchiveTask?: () => void;
  onDeleteTask?: () => void;
};
⋮----
const fetchUpdatedFiles = async () =>
⋮----
// silently ignore fetch errors
⋮----
const handleDeleteClick = () =>
⋮----
const handleCancelDelete = () =>
⋮----
const handleConfirmDelete = () =>
⋮----
const handleProceedClick = () =>
⋮----
const handleArchiveClick = () =>
⋮----
const handleUnarchiveClick = () =>
⋮----
const renderSection = (icon: ReactNode, text: string, actions: ReactNode) =>
⋮----

================
File: src/renderer/src/components/message/DeleteMemoryToolMessage.tsx
================
import { useTranslation } from 'react-i18next';
import { FaBrain, FaCheckCircle, FaExclamationTriangle, FaTimesCircle } from 'react-icons/fa';
import { CgSpinner } from 'react-icons/cg';
import { ToolMessage } from '@common/types';
⋮----
import { ExpandableMessageBlock } from '@/components/message/ExpandableMessageBlock';
import { StreamingToolMessage } from '@/components/message/StreamingToolMessage';
import { Tooltip } from '@/components/ui/Tooltip';
⋮----
type Props = {
  message: ToolMessage;
  onRemove?: () => void;
  compact?: boolean;
  onFork?: () => void;
  onRemoveUpTo?: () => void;
  hideMessageBar?: boolean;
};
⋮----
{/* Memory Details */}

================
File: src/renderer/src/components/message/DeleteTaskToolMessage.tsx
================
import { useTranslation } from 'react-i18next';
import { RiCheckboxCircleFill, RiCloseCircleFill, RiErrorWarningFill } from 'react-icons/ri';
import { CgSpinner } from 'react-icons/cg';
import { LiaFolderMinusSolid } from 'react-icons/lia';
import { ToolMessage } from '@common/types';
⋮----
import { CodeInline } from '@/components/common/CodeInline';
import { ExpandableMessageBlock } from '@/components/message/ExpandableMessageBlock';
import { TaskStateChip } from '@/components/common/TaskStateChip';
import { Tooltip } from '@/components/ui/Tooltip';
⋮----
type Props = {
  message: ToolMessage;
  onRemove?: () => void;
  compact?: boolean;
  onFork?: () => void;
  onRemoveUpTo?: () => void;
  hideMessageBar?: boolean;
};
⋮----
const renderStatusIcon = () =>

================
File: src/renderer/src/components/message/ExpandableMessageBlock.tsx
================
import { ReactNode, useEffect, useState, forwardRef, useImperativeHandle } from 'react';
import { Message, UsageReportData } from '@common/types';
⋮----
import { MessageBar } from './MessageBar';
⋮----
import { Accordion } from '@/components/common/Accordion';
⋮----
export interface ExpandableMessageBlockRef {
  open: () => void;
  close: () => void;
}
⋮----
type Props = {
  title: ReactNode;
  message: Message;
  content: ReactNode;
  copyContent?: string;
  usageReport?: UsageReportData;
  onRemove?: () => void;
  initialExpanded?: boolean;
  onOpenChange?: (open: boolean) => void;
  onFork?: () => void;
  onRemoveUpTo?: () => void;
  hideMessageBar?: boolean;
};
⋮----
const handleExpandedChange = (open: boolean) =>
⋮----
{/* Gradient overlay for initial auto-expand */}

================
File: src/renderer/src/components/message/FetchToolMessage.tsx
================
import { useTranslation } from 'react-i18next';
import { RiLinkM, RiErrorWarningFill, RiCheckboxCircleFill, RiCloseCircleFill } from 'react-icons/ri';
import { CgSpinner } from 'react-icons/cg';
import { ToolMessage } from '@common/types';
⋮----
import { CodeInline } from '@/components/common/CodeInline';
import { ExpandableMessageBlock } from '@/components/message/ExpandableMessageBlock';
import { highlightWithLowlight } from '@/utils/highlighter';
import { CopyMessageButton } from '@/components/message/CopyMessageButton';
import { Tooltip } from '@/components/ui/Tooltip';
⋮----
type Props = {
  message: ToolMessage;
  onRemove?: () => void;
  compact?: boolean;
  onFork?: () => void;
  onRemoveUpTo?: () => void;
  hideMessageBar?: boolean;
};
⋮----
const renderStatusIcon = () =>
⋮----
const renderContent = () =>

================
File: src/renderer/src/components/message/FileEditToolMessage.tsx
================
import { useLayoutEffect, useRef } from 'react';
import { RiCheckboxCircleFill, RiEditLine, RiErrorWarningFill, RiCloseCircleFill } from 'react-icons/ri';
import { getLanguageFromPath } from '@common/utils';
import { CgSpinner } from 'react-icons/cg';
import { useTranslation } from 'react-i18next';
import { ToolMessage } from '@common/types';
⋮----
import { CodeBlock } from '@/components/common/CodeBlock';
import { CodeInline } from '@/components/common/CodeInline';
import { ExpandableMessageBlock, ExpandableMessageBlockRef } from '@/components/message/ExpandableMessageBlock';
import { Tooltip } from '@/components/ui/Tooltip';
⋮----
type Props = {
  message: ToolMessage;
  onRemove?: () => void;
  compact?: boolean;
  onFork?: () => void;
  onRemoveUpTo?: () => void;
  hideMessageBar?: boolean;
};
⋮----
<strong>

================
File: src/renderer/src/components/message/FileReadToolMessage.tsx
================
import { useTranslation } from 'react-i18next';
import { RiFileTextLine, RiCheckboxCircleFill, RiErrorWarningFill, RiCloseCircleFill } from 'react-icons/ri';
import { getLanguageFromPath } from '@common/utils';
import { CgSpinner } from 'react-icons/cg';
import { ToolMessage } from '@common/types';
⋮----
import { CodeBlock } from '@/components/common/CodeBlock';
import { CodeInline } from '@/components/common/CodeInline';
import { ExpandableMessageBlock } from '@/components/message/ExpandableMessageBlock';
import { StreamingToolMessage } from '@/components/message/StreamingToolMessage';
import { Tooltip } from '@/components/ui/Tooltip';
⋮----
type Props = {
  message: ToolMessage;
  onRemove?: () => void;
  compact?: boolean;
  onFork?: () => void;
  onRemoveUpTo?: () => void;
  hideMessageBar?: boolean;
};
⋮----
{/* File content - filter out line numbers if present */}
⋮----
{/* Parameter information */}

================
File: src/renderer/src/components/message/FileWriteToolMessage.tsx
================
import { useLayoutEffect, useRef } from 'react';
import { RiCheckboxCircleFill, RiEditLine, RiErrorWarningFill, RiCloseCircleFill } from 'react-icons/ri';
import { CgSpinner } from 'react-icons/cg';
import { useTranslation } from 'react-i18next';
import { FileWriteMode, ToolMessage } from '@common/types';
import { getLanguageFromPath } from '@common/utils';
⋮----
import { CodeBlock } from '@/components/common/CodeBlock';
import { CodeInline } from '@/components/common/CodeInline';
import { ExpandableMessageBlock, ExpandableMessageBlockRef } from '@/components/message/ExpandableMessageBlock';
import { Tooltip } from '@/components/ui/Tooltip';
⋮----
type Props = {
  message: ToolMessage;
  onRemove?: () => void;
  compact?: boolean;
  onFork?: () => void;
  onRemoveUpTo?: () => void;
  hideMessageBar?: boolean;
};
⋮----
const getToolName = (): string =>

================
File: src/renderer/src/components/message/GetTaskMessageToolMessage.tsx
================
import { useTranslation } from 'react-i18next';
import { RiMessage3Line, RiCheckboxCircleFill, RiErrorWarningFill, RiCloseCircleFill } from 'react-icons/ri';
import { CgSpinner } from 'react-icons/cg';
import { ToolMessage } from '@common/types';
⋮----
import { CodeInline } from '@/components/common/CodeInline';
import { ExpandableMessageBlock } from '@/components/message/ExpandableMessageBlock';
import { Tooltip } from '@/components/ui/Tooltip';
⋮----
type Props = {
  message: ToolMessage;
  onRemove?: () => void;
  compact?: boolean;
  onFork?: () => void;
  onRemoveUpTo?: () => void;
  hideMessageBar?: boolean;
};
⋮----
const renderStatusIcon = () =>

================
File: src/renderer/src/components/message/GetTaskToolMessage.tsx
================
import { useTranslation } from 'react-i18next';
import { RiCheckboxCircleFill, RiCloseCircleFill, RiErrorWarningFill } from 'react-icons/ri';
import { CgSpinner } from 'react-icons/cg';
import { MdAssignment } from 'react-icons/md';
import { ContextFile, ToolMessage } from '@common/types';
⋮----
import { CodeInline } from '@/components/common/CodeInline';
import { ExpandableMessageBlock } from '@/components/message/ExpandableMessageBlock';
import { TaskStateChip } from '@/components/common/TaskStateChip';
import { Tooltip } from '@/components/ui/Tooltip';
⋮----
type Props = {
  message: ToolMessage;
  onRemove?: () => void;
  compact?: boolean;
  onFork?: () => void;
  onRemoveUpTo?: () => void;
  hideMessageBar?: boolean;
};
⋮----
const renderStatusIcon = () =>
⋮----
{/* Task Header */}
⋮----
{/* Task Configuration */}
⋮----
{/* Context Files */}
⋮----
{/* Context Messages Count */}

================
File: src/renderer/src/components/message/GlobToolMessage.tsx
================
import { memo, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { RiFolderLine, RiFileTextLine, RiErrorWarningFill, RiCheckboxCircleFill, RiCloseCircleFill } from 'react-icons/ri';
import { LuFolderSearch } from 'react-icons/lu';
import { CgSpinner } from 'react-icons/cg';
import { ToolMessage } from '@common/types';
⋮----
import { areMessagesEqual } from './utils';
⋮----
import { CodeInline } from '@/components/common/CodeInline';
import { ExpandableMessageBlock } from '@/components/message/ExpandableMessageBlock';
import { StreamingToolMessage } from '@/components/message/StreamingToolMessage';
import { Tooltip } from '@/components/ui/Tooltip';
⋮----
type Props = {
  message: ToolMessage;
  onRemove?: () => void;
  compact?: boolean;
  onFork?: () => void;
  onRemoveUpTo?: () => void;
  hideMessageBar?: boolean;
};

================
File: src/renderer/src/components/message/GrepToolMessage.tsx
================
import { memo, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { RiCheckboxCircleFill, RiCloseCircleFill, RiErrorWarningFill } from 'react-icons/ri';
import { LuFileSearch } from 'react-icons/lu';
import { CgSpinner } from 'react-icons/cg';
import { ToolMessage } from '@common/types';
⋮----
import { areMessagesEqual } from './utils';
⋮----
import { CodeInline } from '@/components/common/CodeInline';
import { ExpandableMessageBlock } from '@/components/message/ExpandableMessageBlock';
import { StreamingToolMessage } from '@/components/message/StreamingToolMessage';
import { Tooltip } from '@/components/ui/Tooltip';
⋮----
interface Match {
  filePath: string;
  lineNumber: number;
  lineContent: string;
  context?: string[];
}
⋮----
type Props = {
  message: ToolMessage;
  onRemove?: () => void;
  compact?: boolean;
  onFork?: () => void;
  onRemoveUpTo?: () => void;
  hideMessageBar?: boolean;
};
⋮----
const parseMarkdownResults = (md: string): Array<Match> | null =>
⋮----

⋮----
if (
    prevProps.compact !== nextProps.compact ||
    prevProps.hideMessageBar !== nextProps.hideMessageBar ||
    (prevProps.onRemove === undefined) !== (nextProps.onRemove === undefined) ||
    (prevProps.onFork === undefined) !== (nextProps.onFork === undefined) ||
    (prevProps.onRemoveUpTo === undefined) !== (nextProps.onRemoveUpTo === undefined)
)

================
File: src/renderer/src/components/message/GroupMessageBlock.tsx
================
import { AnimatePresence, motion } from 'framer-motion';
import { memo, useMemo, useState, useCallback, MouseEvent } from 'react';
import { clsx } from 'clsx';
import { useTranslation } from 'react-i18next';
import { FaRegHourglass } from 'react-icons/fa';
import { FiLayers } from 'react-icons/fi';
import {
  LocalizedString,
  UsageReportData,
  GroupMessage,
  isResponseMessage,
  isToolMessage,
  isUserMessage,
  Message,
  ResponseMessage,
  ToolMessage,
  MessageViewMode,
} from '@common/types';
⋮----
import { MessageBlock } from './MessageBlock';
import { MessageBlockWrapper } from './MessageBlockWrapper';
import { MessageBar } from './MessageBar';
import { areMessagesEqual, groupAssistantMessages } from './utils';
⋮----
import { Accordion } from '@/components/common/Accordion';
import { Button } from '@/components/common/Button';
import { Tooltip } from '@/components/ui/Tooltip';
import { useSettingsStore } from '@/stores/settingsStore';
⋮----
type Props = {
  baseDir: string;
  taskId: string;
  message: GroupMessage;
  allFiles: string[];
  renderMarkdown: boolean;
  remove?: (message: Message) => void;
  removeGroup?: (group: GroupMessage) => void;
  redo?: () => void;
  editUserMessage?: (messageId: string, content: string, images?: string[]) => void;
  onInterrupt?: (interruptId?: string) => void;
  onFork?: (message: Message) => void;
  onRemoveUpTo?: (message: Message) => void;
};
⋮----
const aggregateUsage = (messages: Message[]): UsageReportData | undefined =>
⋮----
// Find all messages with usageReport and the last one
⋮----
// Use tokens from the last message with usage
⋮----
// Sum costs from all messages with usage
⋮----
const getGroupDisplayName = (name?: string | LocalizedString) =>
⋮----
// name is LocalizedString
⋮----
const handleInterrupt = (e: MouseEvent<HTMLButtonElement>) =>
⋮----
// Target the first child (the subagent tool call message whose id === toolCallId), not the last child.
// Subagent group children are frontend-only streaming messages that are not persisted to the backend's
// ContextManager.messages — only the tool call message (first child) exists there. Using the last child
// would cause a "Message with id X not found" error in removeMessagesAfter.
⋮----
// Target the first child (the subagent tool call message whose id === toolCallId). This is the only
// child message that exists in the backend's ContextManager.messages, since subagent streaming messages
// are frontend-only and never persisted. See handleRemoveUpTo for the same reasoning.
⋮----
<div className=
⋮----
{/* Color Bar */}
⋮----
{/* Content */}
⋮----
redoUserPrompt=
⋮----
const arePropsEqual = (prevProps: Props, nextProps: Props): boolean =>

================
File: src/renderer/src/components/message/index.ts
================


================
File: src/renderer/src/components/message/ListMemoriesToolMessage.tsx
================
import { useTranslation } from 'react-i18next';
import { FaBrain, FaCheckCircle, FaExclamationTriangle, FaTimesCircle } from 'react-icons/fa';
import { CgSpinner } from 'react-icons/cg';
import { ToolMessage } from '@common/types';
⋮----
import { CodeInline } from '@/components/common/CodeInline';
import { ExpandableMessageBlock } from '@/components/message/ExpandableMessageBlock';
import { StreamingToolMessage } from '@/components/message/StreamingToolMessage';
import { Tooltip } from '@/components/ui/Tooltip';
⋮----
type Props = {
  message: ToolMessage;
  onRemove?: () => void;
  compact?: boolean;
  onFork?: () => void;
  onRemoveUpTo?: () => void;
  hideMessageBar?: boolean;
};
⋮----
// Handle array data (new format)
⋮----
{/* Memory List */}
⋮----
// Fallback for unexpected content (backward compatibility)

================
File: src/renderer/src/components/message/ListTasksToolMessage.tsx
================
import { useTranslation } from 'react-i18next';
import { RiCheckboxCircleFill, RiCloseCircleFill, RiErrorWarningFill } from 'react-icons/ri';
import { CgSpinner } from 'react-icons/cg';
import { LuClipboardList } from 'react-icons/lu';
import { ToolMessage } from '@common/types';
⋮----
import { ExpandableMessageBlock } from '@/components/message/ExpandableMessageBlock';
import { TaskStateChip } from '@/components/common/TaskStateChip';
import { Tooltip } from '@/components/ui/Tooltip';
⋮----
type Props = {
  message: ToolMessage;
  onRemove?: () => void;
  compact?: boolean;
  onFork?: () => void;
  onRemoveUpTo?: () => void;
  hideMessageBar?: boolean;
};
⋮----
type Task = {
    id: string;
    name: string;
    archived?: boolean;
    description?: string;
    createdAt?: string;
    updatedAt?: string;
    state?: string;
  };
⋮----
const renderStatusIcon = () =>

================
File: src/renderer/src/components/message/LoadingMessageBlock.tsx
================
import { LoadingMessage } from '@common/types';
⋮----
import { useTypingAnimation } from '@/hooks/useTypingAnimation';
import { MessageActions } from '@/components/message/MessageActions';
⋮----
type Props = {
  message: LoadingMessage;
  baseDir: string;
  taskId: string;
  onInterrupt?: () => void;
};

================
File: src/renderer/src/components/message/LogMessageBlock.tsx
================
import { FaInfoCircle, FaExclamationTriangle, FaExclamationCircle } from 'react-icons/fa';
import { MdClose } from 'react-icons/md';
import { useTranslation } from 'react-i18next';
import { LogMessage } from '@common/types';
import { clsx } from 'clsx';
⋮----
import { CopyMessageButton } from './CopyMessageButton';
import { MessageActions } from './MessageActions';
⋮----
import { IconButton } from '@/components/common/IconButton';
import { removeMessageActionId } from '@/stores/taskStore';
⋮----
type Props = {
  baseDir: string;
  taskId: string;
  message: LogMessage;
  onRemove?: () => void;
  compact?: boolean;
  onInterrupt?: () => void;
  removeActionId?: (actionId: string) => void;
};
⋮----
const renderMessage = () => (
    <div className="flex items-start gap-3">
      <Icon className="inline-block h-3 w-3 flex-shrink-0 mt-[3px]" />
      <div>{t(message.content)}</div>
    </div>
  );
⋮----
content=
⋮----
className=

================
File: src/renderer/src/components/message/MessageActions.tsx
================
import { useTranslation } from 'react-i18next';
import { useState } from 'react';
import { MdUndo } from 'react-icons/md';
⋮----
import { Button } from '../common/Button';
⋮----
import { useApi, useIsReadonlyView } from '@/contexts/ApiContext';
import { useTaskStore } from '@/stores/taskStore';
⋮----
type Props = {
  actionIds: string[];
  baseDir: string;
  taskId: string;
  onInterrupt?: () => void;
  removeActionId?: (actionId: string) => void;
};
⋮----
const handleAbortRebase = () =>
⋮----
const handleContinueRebase = () =>
⋮----
const handleResolveConflictsWithAgent = () =>
⋮----
const handleResolveGitErrorWithAgent = () =>
⋮----
const handleRebaseWorktree = () =>
⋮----
const handleUndoContextChange = () =>
⋮----
<Button key=

================
File: src/renderer/src/components/message/MessageBar.tsx
================
import { useRef, useState, useCallback } from 'react';
import { createPortal } from 'react-dom';
import { useTranslation } from 'react-i18next';
import { toPng } from 'html-to-image';
import { UsageReportData, Message } from '@common/types';
import { MdCallSplit, MdDeleteForever, MdDeleteSweep, MdEdit, MdImage, MdRedo } from 'react-icons/md';
import { FaEllipsisVertical } from 'react-icons/fa6';
import { twMerge } from 'tailwind-merge';
⋮----
import { IconButton } from '../common/IconButton';
⋮----
import { CopyMessageButton } from './CopyMessageButton';
import { UsageInfo } from './UsageInfo';
⋮----
import { useApi } from '@/contexts/ApiContext';
import { useClickOutside } from '@/hooks/useClickOutside';
import { ExtensionComponentWrapper } from '@/components/extensions/ExtensionComponentWrapper';
import { useExtensionComponentsWrapper } from '@/components/extensions/useExtensionComponentsWrapper';
import { includeMessageProperty } from '@/components/message/utils';
⋮----
type MenuPosition = {
  top?: number;
  bottom?: number;
  right?: number;
  left?: number;
};
⋮----
type Props = {
  className?: string;
  content?: string;
  usageReport?: UsageReportData;
  message: Message;
  remove?: () => void;
  redo?: () => void;
  edit?: () => void;
  onFork?: () => void;
  onRemoveUpTo?: () => void;
};
⋮----
// Position menu above the button, aligned to the right edge
⋮----
const handleRemoveClick = () =>
⋮----
const handleRedoClick = () =>
⋮----
const handleEditClick = () =>
⋮----
const handleForkClick = () =>
⋮----
const handleRemoveUpToClick = () =>
⋮----
const handleCopyAsImageClick = async () =>
⋮----
// eslint-disable-next-line no-console
⋮----
<div className=

================
File: src/renderer/src/components/message/MessageBlock.tsx
================
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import {
  HELPERS_TOOL_GROUP_NAME,
  HELPERS_TOOL_INVALID_TOOL_ARGUMENTS,
  HELPERS_TOOL_NO_SUCH_TOOL,
  MEMORY_TOOL_GROUP_NAME,
  MEMORY_TOOL_STORE,
  MEMORY_TOOL_RETRIEVE,
  MEMORY_TOOL_DELETE,
  MEMORY_TOOL_LIST,
  MEMORY_TOOL_UPDATE,
  POWER_TOOL_BASH,
  POWER_TOOL_FETCH,
  POWER_TOOL_FILE_EDIT,
  POWER_TOOL_FILE_READ,
  POWER_TOOL_FILE_WRITE,
  POWER_TOOL_GLOB,
  POWER_TOOL_GREP,
  POWER_TOOL_GROUP_NAME,
  POWER_TOOL_SEMANTIC_SEARCH,
  SKILLS_TOOL_ACTIVATE_SKILL,
  SKILLS_TOOL_GROUP_NAME,
  SUBAGENTS_TOOL_GROUP_NAME,
  SUBAGENTS_TOOL_RUN_TASK,
  TASKS_TOOL_GROUP_NAME,
  TASKS_TOOL_LIST_TASKS,
  TASKS_TOOL_GET_TASK,
  TASKS_TOOL_GET_TASK_MESSAGE,
  TASKS_TOOL_CREATE_TASK,
  TASKS_TOOL_DELETE_TASK,
  TASKS_TOOL_RUN_PROMPT,
  TASKS_TOOL_SEARCH_TASK,
  TASKS_TOOL_SEARCH_PARENT_TASK,
} from '@common/tools';
import {
  isCommandOutputMessage,
  isLoadingMessage,
  isLogMessage,
  isReflectedMessage,
  isResponseMessage,
  isToolMessage,
  isUserMessage,
  LogMessage,
  Message,
  ToolMessage,
} from '@common/types';
⋮----
import { CommandOutputMessageBlock } from './CommandOutputMessageBlock';
import { LoadingMessageBlock } from './LoadingMessageBlock';
import { LogMessageBlock } from './LogMessageBlock';
import { UserMessageBlock } from './UserMessageBlock';
import { ReflectedMessageBlock } from './ReflectedMessageBlock';
import { ResponseMessageBlock } from './ResponseMessageBlock';
import { ToolMessageBlock } from './ToolMessageBlock';
import { FileWriteToolMessage } from './FileWriteToolMessage';
import { FileEditToolMessage } from './FileEditToolMessage';
import { FileReadToolMessage } from './FileReadToolMessage';
import { GlobToolMessage } from './GlobToolMessage';
import { GrepToolMessage } from './GrepToolMessage';
import { BashToolMessage } from './BashToolMessage';
import { FetchToolMessage } from './FetchToolMessage';
import { SemanticSearchToolMessage } from './SemanticSearchToolMessage';
import { SubagentToolMessage } from './SubagentToolMessage';
import { ListTasksToolMessage } from './ListTasksToolMessage';
import { GetTaskToolMessage } from './GetTaskToolMessage';
import { GetTaskMessageToolMessage } from './GetTaskMessageToolMessage';
import { CreateTaskToolMessage } from './CreateTaskToolMessage';
import { DeleteTaskToolMessage } from './DeleteTaskToolMessage';
import { RunPromptToolMessage } from './RunPromptToolMessage';
import { SearchTaskToolMessage } from './SearchTaskToolMessage';
import { SearchParentTaskToolMessage } from './SearchParentTaskToolMessage';
import { StoreMemoryToolMessage } from './StoreMemoryToolMessage';
import { RetrieveMemoryToolMessage } from './RetrieveMemoryToolMessage';
import { DeleteMemoryToolMessage } from './DeleteMemoryToolMessage';
import { ListMemoriesToolMessage } from './ListMemoriesToolMessage';
import { UpdateMemoryToolMessage } from './UpdateMemoryToolMessage';
import { ActivateSkillToolMessage } from './ActivateSkillToolMessage';
import { areMessagesEqual } from './utils';
⋮----
import { useProjectTaskWorktreePath } from '@/stores/projectStore';
⋮----
type Props = {
  baseDir: string;
  taskId: string;
  message: Message;
  allFiles: string[];
  renderMarkdown: boolean;
  compact?: boolean;
  showThinking?: boolean;
  hideMessageBar?: boolean;
  remove?: () => void;
  redo?: () => void;
  edit?: (content: string, images?: string[]) => void;
  onInterrupt?: () => void;
  onFork?: () => void;
  onRemoveUpTo?: () => void;
};
⋮----
const MessageBlockComponent = ({
  baseDir,
  taskId,
  message,
  allFiles,
  renderMarkdown,
  compact = false,
  hideMessageBar = false,
  showThinking = true,
  remove,
  redo,
  edit,
  onInterrupt,
  onFork,
  onRemoveUpTo,
}: Props) =>
⋮----
const arePropsEqual = (prevProps: Props, nextProps: Props): boolean =>

================
File: src/renderer/src/components/message/MessageBlockWrapper.tsx
================
import { memo, useMemo, type ReactNode } from 'react';
import { GroupMessage, isGroupMessage, isAssistantGroupMessage, isUserMessage, Message } from '@common/types';
⋮----
import { MessageBlock } from './MessageBlock';
import { GroupMessageBlock } from './GroupMessageBlock';
import { AssistantMessageBlock } from './AssistantMessageBlock';
import { areMessagesEqual } from './utils';
⋮----
import { ExtensionComponentWrapper } from '@/components/extensions/ExtensionComponentWrapper';
import { useExtensionMessageComponent } from '@/components/extensions/useExtensionMessageComponent';
import { includeMessageProperty } from '@/components/message/utils';
⋮----
type Props = {
  baseDir: string;
  taskId: string;
  message: Message;
  allFiles: string[];
  renderMarkdown: boolean;
  compact?: boolean;
  hideMessageBar?: boolean;
  inProgress?: boolean;
  removeMessage?: (message: Message) => void;
  removeGroup?: (group: GroupMessage) => void;
  redoUserPrompt?: (messageId: string) => void;
  editUserMessage?: (messageId: string, content: string, images?: string[]) => void;
  onInterrupt?: () => void;
  onForkFromMessage?: (message: Message) => void;
  onRemoveUpToMessage?: (message: Message) => void;
};
⋮----
onFork=
⋮----
const arePropsEqual = (prevProps: Props, nextProps: Props): boolean =>

================
File: src/renderer/src/components/message/MessageImages.tsx
================
import { useState } from 'react';
import { createPortal } from 'react-dom';
import { IoClose } from 'react-icons/io5';
import { AnimatePresence, motion } from 'framer-motion';
⋮----
type Props = {
  images: string[];
  altPrefix?: string;
  thumbnailClassName?: string;
};
⋮----
const handleImageClick = (index: number) =>
⋮----
const handleExpandedClick = () =>
⋮----
e.stopPropagation();
handleExpandedClick();

================
File: src/renderer/src/components/message/messageMap.ts
================
import { isAssistantGroupMessage, isGroupMessage, isResponseMessage, isUserMessage, Message } from '@common/types';
⋮----
export enum MessageMapRole {
  User = 'user',
  Assistant = 'assistant',
}
⋮----
export type MessageMapEntry = {
  id: string;
  index: number;
  role: MessageMapRole;
  preview: string;
};
⋮----
export type MessageMapTurn = {
  id: string;
  number: number;
  startIndex: number;
  userIndex: number | null;
  userPreview: string;
  lastAssistantIndex: number | null;
  lastAssistantPreview: string;
};
⋮----
export const createMessageMapEntries = (messages: Message[]): MessageMapEntry[] =>
⋮----
const addMessage = (message: Message, index: number, path: string) =>
⋮----
// Collapses the flat message entries into conversation turns. A turn starts at each user prompt and
// absorbs the assistant replies that follow it, tracking the last reply of the turn. Assistant
// replies that appear before any user prompt form a leading turn with no user message.
export const createMessageMapTurns = (messages: Message[]): MessageMapTurn[] =>
⋮----
export type MessageMapMarkerItem = {
  id: string;
  turnNumber: number;
  role: MessageMapRole;
  index: number;
  preview: string;
};
⋮----
// Flattens turns into individually navigable markers: a green user-prompt marker and a blue
// last-assistant-reply marker per turn, kept in ascending rendered order for scroll tracking.
export const createMessageMapMarkers = (turns: MessageMapTurn[]): MessageMapMarkerItem[] =>
⋮----
// Selection follows user messages only: the active marker is the user message currently in the
// scroll view, or the previous one when the viewport sits within that turn's replies.
export const getActiveMarkerId = (markers: MessageMapMarkerItem[], visibleIndex: number): string | null =>

================
File: src/renderer/src/components/message/MessageMap.tsx
================
import { memo, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { MdKeyboardArrowDown, MdKeyboardArrowUp, MdKeyboardDoubleArrowDown } from 'react-icons/md';
import { Message } from '@common/types';
⋮----
import { createMessageMapMarkers, createMessageMapTurns, getActiveMarkerId } from './messageMap';
import { MessageMapMarker } from './MessageMapMarker';
⋮----
import { Tooltip } from '@/components/ui/Tooltip';
⋮----
type Props = {
  messages: Message[];
  visibleIndex: number;
  onNavigate: (index: number, animated: boolean) => void;
  onPreviousUserMessage: () => void;
  onNextUserMessage: () => void;
  hasPreviousUserMessage: boolean;
  hasNextUserMessage: boolean;
  onScrollToBottom: () => void;
};

================
File: src/renderer/src/components/message/MessageMapMarker.tsx
================
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { clsx } from 'clsx';
⋮----
import { MessageMapRole } from './messageMap';
import { MessageMapPreview } from './MessageMapPreview';
⋮----
import { Tooltip } from '@/components/ui/Tooltip';
⋮----
type Props = {
  turnNumber: number;
  role: MessageMapRole;
  index: number;
  preview: string;
  active: boolean;
  onNavigate: (index: number, animated: boolean) => void;
};
⋮----
const handleClick = ()

================
File: src/renderer/src/components/message/MessageMapPreview.tsx
================
import { useTranslation } from 'react-i18next';
import { clsx } from 'clsx';
⋮----
import { MessageMapRole } from './messageMap';
⋮----
type Props = {
  turnNumber: number;
  role: MessageMapRole;
  preview: string;
};
⋮----
export const MessageMapPreview = (
⋮----
className=
⋮----

================
File: src/renderer/src/components/message/Messages.tsx
================
import { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
import { toPng } from 'html-to-image';
import { MdKeyboardDoubleArrowDown } from 'react-icons/md';
import { useTranslation } from 'react-i18next';
import { GroupMessage, isUserMessage, Message, MessageViewMode } from '@common/types';
⋮----
import { MessageBlockWrapper } from './MessageBlockWrapper';
import { MessageMap } from './MessageMap';
⋮----
import { IconButton } from '@/components/common/IconButton';
import { groupAssistantMessages, groupMessagesByPromptContext } from '@/components/message/utils';
import { useScrollingPaused } from '@/hooks/useScrollingPaused';
import { useUserMessageNavigation } from '@/hooks/useUserMessageNavigation';
import { useSettingsStore } from '@/stores/settingsStore';
⋮----
export type MessagesRef = {
  exportToImage: () => void;
  scrollToBottom: () => void;
};
⋮----
type Props = {
  baseDir: string;
  taskId: string;
  inProgress: boolean;
  messages: Message[];
  allFiles?: string[];
  renderMarkdown: boolean;
  onContainerRef?: (container: HTMLDivElement | null) => void;
  removeMessage?: (message: Message) => void;
  removeGroup?: (group: GroupMessage) => void;
  redoUserPrompt?: (messageId: string) => void;
  editUserMessage?: (messageId: string, content: string, images?: string[]) => void;
  onInterrupt?: () => void;
  onForkFromMessage?: (message: Message) => void;
  onRemoveUpToMessage?: (message: Message) => void;
};
⋮----
// Group messages by promptContext.group.id, then optionally group assistant messages for compact mode
⋮----
// Get all user message IDs
⋮----
// Anchor to the last item whose top has entered the viewport. Children are in vertical order.
⋮----
const handleScroll = () =>
⋮----
// eslint-disable-next-line no-console
⋮----
aria-label=

================
File: src/renderer/src/components/message/RebaseConflictsActions.tsx
================
import { TiWarning } from 'react-icons/ti';
import { useTranslation } from 'react-i18next';
import { TaskGitStatus } from '@common/types';
⋮----
import { Button } from '@/components/common/Button';
import { useApi } from '@/contexts/ApiContext';
⋮----
type Props = {
  projectDir: string;
  taskId: string;
  status: TaskGitStatus;
};
⋮----
const handleContinueRebase = () =>
⋮----
const handleResolveConflictsWithAgent = () =>
⋮----
const handleAbortRebase = () =>

================
File: src/renderer/src/components/message/RebaseResolvedActions.tsx
================
import { TiInfo } from 'react-icons/ti';
import { useTranslation } from 'react-i18next';
⋮----
import { Button } from '@/components/common/Button';
import { useApi } from '@/contexts/ApiContext';
⋮----
type Props = {
  projectDir: string;
  taskId: string;
};
⋮----
const handleContinueRebase = () =>
⋮----
const handleAbortRebase = () =>

================
File: src/renderer/src/components/message/ReflectedMessageBlock.tsx
================
import { useState } from 'react';
import { MdKeyboardArrowDown, MdKeyboardArrowRight } from 'react-icons/md';
import { useTranslation } from 'react-i18next';
import { Message } from '@common/types';
⋮----
import { CopyMessageButton } from './CopyMessageButton';
⋮----
import { useParsedContent } from '@/hooks/useParsedContent';
⋮----
type Props = {
  baseDir: string;
  message: Message;
  allFiles: string[];
  compact?: boolean;
};

================
File: src/renderer/src/components/message/ResponseMessageBlock.tsx
================
import { clsx } from 'clsx';
import { RiRobot2Line } from 'react-icons/ri';
import { ResponseMessage } from '@common/types';
⋮----
import { MessageBar } from './MessageBar';
⋮----
import { useParsedContent } from '@/hooks/useParsedContent';
⋮----
type Props = {
  baseDir: string;
  message: ResponseMessage;
  allFiles: string[];
  renderMarkdown: boolean;
  compact?: boolean;
  hideMessageBar?: boolean;
  showThinking?: boolean;
  onRemove?: () => void;
  onFork?: () => void;
  onRemoveUpTo?: () => void;
};
⋮----
export const ResponseMessageBlock = ({
  baseDir,
  message,
  allFiles,
  renderMarkdown,
  compact = false,
  hideMessageBar = false,
  showThinking = true,
  onRemove,
  onFork,
  onRemoveUpTo,
}: Props) =>

================
File: src/renderer/src/components/message/RetrieveMemoryToolMessage.tsx
================
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { FaBrain, FaCheckCircle, FaExclamationTriangle, FaTimesCircle, FaTrash } from 'react-icons/fa';
import { CgSpinner } from 'react-icons/cg';
import { ToolMessage } from '@common/types';
⋮----
import { ExpandableMessageBlock } from '@/components/message/ExpandableMessageBlock';
import { StreamingToolMessage } from '@/components/message/StreamingToolMessage';
import { IconButton } from '@/components/common/IconButton';
import { Tooltip } from '@/components/ui/Tooltip';
import { ConfirmDialog } from '@/components/common/ConfirmDialog';
import { useApi } from '@/contexts/ApiContext';
⋮----
type Props = {
  message: ToolMessage;
  onRemove?: () => void;
  compact?: boolean;
  onFork?: () => void;
  onRemoveUpTo?: () => void;
  hideMessageBar?: boolean;
};
⋮----
const loadMemories = async () =>
⋮----
// Ignore errors loading memories
⋮----
const handleDeleteMemory = async () =>
⋮----
// Ignore errors loading memories
⋮----
{/* Query parameters */}
⋮----
{/* Memory List */}
⋮----
onClick=
⋮----
// Fallback for unexpected content (backward compatibility)
⋮----
title=
⋮----
confirmButtonText=

================
File: src/renderer/src/components/message/RunPromptToolMessage.tsx
================
import { useTranslation } from 'react-i18next';
import { RiCheckboxCircleFill, RiCloseCircleFill, RiErrorWarningFill, RiPauseCircleFill } from 'react-icons/ri';
import { CgSpinner } from 'react-icons/cg';
import { MdPlayArrow } from 'react-icons/md';
import { DefaultTaskState, ToolMessage } from '@common/types';
⋮----
import { CodeInline } from '@/components/common/CodeInline';
import { ExpandableMessageBlock } from '@/components/message/ExpandableMessageBlock';
import { TaskStateChip } from '@/components/common/TaskStateChip';
import { Tooltip } from '@/components/ui/Tooltip';
⋮----
type Props = {
  message: ToolMessage;
  onRemove?: () => void;
  compact?: boolean;
  onFork?: () => void;
  onRemoveUpTo?: () => void;
  hideMessageBar?: boolean;
};
⋮----
const renderStatusIcon = () =>

================
File: src/renderer/src/components/message/SearchParentTaskToolMessage.tsx
================
import { useTranslation } from 'react-i18next';
import { RiCheckboxCircleFill, RiCloseCircleFill, RiErrorWarningFill } from 'react-icons/ri';
import { CgSpinner } from 'react-icons/cg';
import { MdOutlineContentPasteSearch } from 'react-icons/md';
import { ToolMessage } from '@common/types';
⋮----
import { CodeInline } from '@/components/common/CodeInline';
import { ExpandableMessageBlock } from '@/components/message/ExpandableMessageBlock';
import { Tooltip } from '@/components/ui/Tooltip';
⋮----
type Props = {
  message: ToolMessage;
  onRemove?: () => void;
  compact?: boolean;
  onFork?: () => void;
  onRemoveUpTo?: () => void;
  hideMessageBar?: boolean;
};

================
File: src/renderer/src/components/message/SearchTaskToolMessage.tsx
================
import { useTranslation } from 'react-i18next';
import { RiCheckboxCircleFill, RiCloseCircleFill, RiErrorWarningFill } from 'react-icons/ri';
import { CgSpinner } from 'react-icons/cg';
import { MdOutlineContentPasteSearch } from 'react-icons/md';
import { ToolMessage } from '@common/types';
⋮----
import { CodeInline } from '@/components/common/CodeInline';
import { ExpandableMessageBlock } from '@/components/message/ExpandableMessageBlock';
import { Tooltip } from '@/components/ui/Tooltip';
⋮----
type Props = {
  message: ToolMessage;
  onRemove?: () => void;
  compact?: boolean;
  onFork?: () => void;
  onRemoveUpTo?: () => void;
  hideMessageBar?: boolean;
};

================
File: src/renderer/src/components/message/SemanticSearchToolMessage.tsx
================
import { useTranslation } from 'react-i18next';
import { RiErrorWarningFill, RiCheckboxCircleFill, RiCloseCircleFill } from 'react-icons/ri';
import { FaSearchengin } from 'react-icons/fa6';
import { CgSpinner } from 'react-icons/cg';
import { ToolMessage } from '@common/types';
⋮----
import { CodeInline } from '@/components/common/CodeInline';
import { ExpandableMessageBlock } from '@/components/message/ExpandableMessageBlock';
import { StreamingToolMessage } from '@/components/message/StreamingToolMessage';
import { Tooltip } from '@/components/ui/Tooltip';
⋮----
type Props = {
  message: ToolMessage;
  taskDir: string;
  onRemove?: () => void;
  compact?: boolean;
  onFork?: () => void;
  onRemoveUpTo?: () => void;
  hideMessageBar?: boolean;
};

================
File: src/renderer/src/components/message/StoreMemoryToolMessage.tsx
================
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { FaBrain, FaCheckCircle, FaExclamationTriangle, FaTimesCircle, FaTrash } from 'react-icons/fa';
import { CgSpinner } from 'react-icons/cg';
import { ToolMessage } from '@common/types';
⋮----
import { ExpandableMessageBlock } from '@/components/message/ExpandableMessageBlock';
import { IconButton } from '@/components/common/IconButton';
import { Tooltip } from '@/components/ui/Tooltip';
import { ConfirmDialog } from '@/components/common/ConfirmDialog';
import { useApi } from '@/contexts/ApiContext';
⋮----
type Props = {
  message: ToolMessage;
  onRemove?: () => void;
  compact?: boolean;
  onFork?: () => void;
  onRemoveUpTo?: () => void;
  hideMessageBar?: boolean;
};
⋮----
type StoredMemoryResult = {
  success: boolean;
  id: string;
};
⋮----
const tryParseJson = (value: string): unknown =>
⋮----
// Tool results are stored JSON-stringified; versions that returned a JSON string from the tool are stored double-encoded
⋮----
const loadMemories = async () =>
⋮----
// Ignore errors loading memories
⋮----
const handleDeleteMemory = async () =>
⋮----
// Ignore errors loading memories
⋮----
{/* Memory Details */}
⋮----
onClick=
⋮----
title=
⋮----
confirmButtonText=

================
File: src/renderer/src/components/message/StreamingToolMessage.tsx
================
import { ReactNode } from 'react';
import { CgSpinner } from 'react-icons/cg';
import { ToolMessage } from '@common/types';
⋮----
import { ExpandableMessageBlock } from '@/components/message/ExpandableMessageBlock';
⋮----
type Props = {
  message: ToolMessage;
  icon: ReactNode;
  label: string;
  compact?: boolean;
  onRemove?: () => void;
  onFork?: () => void;
  onRemoveUpTo?: () => void;
  hideMessageBar?: boolean;
};
⋮----
export const StreamingToolMessage = (

================
File: src/renderer/src/components/message/SubagentToolMessage.tsx
================
import { useTranslation } from 'react-i18next';
import { RiToolsFill } from 'react-icons/ri';
import { CgSpinner } from 'react-icons/cg';
import { FaExclamationTriangle } from 'react-icons/fa';
import { ToolMessage } from '@common/types';
⋮----
import { MessageBar } from '@/components/message/MessageBar';
import { CopyMessageButton } from '@/components/message/CopyMessageButton';
import { Tooltip } from '@/components/ui/Tooltip';
⋮----
type Props = {
  message: ToolMessage;
  onRemove?: () => void;
  compact?: boolean;
  onRemoveUpTo?: () => void;
  hideMessageBar?: boolean;
};
⋮----
const getToolName = (): string =>

================
File: src/renderer/src/components/message/TaskInfoPanel.tsx
================
import { useTranslation } from 'react-i18next';
import { FaFolderOpen } from 'react-icons/fa';
import { MdAssignment, MdClose } from 'react-icons/md';
import { TaskData } from '@common/types';
⋮----
import { CodeInline } from '@/components/common/CodeInline';
import { CopyMessageButton } from '@/components/message/CopyMessageButton';
import { IconButton } from '@/components/common/IconButton';
import { useApi } from '@/contexts/ApiContext';
import { getTaskStateLabel } from '@/components/common/TaskStateChip';
⋮----
type Props = {
  task: TaskData;
  messageCount: number;
  onClose?: () => void;
};
⋮----
const handleOpenFolder = async (path: string) =>
⋮----
// eslint-disable-next-line no-console
⋮----
const formatDate = (dateString?: string) =>
⋮----
onClick=

================
File: src/renderer/src/components/message/TaskStateActions.tsx
================
import { ExtensionUIComponent, Mode, TaskData } from '@common/types';
import { UIComponentProps } from '@common/extensions';
import { ExtensionDisplayAPI } from '@common/api';
⋮----
import { DefaultTaskStateActions } from '@/components/message/DefaultTaskStateActions';
import { ExtensionComponentRenderer } from '@/components/extensions/ExtensionComponentRenderer';
import { useExtensionComponentsWrapper } from '@/components/extensions/useExtensionComponentsWrapper';
import { RebaseConflictsActions } from '@/components/message/RebaseConflictsActions';
import { RebaseResolvedActions } from '@/components/message/RebaseResolvedActions';
import { useTaskGitStatus } from '@/hooks/useTaskGitStatus';
⋮----
type Props = {
  projectDir: string;
  taskId: string;
  state: string | undefined;
  mode?: Mode;
  isArchived: boolean | undefined;
  task?: TaskData | null;
  onResumeTask?: () => void;
  onMarkAsDone?: () => void;
  onRunPrompt?: (prompt: string) => void;
  onArchiveTask?: () => void;
  onUnarchiveTask?: () => void;
  onDeleteTask?: () => void;
};
⋮----
type ChainProps = {
  components: ExtensionUIComponent[];
  defaultTaskActionsProps: Props;
  componentProps: UIComponentProps;
  additionalProps: Record<string, unknown>;
  libraries: Record<string, Record<string, unknown>>;
  currentProjectDir?: string;
  currentTaskId?: string;
  currentActionProjectDir?: string;
  currentActionTaskId?: string;
  api: ExtensionDisplayAPI;
};

================
File: src/renderer/src/components/message/ThinkingAnswerBlock.tsx
================
import { MouseEvent, useState } from 'react';
import { FaChevronRight } from 'react-icons/fa';
import { TfiThought } from 'react-icons/tfi';
import { useTranslation } from 'react-i18next';
import { clsx } from 'clsx';
⋮----
import { CopyMessageButton } from './CopyMessageButton';
⋮----
import { useParsedContent } from '@/hooks/useParsedContent';
⋮----
type Props = {
  thinking: string;
  answer?: string | null;
  baseDir?: string;
  allFiles?: string[];
  renderMarkdown: boolean;
};
⋮----
// Internal state for manual toggle control
⋮----
// Determine expansion state:
// - Start expanded (true) when there's no answer
// - When answer arrives, auto-collapse to false
// - Manual toggle overrides auto-collapse
⋮----
const handleToggleThinking = (e: MouseEvent<HTMLDivElement>) =>
⋮----
{/* Thinking section */}
⋮----
<div className=
⋮----
{/* Answer section - only show if we have an answer or we're streaming */}
⋮----
className=

================
File: src/renderer/src/components/message/ToolMessageBlock.tsx
================
import { ReactNode } from 'react';
import { CgSpinner } from 'react-icons/cg';
import { RiToolsFill } from 'react-icons/ri';
import { useTranslation } from 'react-i18next';
import { VscError } from 'react-icons/vsc';
import {
  AIDER_TOOL_ADD_CONTEXT_FILES,
  AIDER_TOOL_DROP_CONTEXT_FILES,
  AIDER_TOOL_GROUP_NAME,
  AIDER_TOOL_RUN_PROMPT,
  POWER_TOOL_BASH,
  POWER_TOOL_FETCH,
  POWER_TOOL_FILE_READ,
  POWER_TOOL_GLOB,
  POWER_TOOL_GREP,
  POWER_TOOL_GROUP_NAME,
  POWER_TOOL_SEMANTIC_SEARCH,
} from '@common/tools';
import { ToolMessage } from '@common/types';
⋮----
import { CopyMessageButton } from './CopyMessageButton';
import { formatName, parseToolContent } from './utils';
import { ExpandableMessageBlock } from './ExpandableMessageBlock';
import { ToolResultImages } from './ToolResultImages';
⋮----
import { CodeInline } from '@/components/common/CodeInline';
⋮----
type Props = {
  message: ToolMessage;
  onRemove?: () => void;
  compact?: boolean;
  onFork?: () => void;
  onRemoveUpTo?: () => void;
  hideMessageBar?: boolean;
};
⋮----
const defaultLabel = () => t('toolMessage.toolLabel',
⋮----
// Handles message.args.paths (array) or fallback to message.args.path (string)
⋮----
// Handles message.args.paths (array) or fallback to message.args.path (string)
⋮----
// Display pretty-printed JSON
⋮----
rawContentToCopy = JSON.stringify(parsedResult.json); // Copy raw JSON
⋮----
// Display extracted text if inner JSON parsing failed
⋮----
// Fallback to the original raw content if no text was extracted
⋮----

⋮----
{/* Tool Specific Content */}

================
File: src/renderer/src/components/message/ToolResultImages.tsx
================
import { useTranslation } from 'react-i18next';
⋮----
import { MessageImages } from './MessageImages';
⋮----
import type { ToolResultImage } from './utils';
⋮----
type Props = {
  images: ToolResultImage[];
};
⋮----
export const ToolResultImages = (

================
File: src/renderer/src/components/message/UpdateMemoryToolMessage.tsx
================
import { useTranslation } from 'react-i18next';
import { FaBrain, FaCheckCircle, FaExclamationTriangle, FaTimesCircle } from 'react-icons/fa';
import { CgSpinner } from 'react-icons/cg';
import { ToolMessage } from '@common/types';
⋮----
import { ExpandableMessageBlock } from '@/components/message/ExpandableMessageBlock';
import { Tooltip } from '@/components/ui/Tooltip';
⋮----
type Props = {
  message: ToolMessage;
  onRemove?: () => void;
  compact?: boolean;
  onFork?: () => void;
  onRemoveUpTo?: () => void;
  hideMessageBar?: boolean;
};
⋮----
{/* Memory ID */}
⋮----
{/* New Content */}

================
File: src/renderer/src/components/message/UsageInfo.tsx
================
import { useTranslation } from 'react-i18next';
import { FaArrowRightFromBracket, FaArrowRightToBracket, FaDollarSign, FaDownload, FaUpload } from 'react-icons/fa6';
import { UsageReportData } from '@common/types';
⋮----
import { Tooltip } from '@/components/ui/Tooltip';
⋮----
type Props = {
  usageReport: UsageReportData;
  className?: string;
};

================
File: src/renderer/src/components/message/UserMessageBlock.tsx
================
import { FaRegUser } from 'react-icons/fa';
import { clsx } from 'clsx';
import { AnimatePresence, motion } from 'framer-motion';
import { UserMessage } from '@common/types';
⋮----
import { MessageBar } from './MessageBar';
import { MessageImages } from './MessageImages';
⋮----
import { useParsedContent } from '@/hooks/useParsedContent';
⋮----
type Props = {
  baseDir: string;
  message: UserMessage;
  allFiles: string[];
  renderMarkdown: boolean;
  compact?: boolean;
  onRemove?: () => void;
  onRedo?: () => void;
  onEdit?: (content: string, images?: string[]) => void;
  onFork?: () => void;
  onRemoveUpTo?: () => void;
};
⋮----
export const UserMessageBlock = (
⋮----
const handleEdit = () =>
⋮----
className=

================
File: src/renderer/src/components/message/utils.tsx
================
import { Fragment, ReactNode } from 'react';
import ReactMarkdown from 'react-markdown';
import { type Components } from 'react-markdown';
import remarkGfm from 'remark-gfm';
import {
  Group,
  AssistantGroupMessage,
  GroupMessage,
  isResponseMessage,
  isToolMessage,
  isGroupMessage,
  isAssistantGroupMessage,
  Message,
  ResponseMessage,
  ToolMessage,
} from '@common/types';
⋮----
import { CompactionSnippetBlock } from './CompactionSnippetBlock';
import { CustomCommandBashBlock } from './CustomCommandBashBlock';
import { ThinkingAnswerBlock } from './ThinkingAnswerBlock';
⋮----
import { CodeBlock } from '@/components/common/CodeBlock';
import { MermaidDiagram } from '@/components/common/MermaidDiagram';
import { CodeInline } from '@/components/common/CodeInline';
⋮----
export const formatName = (name: string): string =>
⋮----
// Handle code blocks - check if it's a mermaid diagram or regular code
⋮----
// Check if this is a mermaid code block (className includes 'language-mermaid')
⋮----
// Use CodeInline for inline code elements
⋮----
// Basic styling for preformatted blocks (e.g., indented code)
⋮----
// Table styling - wrapped in scrollable container for wide tables
⋮----
// Use the reasoning property directly if available
if (reasoning)
⋮----
// Fallback: check if the content matches the thinking/answer format (for legacy messages)
⋮----
const normalizeListBlocks = (text: string): string =>
⋮----
const processCodeBlock = () =>
⋮----
// Check if the line is just a filepath
⋮----
// Check if line ends with a filepath
⋮----
const findFileInNextLine = (currentLine: number):
⋮----
// Check if the next line is just a filepath
⋮----
// Check if the next line starts with a filepath
⋮----
// Check for custom-command-bash blocks
⋮----
// Process any accumulated text first
⋮----
// Find the end of the custom-command-bash block
⋮----
// Extract the content between the tags
⋮----
// Skip to after the closing tag
⋮----
// Check for <file-edited> compaction placeholder tags
⋮----
// Check if line starts a code block
⋮----
// Try finding the file in the previous line first
⋮----
// Remove the last line from currentText if it contains the filename
⋮----
// If not found in the previous line, check the next line
⋮----
// Skip the next line in the loop
⋮----
// Extract language for ``` fence
⋮----
// Handle inline code ticks
⋮----
// Check if line ends the code block
⋮----
// Handle any remaining content
⋮----
export const parseThinkingAnswerFormat = (
  content: string,
  baseDir: string = '',
  allFiles: string[] = [],
  renderMarkdown = false,
  renderThinking = true,
): ReactNode | null =>
⋮----
// Check for the thinking section first
⋮----
// Check if the answer section exists
⋮----
export const parseCustomCommandBashFormat = (baseDir: string, content: string): ReactNode | null =>
⋮----
// Find the opening tag
⋮----
// Find the closing tag
⋮----
// Extract content between opening and closing tags
⋮----
// Find command tags
⋮----
// Find output tags (search after the command closing tag)
⋮----
// Extract command and output content
⋮----
// --- Tool Message Parsing ---
interface ParsedToolContentItem {
  type: string;
  text?: string;
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  [key: string]: any; // Allow other properties
}
⋮----
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[key: string]: any; // Allow other properties
⋮----
interface ParsedToolMessage {
  content: ParsedToolContentItem[];
  isError: boolean;
}
⋮----
export interface ToolResultImage {
  data: string;
  mediaType: string;
}
⋮----
export interface ToolContentResult {
  extractedText: string | null;
  json: object | null;
  isError: boolean | null;
  rawContent: string;
  images: ToolResultImage[];
}
⋮----
const extractImagesFromContentArray = (content: ParsedToolContentItem[]): ToolResultImage[] =>
⋮----
const processContentArray = (content: ParsedToolContentItem[], result: ToolContentResult) =>
⋮----
// Extract images from the content array
⋮----
// Extract text from the 'content' array
⋮----
// Try parsing the extracted text as JSON
⋮----
// eslint-disable-next-line no-console
⋮----
/**
 * Parses the content string from a ToolMessage.
 * Handles multiple storage formats:
 * 1. Raw MCP result: { content: [{ type: 'text' }, { type: 'image' }], isError: false }
 * 2. AI SDK ToolResultOutput content array: [{ type: 'text' }, { type: 'file' }]
 * 3. Wrapped ToolResultOutput: { type: 'content', value: [...] }
 * 4. Wrapped JSON: { type: 'json', value: { content: [...] } }
 */
export const parseToolContent = (rawContent: string): ToolContentResult =>
⋮----
// eslint-disable-next-line no-console
⋮----
// Format 2: Direct content array [{ type: 'text' }, { type: 'file' }]
⋮----
// Format 3: Wrapped ToolResultOutput { type: 'content', value: [...] }
⋮----
// Format 4: Wrapped JSON { type: 'json', value: { content: [...] } }
⋮----
// Format 1: Raw MCP result { content: [...], isError: false }
⋮----
// Fallback: treat as JSON object
⋮----
// eslint-disable-next-line no-console
⋮----
export const includeMessageProperty = (message: Message): boolean =>
⋮----
/**
 * Groups ResponseMessage with their following ToolMessages into AssistantGroupMessage.
 * This is used for the compact view mode where assistant turns are displayed as a single unit.
 *
 * Rules:
 * - A ResponseMessage followed by ToolMessages forms a single group
 * - A new ResponseMessage starts a new group
 * - Orphan ToolMessages (without preceding ResponseMessage) remain standalone
 * - Other message types remain standalone
 */
export const groupAssistantMessages = (messages: Message[]): Message[] =>
⋮----
const nextUniqueId = (message: Message): string =>
⋮----
const pushWithUniqueId = (message: Message) =>
⋮----
const flushCurrentGroup = () =>
⋮----
// Create an AssistantGroupMessage
⋮----
// No tool messages, just push the response as-is
⋮----
// Flush any previous group before starting a new one
⋮----
// Add to current group
⋮----
// Orphan tool message - push as-is
⋮----
// Other message types - flush current group and push as-is
⋮----
// Flush any remaining group
⋮----
export const areMessagesEqual = (prevMessage: Message, nextMessage: Message): boolean =>
⋮----
// Check basic message properties
⋮----
// Check reasoning for ResponseMessage
⋮----
// Check usageReport for ResponseMessage and ToolMessage
⋮----
// Check args for ToolMessage
⋮----
// LegendList keys items by id; duplicate ids in the list data cause a
// measurement/re-render feedback loop that crashes with React error #185,
// so every item pushed here must have a unique id.
export const groupMessagesByPromptContext = (messages: Message[]): Message[] =>
⋮----
// First pass: collect messages with groups
⋮----
// Track the latest group information for this group ID, but only update if current group isn't finished
⋮----
// Create GroupMessage for the first message in the group
⋮----
// Only create the group once

================
File: src/renderer/src/components/message/VirtualizedMessages.tsx
================
import { forwardRef, memo, RefObject, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState } from 'react';
import { toPng } from 'html-to-image';
import { MdKeyboardDoubleArrowDown } from 'react-icons/md';
import { useTranslation } from 'react-i18next';
import { LegendList, type LegendListRef } from '@legendapp/list/react';
import { GroupMessage, isUserMessage, Message, MessageViewMode } from '@common/types';
import { twMerge } from 'tailwind-merge';
⋮----
import { MessageBlockWrapper } from './MessageBlockWrapper';
import { MessageMap } from './MessageMap';
⋮----
import { IconButton } from '@/components/common/IconButton';
import { groupAssistantMessages, groupMessagesByPromptContext } from '@/components/message/utils';
import { useUserMessageNavigation } from '@/hooks/useUserMessageNavigation';
import { useSettingsStore } from '@/stores/settingsStore';
⋮----
export type VirtualizedMessagesRef = {
  exportToImage: () => void;
  scrollToBottom: () => void;
};
⋮----
type Props = {
  baseDir: string;
  taskId: string;
  inProgress: boolean;
  messages: Message[];
  allFiles?: string[];
  renderMarkdown: boolean;
  onContainerRef?: (container: HTMLDivElement | null) => void;
  removeMessage?: (message: Message) => void;
  removeGroup?: (group: GroupMessage) => void;
  redoUserPrompt?: (messageId: string) => void;
  editUserMessage?: (messageId: string, content: string, images?: string[]) => void;
  onInterrupt?: () => void;
  onForkFromMessage?: (message: Message) => void;
  onRemoveUpToMessage?: (message: Message) => void;
};
⋮----
const isMoreThanThresholdFromBottom = () =>
⋮----
const handleWheel = (e: WheelEvent) =>
⋮----
const handleTouchStart = (e: TouchEvent) =>
⋮----
const handleTouchMove = (e: TouchEvent) =>
⋮----
const handlePointerDown = () =>
⋮----
const handlePointerUp = () =>
⋮----
const scrollToEndIfFollowing = () =>
⋮----
// eslint-disable-next-line no-console
⋮----
<div className=
⋮----
aria-label=

================
File: src/renderer/src/components/ModelLibrary/providers/AlibabaPlanModelOverrides.tsx
================
import { getDefaultProviderParams, AlibabaPlanProvider, LlmProvider } from '@common/agent';
⋮----
import { AlibabaPlanThinkingSettings } from './AlibabaPlanThinkingSettings';
⋮----
type Props = {
  provider: LlmProvider;
  overrides: Partial<AlibabaPlanProvider>;
  onChange: (overrides: Record<string, unknown>) => void;
};
⋮----
export const AlibabaPlanModelOverrides = (
⋮----
const handleProviderChange = (updatedProvider: AlibabaPlanProvider) =>

================
File: src/renderer/src/components/ModelLibrary/providers/AlibabaPlanParameters.tsx
================
import { ChangeEvent } from 'react';
import { useTranslation } from 'react-i18next';
import { AlibabaPlanProvider } from '@common/agent';
⋮----
import { AlibabaPlanThinkingSettings } from './AlibabaPlanThinkingSettings';
⋮----
import { Input } from '@/components/common/Input';
import { useEffectiveEnvironmentVariable } from '@/hooks/useEffectiveEnvironmentVariable';
⋮----
type Props = {
  provider: AlibabaPlanProvider;
  onChange: (updated: AlibabaPlanProvider) => void;
};
⋮----
const handleApiKeyChange = (e: ChangeEvent<HTMLInputElement>) =>
⋮----
label=

================
File: src/renderer/src/components/ModelLibrary/providers/AlibabaPlanThinkingSettings.tsx
================
import { useTranslation } from 'react-i18next';
import { AlibabaPlanProvider } from '@common/agent';
⋮----
import { Slider } from '@/components/common/Slider';
import { Checkbox } from '@/components/common/Checkbox';
import { InfoIcon } from '@/components/common/InfoIcon';
⋮----
type Props = {
  provider: AlibabaPlanProvider;
  onChange: (updated: AlibabaPlanProvider) => void;
};
⋮----
const handleThinkingEnabledChange = (checked: boolean) =>
⋮----
const handleThinkingBudgetChange = (value: number) =>
⋮----
label=

================
File: src/renderer/src/components/ModelLibrary/providers/AnthropicCompatibleParameters.tsx
================
import { ChangeEvent } from 'react';
import { useTranslation } from 'react-i18next';
import { AnthropicCompatibleProvider } from '@common/agent';
⋮----
import { Input } from '@/components/common/Input';
import { TlsSettings } from '@/components/ModelLibrary/providers/TlsSettings';
import { useEffectiveEnvironmentVariable } from '@/hooks/useEffectiveEnvironmentVariable';
⋮----
type Props = {
  provider: AnthropicCompatibleProvider;
  onChange: (updated: AnthropicCompatibleProvider) => void;
};
⋮----
export const AnthropicCompatibleParameters = (
⋮----
const handleBaseUrlChange = (e: ChangeEvent<HTMLInputElement>) =>
⋮----
const handleApiKeyChange = (e: ChangeEvent<HTMLInputElement>) =>
⋮----
label=

================
File: src/renderer/src/components/ModelLibrary/providers/AnthropicParameters.tsx
================
import { ChangeEvent } from 'react';
import { useTranslation } from 'react-i18next';
import { AnthropicProvider } from '@common/agent';
⋮----
import { Input } from '@/components/common/Input';
import { useEffectiveEnvironmentVariable } from '@/hooks/useEffectiveEnvironmentVariable';
⋮----
type Props = {
  provider: AnthropicProvider;
  onChange: (updated: AnthropicProvider) => void;
};
⋮----
export const AnthropicParameters = (
⋮----
const handleApiKeyChange = (e: ChangeEvent<HTMLInputElement>) =>
⋮----
label=

================
File: src/renderer/src/components/ModelLibrary/providers/AzureAdvancedSettings.tsx
================
import { useTranslation } from 'react-i18next';
import { AzureProvider } from '@common/agent';
import { ReasoningEffort } from '@common/types';
⋮----
import { Select, Option } from '@/components/common/Select';
⋮----
type Props = {
  provider: AzureProvider;
  onChange: (updated: AzureProvider) => void;
};
⋮----
export const AzureAdvancedSettings = (
⋮----
const handleReasoningEffortChange = (value: string) =>
⋮----
<Select label=

================
File: src/renderer/src/components/ModelLibrary/providers/AzureModelOverrides.tsx
================
import { getDefaultProviderParams, AzureProvider, LlmProvider } from '@common/agent';
⋮----
import { DisableStreaming } from '../DisableStreaming';
import { DisableToolCallStreaming } from '../DisableToolCallStreaming';
⋮----
import { AzureAdvancedSettings } from './AzureAdvancedSettings';
⋮----
type Props = {
  provider: LlmProvider;
  overrides: Partial<AzureProvider>;
  onChange: (overrides: Record<string, unknown>) => void;
};
⋮----
export const AzureModelOverrides = (
⋮----
const handleProviderChange = (updatedProvider: AzureProvider) =>
⋮----
const handleDisableStreamingChange = (disableStreaming: boolean) =>
⋮----
const handleDisableToolCallStreamingChange = (disableToolCallStreaming: boolean) =>

================
File: src/renderer/src/components/ModelLibrary/providers/AzureParameters.tsx
================
import { ChangeEvent } from 'react';
import { useTranslation } from 'react-i18next';
import { AzureProvider } from '@common/agent';
⋮----
import { Input } from '@/components/common/Input';
import { useEffectiveEnvironmentVariable } from '@/hooks/useEffectiveEnvironmentVariable';
⋮----
type Props = {
  provider: AzureProvider;
  onChange: (updated: AzureProvider) => void;
};
⋮----
// Environment variable hooks for placeholders
⋮----
const handleApiKeyChange = (e: ChangeEvent<HTMLInputElement>) =>
⋮----
const handleResourceNameChange = (e: ChangeEvent<HTMLInputElement>) =>
⋮----
const handleApiVersionChange = (e: ChangeEvent<HTMLInputElement>) =>
⋮----
label=
⋮----
<p className="text-xs text-info-lightest">

================
File: src/renderer/src/components/ModelLibrary/providers/BedrockParameters.tsx
================
import { ChangeEvent } from 'react';
import { useTranslation } from 'react-i18next';
import { BedrockProvider } from '@common/agent';
⋮----
import { Input } from '@/components/common/Input';
import { InfoIcon } from '@/components/common/InfoIcon';
import { useEffectiveEnvironmentVariable } from '@/hooks/useEffectiveEnvironmentVariable';
⋮----
type Props = {
  provider: BedrockProvider;
  onChange: (updated: BedrockProvider) => void;
};
⋮----
const handleRegionChange = (e: ChangeEvent<HTMLInputElement>) =>
⋮----
const handleAccessKeyIdChange = (e: ChangeEvent<HTMLInputElement>) =>
⋮----
const handleSecretAccessKeyChange = (e: ChangeEvent<HTMLInputElement>) =>
⋮----
const handleSessionTokenChange = (e: ChangeEvent<HTMLInputElement>) =>
⋮----
label=
⋮----
awsAccessKeyIdEnv
⋮----
<span>
⋮----
awsSecretAccessKeyEnv

================
File: src/renderer/src/components/ModelLibrary/providers/CerebrasParameters.tsx
================
import { ChangeEvent } from 'react';
import { useTranslation } from 'react-i18next';
import { CerebrasProvider } from '@common/agent';
⋮----
import { Input } from '@/components/common/Input';
import { useEffectiveEnvironmentVariable } from '@/hooks/useEffectiveEnvironmentVariable';
⋮----
type Props = {
  provider: CerebrasProvider;
  onChange: (updated: CerebrasProvider) => void;
};
⋮----
const handleApiKeyChange = (e: ChangeEvent<HTMLInputElement>) =>
⋮----
label=

================
File: src/renderer/src/components/ModelLibrary/providers/ClinePassParameters.tsx
================
import { ChangeEvent } from 'react';
import { useTranslation } from 'react-i18next';
import { ClinePassProvider } from '@common/agent';
⋮----
import { Input } from '@/components/common/Input';
import { useEffectiveEnvironmentVariable } from '@/hooks/useEffectiveEnvironmentVariable';
⋮----
type Props = {
  provider: ClinePassProvider;
  onChange: (updated: ClinePassProvider) => void;
};
⋮----
const handleApiKeyChange = (e: ChangeEvent<HTMLInputElement>) =>
⋮----
label=

================
File: src/renderer/src/components/ModelLibrary/providers/DeepseekModelOverrides.tsx
================
import { getDefaultProviderParams, DeepseekProvider, DeepseekReasoningEffort, LlmProvider } from '@common/agent';
import { useTranslation } from 'react-i18next';
⋮----
import { DisableStreaming } from '../DisableStreaming';
import { DisableToolCallStreaming } from '../DisableToolCallStreaming';
⋮----
import { Checkbox } from '@/components/common/Checkbox';
import { Select, Option } from '@/components/common/Select';
⋮----
type Props = {
  provider: LlmProvider;
  overrides: Partial<DeepseekProvider>;
  onChange: (overrides: Record<string, unknown>) => void;
};
⋮----
const handleThinkingEnabledChange = (checked: boolean) =>
⋮----
const handleReasoningEffortChange = (value: string) =>
⋮----
const handleDisableStreamingChange = (disableStreaming: boolean) =>
⋮----
const handleDisableToolCallStreamingChange = (disableToolCallStreaming: boolean) =>
⋮----
<Select label=

================
File: src/renderer/src/components/ModelLibrary/providers/DeepseekParameters.tsx
================
import { ChangeEvent } from 'react';
import { useTranslation } from 'react-i18next';
import { DeepseekProvider, DeepseekReasoningEffort } from '@common/agent';
⋮----
import { Input } from '@/components/common/Input';
import { Checkbox } from '@/components/common/Checkbox';
import { Select, Option } from '@/components/common/Select';
import { useEffectiveEnvironmentVariable } from '@/hooks/useEffectiveEnvironmentVariable';
⋮----
type Props = {
  provider: DeepseekProvider;
  onChange: (updated: DeepseekProvider) => void;
};
⋮----
const handleApiKeyChange = (e: ChangeEvent<HTMLInputElement>) =>
⋮----
const handleThinkingEnabledChange = (checked: boolean) =>
⋮----
const handleReasoningEffortChange = (value: string) =>
⋮----
label=
⋮----
<span className="text-sm font-medium">
⋮----
<Select label=

================
File: src/renderer/src/components/ModelLibrary/providers/DefaultModelOverrides.tsx
================
import { getDefaultProviderParams, LlmProvider } from '@common/agent';
⋮----
import { DisableStreaming } from '../DisableStreaming';
import { DisableToolCallStreaming } from '../DisableToolCallStreaming';
⋮----
type Props = {
  provider: LlmProvider;
  overrides: Record<string, unknown>;
  onChange: (overrides: Record<string, unknown>) => void;
};
⋮----
export const DefaultModelOverrides = (
⋮----
const handleDisableStreamingChange = (disableStreaming: boolean) =>
⋮----
const handleDisableToolCallStreamingChange = (disableToolCallStreaming: boolean) =>

================
File: src/renderer/src/components/ModelLibrary/providers/EnableVoiceControl.tsx
================
import { useTranslation } from 'react-i18next';
⋮----
import { Checkbox } from '@/components/common/Checkbox';
⋮----
type Props = {
  voiceEnabled: boolean;
  onChange: (enabled: boolean) => void;
};

================
File: src/renderer/src/components/ModelLibrary/providers/GeminiAdvancedSettings.tsx
================
import { useTranslation } from 'react-i18next';
import { GeminiProvider } from '@common/agent';
⋮----
import { Slider } from '@/components/common/Slider';
import { Checkbox } from '@/components/common/Checkbox';
import { InfoIcon } from '@/components/common/InfoIcon';
⋮----
type Props = {
  provider: GeminiProvider;
  onChange: (updated: GeminiProvider) => void;
};
⋮----
const handleThinkingBudgetChange = (value: number) =>
⋮----
const handleIncludeThoughtsChange = (checked: boolean) =>
⋮----
const handleUseSearchGroundingChange = (checked: boolean) =>
⋮----
label=
⋮----
<Checkbox label=

================
File: src/renderer/src/components/ModelLibrary/providers/GeminiModelOverrides.tsx
================
import { getDefaultProviderParams, GeminiProvider, LlmProvider } from '@common/agent';
⋮----
import { DisableStreaming } from '../DisableStreaming';
import { DisableToolCallStreaming } from '../DisableToolCallStreaming';
⋮----
import { GeminiAdvancedSettings } from './GeminiAdvancedSettings';
⋮----
type Props = {
  provider: LlmProvider;
  overrides: Partial<GeminiProvider>;
  onChange: (overrides: Record<string, unknown>) => void;
};
⋮----
export const GeminiModelOverrides = (
⋮----
// Convert overrides to GeminiProvider format for AdvancedSettings
⋮----
// Convert GeminiProvider back to overrides format
const handleProviderChange = (updatedProvider: GeminiProvider) =>
⋮----
// Remove undefined values
⋮----
// Handle disable streaming change separately
const handleDisableStreamingChange = (disableStreaming: boolean) =>
⋮----
const handleDisableToolCallStreamingChange = (disableToolCallStreaming: boolean) =>

================
File: src/renderer/src/components/ModelLibrary/providers/GeminiParameters.tsx
================
import { ChangeEvent } from 'react';
import { useTranslation } from 'react-i18next';
import { GeminiProvider } from '@common/agent';
⋮----
import { GeminiAdvancedSettings } from './GeminiAdvancedSettings';
import { EnableVoiceControl } from './EnableVoiceControl';
⋮----
import { Input } from '@/components/common/Input';
import { useEffectiveEnvironmentVariable } from '@/hooks/useEffectiveEnvironmentVariable';
⋮----
type Props = {
  provider: GeminiProvider;
  onChange: (updated: GeminiProvider) => void;
};
⋮----
const handleApiKeyChange = (e: ChangeEvent<HTMLInputElement>) =>
⋮----
const handleCustomBaseUrlChange = (e: ChangeEvent<HTMLInputElement>) =>
⋮----
const handleVoiceEnabledChange = (checked: boolean) =>
⋮----
label=

================
File: src/renderer/src/components/ModelLibrary/providers/GpustackParameters.tsx
================
import { ChangeEvent } from 'react';
import { useTranslation } from 'react-i18next';
import { GpustackProvider } from '@common/agent';
⋮----
import { Input } from '@/components/common/Input';
import { useEffectiveEnvironmentVariable } from '@/hooks/useEffectiveEnvironmentVariable';
⋮----
type Props = {
  provider: GpustackProvider;
  onChange: (updated: GpustackProvider) => void;
};
⋮----
export const GpustackParameters = (
⋮----
const handleBaseUrlChange = (e: ChangeEvent<HTMLInputElement>) =>
⋮----
const handleApiKeyChange = (e: ChangeEvent<HTMLInputElement>) =>
⋮----
label=

================
File: src/renderer/src/components/ModelLibrary/providers/GroqParameters.tsx
================
import { ChangeEvent } from 'react';
import { useTranslation } from 'react-i18next';
import { GroqProvider } from '@common/agent';
⋮----
import { Input } from '@/components/common/Input';
import { useEffectiveEnvironmentVariable } from '@/hooks/useEffectiveEnvironmentVariable';
⋮----
type Props = {
  provider: GroqProvider;
  onChange: (updated: GroqProvider) => void;
};
⋮----
const handleApiKeyChange = (e: ChangeEvent<HTMLInputElement>) =>
⋮----
label=

================
File: src/renderer/src/components/ModelLibrary/providers/index.ts
================
// Advanced Settings
⋮----
// Thinking Settings
⋮----
// Model Overrides

================
File: src/renderer/src/components/ModelLibrary/providers/KimiPlanParameters.tsx
================
import { ChangeEvent } from 'react';
import { useTranslation } from 'react-i18next';
import { KimiPlanProvider } from '@common/agent';
⋮----
import { Input } from '@/components/common/Input';
import { useEffectiveEnvironmentVariable } from '@/hooks/useEffectiveEnvironmentVariable';
⋮----
type Props = {
  provider: KimiPlanProvider;
  onChange: (updated: KimiPlanProvider) => void;
};
⋮----
const handleApiKeyChange = (e: ChangeEvent<HTMLInputElement>) =>
⋮----
label=

================
File: src/renderer/src/components/ModelLibrary/providers/LitellmParameters.tsx
================
import { ChangeEvent } from 'react';
import { useTranslation } from 'react-i18next';
import { LitellmProvider } from '@common/agent';
⋮----
import { Input } from '@/components/common/Input';
import { useEffectiveEnvironmentVariable } from '@/hooks/useEffectiveEnvironmentVariable';
⋮----
type Props = {
  provider: LitellmProvider;
  onChange: (updated: LitellmProvider) => void;
};
⋮----
export const LitellmParameters = (
⋮----
const handleBaseUrlChange = (e: ChangeEvent<HTMLInputElement>) =>
⋮----
const handleApiKeyChange = (e: ChangeEvent<HTMLInputElement>) =>
⋮----
label=

================
File: src/renderer/src/components/ModelLibrary/providers/LlmApiParameters.tsx
================
import { ChangeEvent } from 'react';
import { useTranslation } from 'react-i18next';
import { LlmApiProvider } from '@common/agent';
⋮----
import { Input } from '@/components/common/Input';
import { useEffectiveEnvironmentVariable } from '@/hooks/useEffectiveEnvironmentVariable';
⋮----
type Props = {
  provider: LlmApiProvider;
  onChange: (updated: LlmApiProvider) => void;
};
⋮----
const handleApiKeyChange = (e: ChangeEvent<HTMLInputElement>) =>
⋮----
label=

================
File: src/renderer/src/components/ModelLibrary/providers/LmStudioParameters.tsx
================
import { ChangeEvent } from 'react';
import { useTranslation } from 'react-i18next';
import { LmStudioProvider } from '@common/agent';
⋮----
import { Input } from '@/components/common/Input';
import { useEffectiveEnvironmentVariable } from '@/hooks/useEffectiveEnvironmentVariable';
⋮----
type Props = {
  provider: LmStudioProvider;
  onChange: (updated: LmStudioProvider) => void;
};
⋮----
export const LmStudioParameters = (
⋮----
const handleBaseUrlChange = (e: ChangeEvent<HTMLInputElement>) =>
⋮----
label=

================
File: src/renderer/src/components/ModelLibrary/providers/MinimaxParameters.tsx
================
import { ChangeEvent } from 'react';
import { useTranslation } from 'react-i18next';
import { MinimaxProvider } from '@common/agent';
⋮----
import { Input } from '@/components/common/Input';
import { useEffectiveEnvironmentVariable } from '@/hooks/useEffectiveEnvironmentVariable';
⋮----
type Props = {
  provider: MinimaxProvider;
  onChange: (updated: MinimaxProvider) => void;
};
⋮----
const handleApiKeyChange = (e: ChangeEvent<HTMLInputElement>) =>
⋮----
label=

================
File: src/renderer/src/components/ModelLibrary/providers/MistralParameters.tsx
================
import { ChangeEvent } from 'react';
import { useTranslation } from 'react-i18next';
import { MistralProvider } from '@common/agent';
⋮----
import { Input } from '@/components/common/Input';
import { useEffectiveEnvironmentVariable } from '@/hooks/useEffectiveEnvironmentVariable';
⋮----
type Props = {
  provider: MistralProvider;
  onChange: (updated: MistralProvider) => void;
};
⋮----
const handleApiKeyChange = (e: ChangeEvent<HTMLInputElement>) =>
⋮----
label=

================
File: src/renderer/src/components/ModelLibrary/providers/NeuralwattAdvancedSettings.tsx
================
import { useTranslation } from 'react-i18next';
import { NeuralwattProvider } from '@common/agent';
import { ReasoningEffort } from '@common/types';
⋮----
import { InfoIcon } from '@/components/common/InfoIcon';
import { Select, Option } from '@/components/common/Select';
⋮----
type Props = {
  provider: NeuralwattProvider;
  onChange: (updated: NeuralwattProvider) => void;
};
⋮----
const handleReasoningEffortChange = (value: string) =>
⋮----
<span>

================
File: src/renderer/src/components/ModelLibrary/providers/NeuralwattModelOverrides.tsx
================
import { getDefaultProviderParams, LlmProvider, NeuralwattProvider } from '@common/agent';
⋮----
import { DisableStreaming } from '../DisableStreaming';
import { DisableToolCallStreaming } from '../DisableToolCallStreaming';
⋮----
import { NeuralwattAdvancedSettings } from './NeuralwattAdvancedSettings';
⋮----
type Props = {
  provider: LlmProvider;
  overrides: Partial<NeuralwattProvider>;
  onChange: (overrides: Record<string, unknown>) => void;
};
⋮----
export const NeuralwattModelOverrides = (
⋮----
const handleProviderChange = (updatedProvider: NeuralwattProvider) =>
⋮----
// Remove undefined values
⋮----
// Handle disable streaming change separately
const handleDisableStreamingChange = (disableStreaming: boolean) =>
⋮----
const handleDisableToolCallStreamingChange = (disableToolCallStreaming: boolean) =>

================
File: src/renderer/src/components/ModelLibrary/providers/NeuralwattParameters.tsx
================
import { ChangeEvent } from 'react';
import { useTranslation } from 'react-i18next';
import { NeuralwattProvider } from '@common/agent';
⋮----
import { NeuralwattAdvancedSettings } from './NeuralwattAdvancedSettings';
⋮----
import { Input } from '@/components/common/Input';
import { useEffectiveEnvironmentVariable } from '@/hooks/useEffectiveEnvironmentVariable';
⋮----
type Props = {
  provider: NeuralwattProvider;
  onChange: (updated: NeuralwattProvider) => void;
};
⋮----
const handleApiKeyChange = (e: ChangeEvent<HTMLInputElement>) =>
⋮----
label=

================
File: src/renderer/src/components/ModelLibrary/providers/OllamaParameters.tsx
================
import { ChangeEvent } from 'react';
import { useTranslation } from 'react-i18next';
import { OllamaProvider } from '@common/agent';
⋮----
import { Input } from '@/components/common/Input';
import { useEffectiveEnvironmentVariable } from '@/hooks/useEffectiveEnvironmentVariable';
⋮----
type Props = {
  provider: OllamaProvider;
  onChange: (updated: OllamaProvider) => void;
};
⋮----
export const OllamaParameters = (
⋮----
const handleBaseUrlChange = (e: ChangeEvent<HTMLInputElement>) =>
⋮----
label=

================
File: src/renderer/src/components/ModelLibrary/providers/OpenAiAdvancedSettings.tsx
================
import { useTranslation } from 'react-i18next';
import { OpenAiProvider } from '@common/agent';
import { ReasoningEffort } from '@common/types';
⋮----
import { Select, Option } from '@/components/common/Select';
import { Checkbox } from '@/components/common/Checkbox';
import { InfoIcon } from '@/components/common/InfoIcon';
⋮----
type Props = {
  provider: OpenAiProvider;
  onChange: (updated: OpenAiProvider) => void;
};
⋮----
const handleReasoningEffortChange = (value: string) =>
⋮----
const handleUseWebSearchChange = (checked: boolean) =>
⋮----
const handleStoreChange = (checked: boolean) =>
⋮----
<Select label=

================
File: src/renderer/src/components/ModelLibrary/providers/OpenAiCompatibleAdvancedSettings.tsx
================
import { ChangeEvent, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { OpenAiCompatibleProvider } from '@common/agent';
import { ReasoningEffort } from '@common/types';
⋮----
import { Select, Option } from '@/components/common/Select';
import { Checkbox } from '@/components/common/Checkbox';
import { TextArea } from '@/components/common/TextArea';
import { Accordion } from '@/components/common/Accordion';
import { InfoIcon } from '@/components/common/InfoIcon';
⋮----
type Props = {
  provider: OpenAiCompatibleProvider;
  onChange: (updated: OpenAiCompatibleProvider) => void;
};
⋮----
const handleReasoningEffortChange = (value: string) =>
⋮----
const handleTrackTokenUsageChange = (trackTokenUsage: boolean) =>
⋮----
const handleExtraBodyChange = (e: ChangeEvent<HTMLTextAreaElement>) =>
⋮----
<span>
⋮----
<Checkbox label=

================
File: src/renderer/src/components/ModelLibrary/providers/OpenAiCompatibleModelOverrides.tsx
================
import { getDefaultProviderParams, LlmProvider, OpenAiCompatibleProvider } from '@common/agent';
⋮----
import { DisableStreaming } from '../DisableStreaming';
import { DisableToolCallStreaming } from '../DisableToolCallStreaming';
⋮----
import { OpenAiCompatibleAdvancedSettings } from './OpenAiCompatibleAdvancedSettings';
⋮----
type Props = {
  provider: LlmProvider;
  overrides: Partial<OpenAiCompatibleProvider>;
  onChange: (overrides: Record<string, unknown>) => void;
};
⋮----
export const OpenAiCompatibleModelOverrides = (
⋮----
// Convert overrides to OpenAiCompatibleProvider format for AdvancedSettings
⋮----
// Convert OpenAiCompatibleProvider back to overrides format
const handleProviderChange = (updatedProvider: OpenAiCompatibleProvider) =>
⋮----
// Remove undefined values
⋮----
// Handle disable streaming change separately
const handleDisableStreamingChange = (disableStreaming: boolean) =>
⋮----
const handleDisableToolCallStreamingChange = (disableToolCallStreaming: boolean) =>

================
File: src/renderer/src/components/ModelLibrary/providers/OpenAiCompatibleParameters.tsx
================
import { ChangeEvent } from 'react';
import { useTranslation } from 'react-i18next';
import { OpenAiCompatibleProvider } from '@common/agent';
⋮----
import { OpenAiCompatibleAdvancedSettings } from './OpenAiCompatibleAdvancedSettings';
import { TlsSettings } from './TlsSettings';
⋮----
import { Input } from '@/components/common/Input';
import { useEffectiveEnvironmentVariable } from '@/hooks/useEffectiveEnvironmentVariable';
⋮----
type Props = {
  provider: OpenAiCompatibleProvider;
  onChange: (updated: OpenAiCompatibleProvider) => void;
};
⋮----
export const OpenAiCompatibleParameters = (
⋮----
const handleBaseUrlChange = (e: ChangeEvent<HTMLInputElement>) =>
⋮----
const handleApiKeyChange = (e: ChangeEvent<HTMLInputElement>) =>
⋮----
label=

================
File: src/renderer/src/components/ModelLibrary/providers/OpenAiModelOverrides.tsx
================
import { getDefaultProviderParams, LlmProvider, OpenAiProvider } from '@common/agent';
⋮----
import { DisableStreaming } from '../DisableStreaming';
import { DisableToolCallStreaming } from '../DisableToolCallStreaming';
⋮----
import { OpenAiAdvancedSettings } from './OpenAiAdvancedSettings';
⋮----
type Props = {
  provider: LlmProvider;
  overrides: Partial<OpenAiProvider>;
  onChange: (overrides: Record<string, unknown>) => void;
};
⋮----
export const OpenAiModelOverrides = (
⋮----
// Convert overrides to OpenAiProvider format for AdvancedSettings
⋮----
// Convert OpenAiProvider back to overrides format
const handleProviderChange = (updatedProvider: OpenAiProvider) =>
⋮----
// Remove undefined values
⋮----
// Handle disable streaming change separately
const handleDisableStreamingChange = (disableStreaming: boolean) =>
⋮----
const handleDisableToolCallStreamingChange = (disableToolCallStreaming: boolean) =>

================
File: src/renderer/src/components/ModelLibrary/providers/OpenAiParameters.tsx
================
import { ChangeEvent } from 'react';
import { useTranslation } from 'react-i18next';
import { OpenAiProvider } from '@common/agent';
⋮----
import { OpenAiAdvancedSettings } from './OpenAiAdvancedSettings';
import { EnableVoiceControl } from './EnableVoiceControl';
⋮----
import { Input } from '@/components/common/Input';
import { useEffectiveEnvironmentVariable } from '@/hooks/useEffectiveEnvironmentVariable';
⋮----
type Props = {
  provider: OpenAiProvider;
  onChange: (updated: OpenAiProvider) => void;
};
⋮----
const handleApiKeyChange = (e: ChangeEvent<HTMLInputElement>) =>
⋮----
const handleVoiceEnabledChange = (checked: boolean) =>
⋮----
label=

================
File: src/renderer/src/components/ModelLibrary/providers/OpenCodeAdvancedSettings.tsx
================
export const OpenCodeAdvancedSettings = () =>

================
File: src/renderer/src/components/ModelLibrary/providers/OpenCodeGoAdvancedSettings.tsx
================
export const OpenCodeGoAdvancedSettings = () =>

================
File: src/renderer/src/components/ModelLibrary/providers/OpenCodeGoModelOverrides.tsx
================
import { getDefaultProviderParams, LlmProvider } from '@common/agent';
⋮----
import { DisableStreaming } from '../DisableStreaming';
import { DisableToolCallStreaming } from '../DisableToolCallStreaming';
⋮----
type Props = {
  provider: LlmProvider;
  overrides: Record<string, unknown>;
  onChange: (overrides: Record<string, unknown>) => void;
};
⋮----
export const OpenCodeGoModelOverrides = (
⋮----
const handleDisableStreamingChange = (disableStreaming: boolean) =>
⋮----
const handleDisableToolCallStreamingChange = (disableToolCallStreaming: boolean) =>

================
File: src/renderer/src/components/ModelLibrary/providers/OpenCodeGoParameters.tsx
================
import { ChangeEvent } from 'react';
import { useTranslation } from 'react-i18next';
import { OpenCodeGoProvider } from '@common/agent';
⋮----
import { Input } from '@/components/common/Input';
import { useEffectiveEnvironmentVariable } from '@/hooks/useEffectiveEnvironmentVariable';
⋮----
type Props = {
  provider: OpenCodeGoProvider;
  onChange: (updated: OpenCodeGoProvider) => void;
};
⋮----
const handleApiKeyChange = (e: ChangeEvent<HTMLInputElement>) =>
⋮----
label=

================
File: src/renderer/src/components/ModelLibrary/providers/OpenCodeModelOverrides.tsx
================
import { getDefaultProviderParams, LlmProvider } from '@common/agent';
⋮----
import { DisableStreaming } from '../DisableStreaming';
import { DisableToolCallStreaming } from '../DisableToolCallStreaming';
⋮----
type Props = {
  provider: LlmProvider;
  overrides: Record<string, unknown>;
  onChange: (overrides: Record<string, unknown>) => void;
};
⋮----
export const OpenCodeModelOverrides = (
⋮----
const handleDisableStreamingChange = (disableStreaming: boolean) =>
⋮----
const handleDisableToolCallStreamingChange = (disableToolCallStreaming: boolean) =>

================
File: src/renderer/src/components/ModelLibrary/providers/OpenCodeParameters.tsx
================
import { ChangeEvent } from 'react';
import { useTranslation } from 'react-i18next';
import { OpenCodeProvider } from '@common/agent';
⋮----
import { Input } from '@/components/common/Input';
import { useEffectiveEnvironmentVariable } from '@/hooks/useEffectiveEnvironmentVariable';
⋮----
type Props = {
  provider: OpenCodeProvider;
  onChange: (updated: OpenCodeProvider) => void;
};
⋮----
const handleApiKeyChange = (e: ChangeEvent<HTMLInputElement>) =>
⋮----
label=

================
File: src/renderer/src/components/ModelLibrary/providers/OpenRouterAdvancedSettings.tsx
================
import { useTranslation } from 'react-i18next';
import { OpenRouterProvider } from '@common/agent';
⋮----
import { useArrayField } from './useArrayField';
⋮----
import { InfoIcon } from '@/components/common/InfoIcon';
import { Input } from '@/components/common/Input';
import { Select } from '@/components/common/Select';
import { Checkbox } from '@/components/common/Checkbox';
⋮----
type Props = {
  provider: OpenRouterProvider;
  onChange: (updated: OpenRouterProvider) => void;
};
⋮----
const handleAllowFallbacksChange = (checked: boolean) =>
⋮----
const handleDataCollectionChange = (value: string) =>
⋮----
const handleSortChange = (value: string) =>
⋮----
const handleRequireParametersChange = (checked: boolean) =>

================
File: src/renderer/src/components/ModelLibrary/providers/OpenRouterModelOverrides.tsx
================
import { getDefaultProviderParams, LlmProvider, OpenRouterProvider } from '@common/agent';
⋮----
import { DisableStreaming } from '../DisableStreaming';
import { DisableToolCallStreaming } from '../DisableToolCallStreaming';
⋮----
import { OpenRouterAdvancedSettings } from './OpenRouterAdvancedSettings';
⋮----
type Props = {
  provider: LlmProvider;
  overrides: Partial<OpenRouterProvider>;
  onChange: (overrides: Record<string, unknown>) => void;
};
⋮----
export const OpenRouterModelOverrides = (
⋮----
// Convert overrides to OpenRouterProvider format for AdvancedSettings
⋮----
// Convert OpenRouterProvider back to overrides format
const handleProviderChange = (updatedProvider: OpenRouterProvider) =>
⋮----
// Remove undefined values
⋮----
// Handle disable streaming change separately
const handleDisableStreamingChange = (disableStreaming: boolean) =>
⋮----
const handleDisableToolCallStreamingChange = (disableToolCallStreaming: boolean) =>

================
File: src/renderer/src/components/ModelLibrary/providers/OpenRouterParameters.tsx
================
import { ChangeEvent, ReactNode } from 'react';
import { useTranslation } from 'react-i18next';
import { OpenRouterProvider } from '@common/agent';
⋮----
import { OpenRouterAdvancedSettings } from './OpenRouterAdvancedSettings';
⋮----
import { Input } from '@/components/common/Input';
import { useEffectiveEnvironmentVariable } from '@/hooks/useEffectiveEnvironmentVariable';
import { Accordion } from '@/components/common/Accordion';
⋮----
type Props = {
  provider: OpenRouterProvider;
  onChange: (updated: OpenRouterProvider) => void;
};
⋮----
const handleApiKeyChange = (e: ChangeEvent<HTMLInputElement>) =>
⋮----
const renderSectionAccordion = (title: ReactNode, children: ReactNode, open?: boolean, setOpen?: (open: boolean) => void) => (
    <Accordion
      title={<div className="flex-1 text-left text-sm font-medium px-2">{title}</div>}
      chevronPosition="right"
      className="mb-2 border rounded-md border-border-default-dark"
      isOpen={open}
      onOpenChange={setOpen}
    >
      <div className="p-4 pt-2">{children}</div>
    </Accordion>
  );
⋮----
label=
⋮----
openRouterApiKeyEnv

================
File: src/renderer/src/components/ModelLibrary/providers/ProviderModels.tsx
================
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { IoClose } from 'react-icons/io5';
import { HiPlus } from 'react-icons/hi';
import { clsx } from 'clsx';
⋮----
import { Input } from '@/components/common/Input';
import { Button } from '@/components/common/Button';
import { IconButton } from '@/components/common/IconButton';
import { MultiSelect } from '@/components/common/MultiSelect';
⋮----
type Props = {
  models: string[];
  onChange: (updatedModels: string[]) => void;
  placeholder?: string;
  availableModels?: string[];
};
⋮----
const handleModelsChange = (updatedModels: string[]) =>
⋮----
const handleRemoveModel = (index: number) =>
⋮----
const handleAddModels = () =>
⋮----
// Add all selected models at once
⋮----
onChange=

================
File: src/renderer/src/components/ModelLibrary/providers/RequestyAdvancedSettings.tsx
================
import { useTranslation } from 'react-i18next';
import { RequestyProvider } from '@common/agent';
import { ReasoningEffort } from '@common/types';
⋮----
import { Checkbox } from '@/components/common/Checkbox';
import { InfoIcon } from '@/components/common/InfoIcon';
import { Select, Option } from '@/components/common/Select';
⋮----
type Props = {
  provider: RequestyProvider;
  onChange: (updated: RequestyProvider) => void;
};
⋮----
const handleUseAutoCacheChange = (checked: boolean) =>
⋮----
const handleReasoningEffortChange = (value: string) =>
⋮----
<Checkbox label=
⋮----
<span>

================
File: src/renderer/src/components/ModelLibrary/providers/RequestyModelOverrides.tsx
================
import { getDefaultProviderParams, LlmProvider, RequestyProvider } from '@common/agent';
⋮----
import { DisableStreaming } from '../DisableStreaming';
import { DisableToolCallStreaming } from '../DisableToolCallStreaming';
⋮----
import { RequestyAdvancedSettings } from './RequestyAdvancedSettings';
⋮----
type Props = {
  provider: LlmProvider;
  overrides: Partial<RequestyProvider>;
  onChange: (overrides: Record<string, unknown>) => void;
};
⋮----
export const RequestyModelOverrides = (
⋮----
// Convert overrides to RequestyProvider format for AdvancedSettings
⋮----
// Convert RequestyProvider back to overrides format
const handleProviderChange = (updatedProvider: RequestyProvider) =>
⋮----
// Remove undefined values
⋮----
// Handle disable streaming change separately
const handleDisableStreamingChange = (disableStreaming: boolean) =>
⋮----
const handleDisableToolCallStreamingChange = (disableToolCallStreaming: boolean) =>

================
File: src/renderer/src/components/ModelLibrary/providers/RequestyParameters.tsx
================
import { ChangeEvent } from 'react';
import { useTranslation } from 'react-i18next';
import { RequestyProvider } from '@common/agent';
⋮----
import { RequestyAdvancedSettings } from './RequestyAdvancedSettings';
⋮----
import { Input } from '@/components/common/Input';
import { useEffectiveEnvironmentVariable } from '@/hooks/useEffectiveEnvironmentVariable';
⋮----
type Props = {
  provider: RequestyProvider;
  onChange: (updated: RequestyProvider) => void;
};
⋮----
const handleApiKeyChange = (e: ChangeEvent<HTMLInputElement>) =>
⋮----
label=

================
File: src/renderer/src/components/ModelLibrary/providers/SyntheticParameters.tsx
================
import { ChangeEvent } from 'react';
import { useTranslation } from 'react-i18next';
import { SyntheticProvider } from '@common/agent';
⋮----
import { Input } from '@/components/common/Input';
import { useEffectiveEnvironmentVariable } from '@/hooks/useEffectiveEnvironmentVariable';
⋮----
type Props = {
  provider: SyntheticProvider;
  onChange: (updated: SyntheticProvider) => void;
};
⋮----
const handleApiKeyChange = (e: ChangeEvent<HTMLInputElement>) =>
⋮----
label=

================
File: src/renderer/src/components/ModelLibrary/providers/TlsSettings.tsx
================
import { ChangeEvent } from 'react';
import { useTranslation } from 'react-i18next';
import { LlmProviderBase } from '@common/agent';
⋮----
import { Input } from '@/components/common/Input';
import { Checkbox } from '@/components/common/Checkbox';
import { Accordion } from '@/components/common/Accordion';
import { InfoIcon } from '@/components/common/InfoIcon';
⋮----
type TlsCapableProvider = LlmProviderBase & { sslVerify?: boolean; caCertPath?: string };
⋮----
type Props<T extends TlsCapableProvider> = {
  provider: T;
  onChange: (updated: T) => void;
};
⋮----
const handleSslVerifyChange = (checked: boolean) =>
⋮----
const handleCaCertPathChange = (e: ChangeEvent<HTMLInputElement>) =>
⋮----
<span>
⋮----
label=

================
File: src/renderer/src/components/ModelLibrary/providers/useArrayField.ts
================
import { ChangeEvent, useEffect, useState } from 'react';
import { OpenRouterProvider } from '@common/agent';
⋮----
export const useArrayField = (
  provider: OpenRouterProvider,
  field: keyof Pick<OpenRouterProvider, 'order' | 'only' | 'ignore' | 'quantizations'>,
  onChange: (updated: OpenRouterProvider) => void,
) =>
⋮----
// eslint-disable-next-line react-hooks/set-state-in-effect
⋮----
const handleChange = (e: ChangeEvent<HTMLInputElement>) =>
⋮----
const handleBlur = () =>

================
File: src/renderer/src/components/ModelLibrary/providers/VertexAiAdvancedSettings.tsx
================
import { useTranslation } from 'react-i18next';
import { VertexAiProvider } from '@common/agent';
⋮----
import { Slider } from '@/components/common/Slider';
import { Checkbox } from '@/components/common/Checkbox';
import { InfoIcon } from '@/components/common/InfoIcon';
⋮----
type Props = {
  provider: VertexAiProvider;
  onChange: (updated: VertexAiProvider) => void;
};
⋮----
const handleThinkingBudgetChange = (value: number) =>
⋮----
const handleIncludeThoughtsChange = (checked: boolean) =>
⋮----
label=

================
File: src/renderer/src/components/ModelLibrary/providers/VertexAiModelOverrides.tsx
================
import { getDefaultProviderParams, LlmProvider, VertexAiProvider } from '@common/agent';
⋮----
import { DisableStreaming } from '../DisableStreaming';
import { DisableToolCallStreaming } from '../DisableToolCallStreaming';
⋮----
import { VertexAiAdvancedSettings } from './VertexAiAdvancedSettings';
⋮----
type Props = {
  provider: LlmProvider;
  overrides: Partial<VertexAiProvider>;
  onChange: (overrides: Record<string, unknown>) => void;
};
⋮----
export const VertexAiModelOverrides = (
⋮----
// Convert overrides to VertexAiProvider format for AdvancedSettings
⋮----
// Convert VertexAiProvider back to overrides format
const handleProviderChange = (updatedProvider: VertexAiProvider) =>
⋮----
// Remove undefined values
⋮----
// Handle disable streaming change separately
const handleDisableStreamingChange = (disableStreaming: boolean) =>
⋮----
const handleDisableToolCallStreamingChange = (disableToolCallStreaming: boolean) =>

================
File: src/renderer/src/components/ModelLibrary/providers/VertexAIParameters.tsx
================
import { useTranslation } from 'react-i18next';
import { VertexAiProvider } from '@common/agent';
import { ChangeEvent } from 'react';
⋮----
import { VertexAiAdvancedSettings } from './VertexAiAdvancedSettings';
⋮----
import { Input } from '@/components/common/Input';
import { TextArea } from '@/components/common/TextArea';
⋮----
type Props = {
  provider: VertexAiProvider;
  onChange: (updated: VertexAiProvider) => void;
};
⋮----
const handleProjectChange = (e: ChangeEvent<HTMLInputElement>) =>
⋮----
const handleLocationChange = (e: ChangeEvent<HTMLInputElement>) =>
⋮----
const handleCredentialsChange = (e: ChangeEvent<HTMLTextAreaElement>) =>
⋮----
label=

================
File: src/renderer/src/components/ModelLibrary/providers/ZaiPlanAdvancedSettings.tsx
================
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { ZaiPlanProvider } from '@common/agent';
import { McpServerConfig } from '@common/types';
import { FaCheck, FaPlus, FaInfoCircle, FaExternalLinkAlt } from 'react-icons/fa';
⋮----
import { ZaiPlanThinkingSetting } from './ZaiPlanThinkingSetting';
⋮----
import { useMcpServers } from '@/contexts/McpServersContext';
import { Button } from '@/components/common/Button';
import { Tooltip } from '@/components/ui/Tooltip';
⋮----
type Props = {
  provider: ZaiPlanProvider;
  onChange: (updated: ZaiPlanProvider) => void;
};
⋮----
type McpServerInfo = {
  key: string;
  name: string;
  description: string;
  config: McpServerConfig;
  documentationLink: string;
};
⋮----
const isServerConfigured = (serverKey: string): boolean =>
⋮----
const handleAddServer = async (serverInfo: McpServerInfo) =>
⋮----
// eslint-disable-next-line no-console
⋮----
<Button onClick=
⋮----

================
File: src/renderer/src/components/ModelLibrary/providers/ZaiPlanModelOverrides.tsx
================
import { getDefaultProviderParams, LlmProvider, ZaiPlanProvider } from '@common/agent';
⋮----
import { ZaiPlanThinkingSetting } from './ZaiPlanThinkingSetting';
⋮----
type Props = {
  provider: LlmProvider;
  overrides: Partial<ZaiPlanProvider>;
  onChange: (overrides: Record<string, unknown>) => void;
};
⋮----
export const ZaiPlanModelOverrides = (
⋮----
const handleProviderChange = (updatedProvider: ZaiPlanProvider) =>

================
File: src/renderer/src/components/ModelLibrary/providers/ZaiPlanParameters.tsx
================
import { ChangeEvent } from 'react';
import { useTranslation } from 'react-i18next';
import { ZaiPlanProvider } from '@common/agent';
⋮----
import { ZaiPlanAdvancedSettings } from './ZaiPlanAdvancedSettings';
⋮----
import { Input } from '@/components/common/Input';
import { useEffectiveEnvironmentVariable } from '@/hooks/useEffectiveEnvironmentVariable';
⋮----
type Props = {
  provider: ZaiPlanProvider;
  onChange: (updated: ZaiPlanProvider) => void;
};
⋮----
const handleApiKeyChange = (e: ChangeEvent<HTMLInputElement>) =>
⋮----
label=

================
File: src/renderer/src/components/ModelLibrary/providers/ZaiPlanThinkingSetting.tsx
================
import { useTranslation } from 'react-i18next';
import { ZaiPlanProvider } from '@common/agent';
import { ReasoningEffort } from '@common/types';
⋮----
import { Checkbox } from '@/components/common/Checkbox';
import { InfoIcon } from '@/components/common/InfoIcon';
import { Select, Option } from '@/components/common/Select';
⋮----
type Props = {
  provider: ZaiPlanProvider;
  onChange: (updated: ZaiPlanProvider) => void;
};
⋮----
const handleThinkingEnabledChange = (checked: boolean) =>
⋮----
const handleReasoningEffortChange = (value: string) =>
⋮----
<span>

================
File: src/renderer/src/components/ModelLibrary/DisableStreaming.tsx
================
import { useTranslation } from 'react-i18next';
⋮----
import { Checkbox } from '@/components/common/Checkbox';
import { InfoIcon } from '@/components/common/InfoIcon';
⋮----
type Props = {
  checked: boolean;
  onChange: (checked: boolean) => void;
};
⋮----
<Checkbox label=

================
File: src/renderer/src/components/ModelLibrary/DisableToolCallStreaming.tsx
================
import { useTranslation } from 'react-i18next';
⋮----
import { Checkbox } from '@/components/common/Checkbox';
import { InfoIcon } from '@/components/common/InfoIcon';
⋮----
type Props = {
  checked: boolean;
  onChange: (checked: boolean) => void;
};
⋮----
<Checkbox label=

================
File: src/renderer/src/components/ModelLibrary/index.ts
================


================
File: src/renderer/src/components/ModelLibrary/ModelDialog.tsx
================
import { startTransition, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Model, ProviderProfile } from '@common/types';
⋮----
import { ModelParameterOverrides } from './ModelParameterOverrides';
⋮----
import { Input } from '@/components/common/Input';
import { Select } from '@/components/common/Select';
import { Checkbox } from '@/components/common/Checkbox';
import { ConfirmDialog } from '@/components/common/ConfirmDialog';
import { Slider } from '@/components/common/Slider';
import { InfoIcon } from '@/components/common/InfoIcon';
⋮----
type Props = {
  model?: Model;
  providers: ProviderProfile[];
  onSave: (model: Model) => void;
  onCancel: () => void;
};
⋮----
// Batch state updates to avoid cascading renders
⋮----
// Batch state updates to avoid cascading renders
⋮----
const validateForm = (): boolean =>
⋮----
const handleSubmit = () =>
⋮----
const handleInputChange = (field: keyof Model, value: unknown) =>
⋮----
// Clear error when user starts typing
⋮----
const handleCostBlur = (field: keyof Model, displayField: keyof typeof costDisplayValues, inputValue: string) =>
⋮----
const handleTemperatureToggle = (enabled: boolean) =>
⋮----
label=
⋮----
onChange=
⋮----
disabled={!!model} // Don't allow changing provider for existing models
⋮----
disabled={!!model} // Don't allow changing ID for existing models
⋮----
onBlur=
⋮----
<span>
⋮----
<Slider min=
⋮----
{/* Advanced Settings - Provider Overrides */}
⋮----
<Checkbox label=

================
File: src/renderer/src/components/ModelLibrary/ModelLibrary.tsx
================
import { useState, useEffect, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { useHotkeys } from 'react-hotkeys-hook';
import { Model, ProviderProfile } from '@common/types';
import { LlmProviderName } from '@common/agent';
⋮----
import { ModelDialog } from './ModelDialog';
import { ProviderSelection } from './ProviderSelection';
import { ProviderProfileForm } from './ProviderProfileForm';
import { ProviderProfileDialog } from './ProviderProfileDialog';
import { ProviderHeader } from './ProviderHeader';
import { ModelTableSection } from './ModelTableSection';
⋮----
import { ModalOverlayLayout } from '@/components/common/ModalOverlayLayout';
import { useModelProviders } from '@/contexts/ModelProviderContext';
⋮----
type Props = {
  onClose: () => void;
};
⋮----
export const ModelLibrary = (
⋮----
const handleCancelConfigure = () =>
⋮----
const handleClose = () =>
⋮----
const handleToggleProviderSelect = (profileId: string) =>
⋮----
const handleAddProvider = () =>
⋮----
const handleSelectProvider = (provider: LlmProviderName) =>
⋮----
const handleEditProfile = (profile: ProviderProfile) =>
⋮----
const handleDeleteProfile = async (profile: ProviderProfile) =>
⋮----
const handleSaveProfile = async (profile: ProviderProfile) =>
⋮----
const handleAddModel = () =>
⋮----
const handleEditModel = (model: Model) =>
⋮----
const handleDeleteModel = async (model: Model) =>
⋮----
const handleSaveModel = async (model: Model) =>
⋮----
const handleToggleHidden = async (model: Model) =>
⋮----
const handleBulkToggleHidden = async (modelIds: string[], isHidden: boolean) =>
⋮----
// Show provider selection when adding new provider
⋮----
<ModalOverlayLayout title=
⋮----
// Show provider configuration form
⋮----
onCancel=

================
File: src/renderer/src/components/ModelLibrary/ModelParameterOverrides.tsx
================
import { useTranslation } from 'react-i18next';
import { LlmProvider, LlmProviderName } from '@common/agent';
import { ComponentType } from 'react';
⋮----
import { OpenRouterModelOverrides } from './providers/OpenRouterModelOverrides';
import { GeminiModelOverrides } from './providers/GeminiModelOverrides';
import { VertexAiModelOverrides } from './providers/VertexAiModelOverrides';
import { OpenAiModelOverrides } from './providers/OpenAiModelOverrides';
import { OpenCodeModelOverrides } from './providers/OpenCodeModelOverrides';
import { OpenCodeGoModelOverrides } from './providers/OpenCodeGoModelOverrides';
import { RequestyModelOverrides } from './providers/RequestyModelOverrides';
import { AzureModelOverrides } from './providers/AzureModelOverrides';
import { OpenAiCompatibleModelOverrides } from './providers/OpenAiCompatibleModelOverrides';
import { DefaultModelOverrides } from './providers/DefaultModelOverrides';
import { ZaiPlanModelOverrides } from './providers/ZaiPlanModelOverrides';
import { AlibabaPlanModelOverrides } from './providers/AlibabaPlanModelOverrides';
import { DeepseekModelOverrides } from './providers/DeepseekModelOverrides';
import { NeuralwattModelOverrides } from './providers/NeuralwattModelOverrides';
⋮----
import { Button } from '@/components/common/Button';
import { Accordion } from '@/components/common/Accordion';
⋮----
type ProviderOverridesProps = {
  provider: LlmProvider;
  overrides: Record<string, unknown>;
  onChange: (overrides: Record<string, unknown>) => void;
};
⋮----
// Providers without specific overrides use DefaultModelOverrides
⋮----
type Props = {
  provider: LlmProvider;
  overrides: Record<string, unknown>;
  onChange: (overrides: Record<string, unknown>) => void;
  className?: string;
};
⋮----
const handleProviderOverrideChange = (providerOverrides: Record<string, unknown>) =>
⋮----
const handleResetToDefaults = () =>
⋮----
<div className="text-sm font-medium text-text-primary">

================
File: src/renderer/src/components/ModelLibrary/ModelTableSection.tsx
================
import { useState, ReactNode, useRef, useEffect, RefObject, useCallback, useMemo } from 'react';
import { useDebounce } from '@reactuses/core';
import { useTranslation } from 'react-i18next';
import { FiEdit2, FiTrash2, FiPlus, FiEye, FiSliders, FiRefreshCw, FiChevronDown } from 'react-icons/fi';
import { Model, ProviderProfile } from '@common/types';
import { MdThermostat } from 'react-icons/md';
⋮----
import { Input } from '@/components/common/Input';
import { Button } from '@/components/common/Button';
import { Checkbox } from '@/components/common/Checkbox';
import { Column } from '@/components/common/Table';
import { VirtualTable } from '@/components/common/VirtualTable';
import { IconButton } from '@/components/common/IconButton';
import { Tooltip } from '@/components/ui/Tooltip';
import { type MenuOption } from '@/components/common/VerticalDotsMenu';
⋮----
type Props = {
  models: Model[];
  selectedProviderIds: string[];
  providers: ProviderProfile[];
  onAddModel: () => void;
  onEditModel: (model: Model) => void;
  onDeleteModel: (model: Model) => void;
  onToggleHidden: (model: Model) => void;
  onBulkToggleHidden: (modelIds: string[], isHidden: boolean) => void;
  onRefreshModels: () => void;
  modelsLoading?: boolean;
};
⋮----
const useClickOutside = (refs: React.RefObject<HTMLElement>[], callback: () => void) =>
⋮----
const handleClickOutside = (event: MouseEvent) =>
⋮----
const handleClearSelection = () =>
⋮----
const handleBulkHide = () =>
⋮----
const handleBulkUnhide = () =>
⋮----
const provider = providers.find((p)
⋮----
onClick=
⋮----
{/* Search and Info Header */}
⋮----
<Checkbox label=
⋮----
option.action();
setIsBulkActionsOpen(false);
⋮----
{/* Table */}

================
File: src/renderer/src/components/ModelLibrary/ProviderCard.tsx
================
import { ReactNode } from 'react';
⋮----
type Props = {
  name: string;
  icon: ReactNode;
  onClick: () => void;
};
⋮----
export const ProviderCard = (

================
File: src/renderer/src/components/ModelLibrary/ProviderHeader.tsx
================
import { useTranslation } from 'react-i18next';
import { FiPlus } from 'react-icons/fi';
import { ProviderProfile } from '@common/types';
⋮----
import { ProviderProfileCard } from './ProviderProfileCard';
⋮----
import { Button } from '@/components/common/Button';
⋮----
type Props = {
  providers: ProviderProfile[];
  providerErrors: Record<string, string>;
  selectedProfileIds: string[];
  onToggleSelect: (profileId: string) => void;
  onAddProvider: () => void;
  onEditProfile: (profile: ProviderProfile) => void;
  onDeleteProfile: (profile: ProviderProfile) => void;
};
⋮----
onToggleSelect=
⋮----
onEdit=
onDelete=

================
File: src/renderer/src/components/ModelLibrary/ProviderProfileCard.tsx
================
import { useState, ComponentType } from 'react';
import { useTranslation } from 'react-i18next';
import { FiEdit2, FiTrash2, FiMic, FiCpu } from 'react-icons/fi';
import { clsx } from 'clsx';
import { ProviderProfile } from '@common/types';
import { LlmProviderName } from '@common/agent';
import { FaExclamationCircle } from 'react-icons/fa';
⋮----
import { IconButton } from '@/components/common/IconButton';
import { ConfirmDialog } from '@/components/common/ConfirmDialog';
import {
  AnthropicIcon,
  AnthropicCompatibleIcon,
  AzureIcon,
  BedrockIcon,
  CerebrasIcon,
  ClinePassIcon,
  DeepseekIcon,
  GeminiIcon,
  GpustackIcon,
  GroqIcon,
  LitellmIcon,
  LlmApiIcon,
  LmStudioIcon,
  MinimaxIcon,
  MistralIcon,
  NeuralwattIcon,
  OllamaIcon,
  OpenAiCompatibleIcon,
  OpenAiIcon,
  OpenCodeIcon,
  OpenCodeGoIcon,
  OpenRouterIcon,
  RequestyIcon,
  SyntheticIcon,
  VertexAiIcon,
  ZaiPlanIcon,
} from '@/icons';
import { useResponsive } from '@/hooks/useResponsive';
import { Tooltip } from '@/components/ui/Tooltip';
import { AlibabaPlanIcon } from '@/icons/AlibabaPlanIcon';
import { KimiPlanIcon } from '@/icons/KimiPlanIcon';
⋮----
type Props = {
  provider: ProviderProfile;
  error?: string;
  isSelected: boolean;
  onToggleSelect: () => void;
  onEdit: () => void;
  onDelete: () => void;
};
⋮----
const handleCardClick = () =>
⋮----
// If already selected and no actions showing, show actions (mobile behavior)
⋮----
// Otherwise, toggle selection (mobile behavior)
⋮----
const handleEdit = () =>
⋮----
const handleDelete = () =>
⋮----
className=
⋮----
{/* Action buttons - shown on hover (desktop) or when showActions is true (mobile) */}
⋮----
title=
⋮----
onDelete();
setShowDeleteConfirm(false);
⋮----
onCancel=

================
File: src/renderer/src/components/ModelLibrary/ProviderProfileDialog.tsx
================
import { useTranslation } from 'react-i18next';
import { useRef, useState } from 'react';
import { LlmProviderName } from '@common/agent';
import { ProviderProfile } from '@common/types';
⋮----
import { ProviderProfileForm, ProviderProfileFormRef } from './ProviderProfileForm';
⋮----
import { ConfirmDialog } from '@/components/common/ConfirmDialog';
import { Button } from '@/components/common/Button';
⋮----
type Props = {
  provider: LlmProviderName;
  profile?: ProviderProfile;
  providers: ProviderProfile[];
  onSave: (profile: ProviderProfile) => void;
  onCancel: () => void;
};
⋮----
export const ProviderProfileDialog = (
⋮----
const handleSave = (savedProfile: ProviderProfile) =>
⋮----
const handleToggleDisabled = () =>
⋮----
title=
⋮----
confirmButtonText=

================
File: src/renderer/src/components/ModelLibrary/ProviderProfileForm.tsx
================
import { getDefaultProviderParams, LlmProvider, LlmProviderName } from '@common/agent';
import { ProviderProfile } from '@common/types';
import { useTranslation } from 'react-i18next';
import { ComponentType, forwardRef, useImperativeHandle, useState } from 'react';
import { v4 as uuidv4 } from 'uuid';
import { FaPlus, FaTrash } from 'react-icons/fa';
⋮----
import { DisableStreaming } from './DisableStreaming';
import { DisableToolCallStreaming } from './DisableToolCallStreaming';
⋮----
import {
  AnthropicParameters,
  AnthropicCompatibleParameters,
  AzureParameters,
  BedrockParameters,
  ClinePassParameters,
  DeepseekParameters,
  GeminiParameters,
  GpustackParameters,
  GroqParameters,
  AlibabaPlanParameters,
  KimiPlanParameters,
  LitellmParameters,
  LlmApiParameters,
  LmStudioParameters,
  MinimaxParameters,
  MistralParameters,
  NeuralwattParameters,
  OllamaParameters,
  OpenAiCompatibleParameters,
  OpenAiParameters,
  OpenCodeParameters,
  OpenCodeGoParameters,
  OpenRouterParameters,
  RequestyParameters,
  SyntheticParameters,
  VertexAIParameters,
  ZaiPlanParameters,
} from '@/components/ModelLibrary/providers';
import { Input } from '@/components/common/Input';
import { Button } from '@/components/common/Button';
import { IconButton } from '@/components/common/IconButton';
import { Accordion } from '@/components/common/Accordion';
⋮----
type ProviderParametersProps<T extends LlmProvider> = {
  provider: T;
  onChange: (updated: T) => void;
};
⋮----
// @ts-expect-error using LlmProvider as type
⋮----
type Header = { id: string; key: string; value: string };
⋮----
type Props = {
  provider: LlmProviderName;
  editProfile?: ProviderProfile;
  providers: ProviderProfile[];
  onSave: (profile: ProviderProfile) => void;
  onCancel: () => void;
  hideActions?: boolean;
  hideTitle?: boolean;
};
⋮----
export type ProviderProfileFormRef = {
  submit: () => void;
};
⋮----
const handleIdChange = (value: string) =>
⋮----
// Clear error when user starts typing
⋮----
const handleHeaderChange = (id: string, field: 'key' | 'value', value: string) =>
⋮----
const addHeader = () =>
⋮----
const removeHeader = (id: string) =>
⋮----
const handleDisableStreamingChange = (disableStreaming: boolean) =>
⋮----
const handleDisableToolCallStreamingChange = (disableToolCallStreaming: boolean) =>
⋮----
const validateForm = (): boolean =>
⋮----
// Check for duplicate IDs
⋮----
const handleSave = () =>
⋮----
placeholder=
⋮----
onChange=
⋮----
{/* Disable streaming checkbox */}

================
File: src/renderer/src/components/ModelLibrary/ProviderSelection.tsx
================
import { LlmProviderName, AVAILABLE_PROVIDERS } from '@common/agent';
import { useTranslation } from 'react-i18next';
import { ComponentType } from 'react';
import { FiArrowLeft } from 'react-icons/fi';
⋮----
import { ProviderCard } from './ProviderCard';
⋮----
import { IconButton } from '@/components/common/IconButton';
import {
  AlibabaPlanIcon,
  AnthropicCompatibleIcon,
  AnthropicIcon,
  AzureIcon,
  BedrockIcon,
  CerebrasIcon,
  ClinePassIcon,
  DeepseekIcon,
  GeminiIcon,
  GpustackIcon,
  GroqIcon,
  KimiPlanIcon,
  LitellmIcon,
  LlmApiIcon,
  LmStudioIcon,
  MinimaxIcon,
  MistralIcon,
  NeuralwattIcon,
  OllamaIcon,
  OpenAiCompatibleIcon,
  OpenAiIcon,
  OpenCodeIcon,
  OpenCodeGoIcon,
  OpenRouterIcon,
  RequestyIcon,
  SyntheticIcon,
  VertexAiIcon,
  ZaiPlanIcon,
} from '@/icons';
⋮----
type Props = {
  onSelectProvider: (provider: LlmProviderName) => void;
  onCancel?: () => void;
  showTitle?: boolean;
};
⋮----
onClick=

================
File: src/renderer/src/components/onboarding/OnboardingPathChoice.tsx
================
import { HiOutlineCpuChip, HiOutlineCommandLine } from 'react-icons/hi2';
import { ReactNode } from 'react';
import { useTranslation } from 'react-i18next';
import { clsx } from 'clsx';
⋮----
export type OnboardingPath = 'agent' | 'aider';
⋮----
type PathOption = {
  path: OnboardingPath;
  icon: ReactNode;
  title: string;
  subtitle: string;
  description: string;
  features: readonly string[];
  modeLabel: string;
};
⋮----
type Props = {
  selectedPath: OnboardingPath | null;
  onSelectPath: (path: OnboardingPath) => void;
};
⋮----
<span className=
⋮----
className=

================
File: src/renderer/src/components/onboarding/OnboardingProviderSetup.tsx
================
import { useState } from 'react';
import { ProviderProfile } from '@common/types';
import { LlmProviderName } from '@common/agent';
⋮----
import { ProviderSelection } from '@/components/ModelLibrary/ProviderSelection';
import { ProviderProfileForm } from '@/components/ModelLibrary/ProviderProfileForm';
import { useModelProviders } from '@/contexts/ModelProviderContext';
import { ProviderHeader } from '@/components/ModelLibrary/ProviderHeader';
⋮----
export const OnboardingProviderSetup = () =>
⋮----
const handleAddProvider = () =>
⋮----
const handleSelectProvider = (provider: LlmProviderName) =>
⋮----
const handleEditProfile = (profile: ProviderProfile) =>
⋮----
const handleDeleteProfile = async (profile: ProviderProfile) =>
⋮----
const handleCancelConfigure = () =>
⋮----
const handleSaveProfile = async (profile: ProviderProfile) =>
⋮----
// Show provider configuration form
⋮----
// Show provider selection when adding new provider

================
File: src/renderer/src/components/onboarding/OnboardingStepper.tsx
================
import { HiCheck } from 'react-icons/hi';
import { clsx } from 'clsx';
⋮----
type Step = {
  title: string;
};
⋮----
type Props = {
  steps: Step[];
  currentStep: number;
};
⋮----
{/* Step Circle */}
⋮----
className=
⋮----
{/* Step Title */}
⋮----
{/* Connector Line */}

================
File: src/renderer/src/components/project/TaskSidebar/TaskItem.tsx
================
import { TaskData, DefaultTaskState } from '@common/types';
import { useTranslation } from 'react-i18next';
import { MouseEvent, DragEvent, memo, useRef, useState } from 'react';
import { HiPlus, HiCheck, HiSparkles } from 'react-icons/hi';
import { IoGitBranch } from 'react-icons/io5';
import { MdPushPin, MdChevronRight } from 'react-icons/md';
import { RiFolderAddLine } from 'react-icons/ri';
import { clsx } from 'clsx';
import { useLongPress } from '@reactuses/core';
import { BiArchiveIn } from 'react-icons/bi';
⋮----
import { TaskStatusIcon } from './TaskStatusIcon';
import { TaskMenuButton } from './TaskMenuButton';
⋮----
import { InlineEditPanel } from '@/components/common/InlineEditPanel';
import { Button } from '@/components/common/Button';
import { LoadingText } from '@/components/common/LoadingText';
import { TaskStateChip } from '@/components/common/TaskStateChip';
import { ExtensionComponentWrapper } from '@/components/extensions/ExtensionComponentWrapper';
import { Tooltip } from '@/components/ui/Tooltip';
import { countAllSubtasks } from '@/utils/task-utils';
⋮----
type Props = {
  task: TaskData;
  tasks: TaskData[];
  readonly?: boolean;
  level: number;
  selectedTasks: Set<string>;
  deleteConfirmTaskId: string | null;
  isMultiselectMode: boolean;
  setIsMultiselectMode: (value: boolean) => void;
  activeTaskId: string | null;
  onTaskClick: (e: MouseEvent, taskId: string) => void;
  createNewTask?: (parentId?: string) => void;
  editingTaskId: string | null;
  onEditClick: (taskId: string) => void;
  onEditConfirm: (taskId: string, newName: string) => Promise<void>;
  onEditCancel: () => void;
  onDeleteClick: (taskId: string) => void;
  onArchiveTask: (taskId: string) => Promise<void>;
  onUnarchiveTask: (taskId: string) => Promise<void>;
  onTogglePin: (taskId: string) => Promise<void>;
  onChangeState: (taskId: string, newState: string) => Promise<void>;
  onMoveToTop?: (taskId: string) => Promise<void>;
  onCopyAsMarkdown?: (taskId: string) => void;
  onExportToMarkdown?: (taskId: string) => void;
  onExportToImage?: (taskId: string) => void;
  onDuplicateTask?: (taskId: string) => void;
  handleConfirmDelete: (taskId: string) => Promise<void>;
  handleCancelDelete: () => void;
  isExpanded: boolean;
  onToggleExpand: (taskId: string) => void;
  hasChildren: boolean;
  showSpinner: boolean;
  draggedTaskIds: Set<string>;
  dragOverTaskId: string | null;
  onDragStart: (taskId: string) => void;
  onDragEnd: () => void;
  onDropOnTask: (targetId: string) => void;
  setDragOverTaskId: (taskId: string | null) => void;
};
⋮----
const arePropsEqual = (prevProps: Props, nextProps: Props): boolean =>
⋮----
// Compare task object - only properties used in rendering
⋮----
// Compare tasks array reference (used for subtasks computation)
⋮----
// Compare primitive props
⋮----
// Compare task-id-specific props - only re-render if our task is affected
⋮----
// Compare selectedTasks - only care about our task's membership
⋮----
// Compare function props by reference
⋮----
const toggleExpand = (e: MouseEvent) =>
⋮----
const handleCreateSubtask = (e: MouseEvent) =>
⋮----
const handleOnEdit = () =>
⋮----
const handleDragStart = (e: DragEvent<HTMLDivElement>) =>
⋮----
const handleDragEnd = () =>
⋮----
const handleDragOver = (e: DragEvent<HTMLDivElement>) =>
⋮----
const handleDragLeave = (e: DragEvent<HTMLDivElement>) =>
⋮----
const handleDrop = (e: DragEvent<HTMLDivElement>) =>
⋮----
className=
⋮----
label=
⋮----
onDelete=
⋮----
onChangeState=

================
File: src/renderer/src/components/project/TaskSidebar/TaskMenuButton.tsx
================
import { DefaultTaskState, TaskData } from '@common/types';
import { useTranslation } from 'react-i18next';
import { MouseEvent, useRef, useState, useEffect, memo } from 'react';
import { HiOutlinePencil, HiOutlineTrash } from 'react-icons/hi';
import { FaEllipsisVertical } from 'react-icons/fa6';
import { IoLogoMarkdown } from 'react-icons/io';
import { RiFlag2Line } from 'react-icons/ri';
import { MdImage, MdPushPin, MdChevronRight, MdVerticalAlignTop } from 'react-icons/md';
import { BiDuplicate, BiArchive, BiArchiveIn, BiCopy } from 'react-icons/bi';
import { createPortal } from 'react-dom';
import { clsx } from 'clsx';
⋮----
import { useClickOutside } from '@/hooks/useClickOutside';
import { getTaskStateLabel } from '@/components/common/TaskStateChip';
⋮----
type Props = {
  task: TaskData;
  onEdit: () => void;
  onDelete?: () => void;
  onCopyAsMarkdown?: () => void;
  onExportToMarkdown?: () => void;
  onExportToImage?: () => void;
  onDuplicateTask?: () => void;
  onArchiveTask?: () => void;
  onUnarchiveTask?: () => void;
  onTogglePin?: () => void;
  onChangeState?: (newState: string) => void;
  onMoveToTop?: () => void;
  isPinned?: boolean;
};
⋮----
const handleMenuClick = () =>
⋮----
const handleEditClick = (e: MouseEvent) =>
⋮----
const handleDeleteClick = (e: MouseEvent) =>
⋮----
const handleCopyAsMarkdownClick = (e: MouseEvent) =>
⋮----
const handleExportToMarkdownClick = (e: MouseEvent) =>
⋮----
const handleExportToImageClick = (e: MouseEvent) =>
⋮----
const handleDuplicateTaskClick = (e: MouseEvent) =>
⋮----
const handleArchiveTaskClick = (e: MouseEvent) =>
⋮----
const handleUnarchiveTaskClick = (e: MouseEvent) =>
⋮----
const handlePinClick = (e: MouseEvent) =>
⋮----
const handleMoveToTopClick = (e: MouseEvent) =>
⋮----
const handleStateSubmenuToggle = () =>
⋮----
const handleStateChange = (e: MouseEvent, state: string) =>
⋮----
<div className=
⋮----
className={clsx(
              'transition-opacity p-1.5 rounded-md hover:bg-bg-tertiary text-text-muted hover:text-text-primary',
              !isMenuOpen && 'opacity-0 group-hover:opacity-100',
            )}
onClick=
⋮----
e.stopPropagation();
handleStateSubmenuToggle();

================
File: src/renderer/src/components/project/TaskSidebar/TaskSectionHeader.tsx
================
import { clsx } from 'clsx';
⋮----
type Props = {
  title: string;
  className?: string;
};
⋮----
export const TaskSectionHeader = (
⋮----
return <div className=

================
File: src/renderer/src/components/project/TaskSidebar/TaskSidebar.tsx
================
import { DefaultTaskState, TaskData } from '@common/types';
import { useTranslation } from 'react-i18next';
import {
  MouseEvent,
  DragEvent,
  useState,
  useRef,
  useEffect,
  useOptimistic,
  startTransition,
  Activity,
  memo,
  useCallback,
  useMemo,
  forwardRef,
  useImperativeHandle,
  ForwardedRef,
} from 'react';
import { HiPlus } from 'react-icons/hi';
import { RiMenuUnfold4Line, RiMenuFold2Line } from 'react-icons/ri';
import { CgSpinner } from 'react-icons/cg';
import { MdOutlineSearch } from 'react-icons/md';
import { clsx } from 'clsx';
import { AnimatePresence, motion } from 'framer-motion';
import { FiFilter } from 'react-icons/fi';
import { HiXMark } from 'react-icons/hi2';
import { useDebounce, useLocalStorage } from '@reactuses/core';
import { useVirtualizer } from '@tanstack/react-virtual';
⋮----
import { TaskSidebarMultiSelectMenu } from './TaskSidebarMultiSelectMenu';
import { TaskItem } from './TaskItem';
import { TaskSectionHeader } from './TaskSectionHeader';
⋮----
import { ExtensionComponentWrapper } from '@/components/extensions/ExtensionComponentWrapper';
import { flattenTasksForVirtualization } from '@/utils/task-utils';
import { Input } from '@/components/common/Input';
import { useClickOutside } from '@/hooks/useClickOutside';
import { ConfirmDialog } from '@/components/common/ConfirmDialog';
import { Tooltip } from '@/components/ui/Tooltip';
import { Checkbox } from '@/components/common/Checkbox';
import { Toggle } from '@/components/common/Toggle';
import { getTaskStateLabel, getStateTextClass } from '@/components/common/TaskStateChip';
import { useTaskSidebarStore } from '@/stores/taskSidebarStore';
⋮----
type Props = {
  loading: boolean;
  tasks: TaskData[];
  readonly?: boolean;
  activeTaskId: string | null;
  onTaskSelect: (taskId: string) => void;
  createNewTask?: (parentId?: string) => void;
  className?: string;
  isCollapsed: boolean;
  onToggleCollapse: () => void;
  updateTask?: (taskId: string, updates: Partial<TaskData>) => Promise<void>;
  deleteTask?: (taskId: string) => Promise<void>;
  onCopyAsMarkdown?: (taskId: string) => void;
  onExportToMarkdown?: (taskId: string) => void;
  onExportToImage?: (taskId: string) => void;
  onDuplicateTask?: (taskId: string) => void;
  isMobile?: boolean;
  onClose?: () => void;
  width?: number;
  onResize?: (width: number) => void;
  contentRef?: React.RefObject<HTMLDivElement | null>;
};
⋮----
export type TaskSidebarRef = {
  startRenaming: (taskId: string) => void;
};
⋮----
// eslint-disable-next-line react-compiler/react-compiler
⋮----
const handleMouseMove = (moveEvent: globalThis.MouseEvent) =>
⋮----
const handleMouseUp = () =>
⋮----
// Multiselect state
⋮----
const handleMultiselectClose = () =>
⋮----
// Handle ESC key to exit multiselect mode
⋮----
const handleKeyDown = (e: globalThis.KeyboardEvent) =>
⋮----
// Handle click outside for multiselect menu
⋮----
// propagate through the whole chain of collapsed ancestors so that
// the closest visible ancestor of the in-progress subtask shows the spinner
⋮----
// eslint-disable-next-line react-hooks/incompatible-library
⋮----
// the active task may be activated before the task list update arrives, retry on the next tasks update
⋮----
// eslint-disable-next-line no-console
⋮----
// eslint-disable-next-line no-console
⋮----
const handleCreateTask = () =>
⋮----
const handleSearchToggle = () =>
⋮----
const handleSearchClose = () =>
⋮----
// eslint-disable-next-line no-console
⋮----
// eslint-disable-next-line no-console
⋮----
// eslint-disable-next-line no-console
⋮----
// eslint-disable-next-line no-console
⋮----
// eslint-disable-next-line no-console
⋮----
const clearDragState = () =>
⋮----
// eslint-disable-next-line no-console
⋮----
// eslint-disable-next-line no-console
⋮----
// Multiselect handlers
⋮----
const handleBulkDelete = async () =>
⋮----
// eslint-disable-next-line no-console
⋮----
const handleBulkArchive = async () =>
⋮----
// eslint-disable-next-line no-console
⋮----
<RiMenuUnfold4Line className=
⋮----
<h3 className="text-sm font-semibold uppercase h-5">
⋮----
onArchive=
onUnarchive=
⋮----
checked=
⋮----
setSelectedStates((prev) =>
⋮----
{/* Bulk Delete Confirmation */}
⋮----
title=
⋮----
{/* Bulk Archive Confirmation */}
⋮----
// Custom comparison function for React.memo
⋮----
// Compare primitive props
⋮----
// Compare function props
⋮----
// Compare tasks array - shallow check first, then deep check for task properties
⋮----
// Check if tasks have changed in meaningful ways
⋮----
// Only check properties that affect rendering

================
File: src/renderer/src/components/project/TaskSidebar/TaskSidebarMultiSelectMenu.tsx
================
import { useTranslation } from 'react-i18next';
import { HiOutlineTrash } from 'react-icons/hi';
import { FaEllipsisVertical } from 'react-icons/fa6';
import { BiArchive, BiArchiveIn } from 'react-icons/bi';
⋮----
type Props = {
  hasArchived: boolean;
  onDelete: () => void;
  onArchive: () => void;
  onUnarchive: () => void;
  isOpen: boolean;
  onToggle: () => void;
  menuRef: React.RefObject<HTMLDivElement | null>;
  buttonRef: React.RefObject<HTMLDivElement | null>;
};
⋮----
e.stopPropagation();
onDelete();
⋮----
onArchive();
⋮----
onUnarchive();

================
File: src/renderer/src/components/project/TaskSidebar/TaskStatusIcon.tsx
================
import { DefaultTaskState } from '@common/types';
import { CgSpinner } from 'react-icons/cg';
import { clsx } from 'clsx';
⋮----
import { useTaskQuestion } from '@/stores/taskStore';
⋮----
type Props = {
  taskId: string;
  state?: string;
  isCollapsed?: boolean;
  showSpinner?: boolean;
};
⋮----
export const TaskStatusIcon = (
⋮----
return <span className=

================
File: src/renderer/src/components/project/AddFileDialog.tsx
================
import { useCallback, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
⋮----
import { FileFinder } from '@/components/project/FileFinder';
import { FileChip } from '@/components/common/FileChip';
import { ConfirmDialog } from '@/components/common/ConfirmDialog';
import { Checkbox } from '@/components/common/Checkbox';
import { useApi } from '@/contexts/ApiContext';
⋮----
type Props = {
  baseDir: string;
  taskId: string;
  onClose: () => void;
  onAddFiles: (filePaths: string[], readOnly?: boolean) => void;
  initialReadOnly?: boolean;
};
⋮----
const handleDragEnter = (event: DragEvent) =>
⋮----
const handleDragOver = (event: DragEvent) =>
⋮----
const handleDrop = async (event: DragEvent) =>
⋮----
const handleOnPaste = async (pastedText: string): Promise<boolean> =>
⋮----
const handleRemovePath = (pathToRemove: string) =>
⋮----
const handleAddFiles = () =>
⋮----
// onClose will be called by ConfirmDialog's onConfirm
⋮----
title=
⋮----
className="w-full" // Changed from flex-1 as it's the only main element now
⋮----
<Checkbox label=

================
File: src/renderer/src/components/project/FileFinder.tsx
================
import { matchSorter } from 'match-sorter';
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { FaFile, FaFolder } from 'react-icons/fa';
import { PiKeyReturn } from 'react-icons/pi';
⋮----
import { AutocompletionInput } from '@/components/AutocompletionInput';
import { IconButton } from '@/components/common/IconButton';
import { useApi } from '@/contexts/ApiContext';
⋮----
type Props = {
  baseDir?: string;
  taskId?: string;
  isReadOnly: boolean;
  selectedPaths: string[];
  onPathAdded: (path: string) => void;
  onSubmit?: () => void;
  onPaste?: (pastedText: string) => Promise<boolean>;
  autoFocus?: boolean;
  className?: string;
  allowFiles?: boolean;
  allowDirectories?: boolean;
};
⋮----
// eslint-disable-next-line react-hooks/set-state-in-effect
⋮----
const checkValidPath = async () =>
⋮----
const updateSuggestions = async () =>
⋮----
const getParentDirectories = () =>
⋮----
continue; // No parent directories if it's a root file or empty/invalid path
⋮----
// Construct the parent path by joining segments from start up to current segment 'i'
⋮----
// Add to set if it's not an empty string (e.g. from a path like "/file.txt" where first segment is empty)
⋮----
// Deduplicate in case a path could be sourced from both lists or if suggestionFiles had duplicates
⋮----
const handleInputSubmit = (isMultiSelect = false) =>
⋮----
const handleAutocompleteInputChange = (value: string, isFromSuggestion?: boolean, isMultiSelect?: boolean) =>
⋮----
setInputValue(''); // Clear input after adding from suggestion
setShowSuggestions(false); // Hide suggestions after selection
⋮----
const getPlaceholderKey = (isForSelectedPaths: boolean) =>
⋮----
// Fallback, though ideally one of them should always be true
⋮----
const handleBrowse = async (browseType: 'file' | 'directory') =>
⋮----
// eslint-disable-next-line no-console
⋮----
onClick=
⋮----
tooltip=

================
File: src/renderer/src/components/project/FilesContextInfoContent.tsx
================
import { ContextFile, Mode, TaskData } from '@common/types';
⋮----
import { Workspace } from '@/components/Workspace';
import { CostInfo } from '@/components/CostInfo';
import { useTaskFileTokensInfo, useTaskTokensInfo } from '@/stores/taskStore';
⋮----
type Props = {
  baseDir: string;
  taskId: string;
  allFiles: string[];
  contextFiles: ContextFile[];
  aiderTotalCost: number;
  maxInputTokens: number;
  clearMessages: (clearContext?: boolean) => void;
  runCommand: (command: string) => void;
  resetTask: () => void;
  mode: Mode;
  showFileDialog: () => void;
  task: TaskData;
  updateTask: (taskId: string, updates: Partial<TaskData>) => void;
  refreshAllFiles: (useGit?: boolean) => Promise<void>;
  refreshContextFiles: () => Promise<void>;
  onToggleFilesSidebarCollapse?: () => void;
};
⋮----
export const FilesContextInfoContent = ({
  baseDir,
  taskId,
  allFiles,
  contextFiles,
  aiderTotalCost,
  maxInputTokens,
  clearMessages,
  runCommand,
  resetTask,
  mode,
  showFileDialog,
  task,
  updateTask,
  refreshAllFiles,
  refreshContextFiles,
  onToggleFilesSidebarCollapse,
}: Props) =>

================
File: src/renderer/src/components/project/GitBranchesButton.tsx
================
import { useCallback, useEffect, useRef, useState } from 'react';
import { FaArrowsRotate, FaBan, FaCodeMerge, FaCompress, FaDownload, FaFileLines, FaPlay, FaPlus, FaRobot, FaUpload } from 'react-icons/fa6';
import { TbDeviceImacDown } from 'react-icons/tb';
import { FaPencilAlt } from 'react-icons/fa';
import { IoGitBranch } from 'react-icons/io5';
import { VscWorktreeSmall } from 'react-icons/vsc';
import { MdKeyboardArrowDown } from 'react-icons/md';
import { useTranslation } from 'react-i18next';
import { useHotkeys } from 'react-hotkeys-hook';
import { useLocalStorage } from '@reactuses/core';
import { BranchInfo, GitSyncCommits, TaskGitStatus } from '@common/types';
⋮----
import { useApi } from '@/contexts/ApiContext';
import { useClickOutside } from '@/hooks/useClickOutside';
import { useConfiguredHotkeys } from '@/hooks/useConfiguredHotkeys';
import { invokeAction, registerAction, unregisterAction } from '@/stores/actionsStore';
import { ConfirmDialog } from '@/components/common/ConfirmDialog';
import { Checkbox } from '@/components/common/Checkbox';
import { RadioButton } from '@/components/common/RadioButton';
import { InlineEditPanel } from '@/components/common/InlineEditPanel';
import { WorktreeActionDialog } from '@/components/project/WorktreeActionDialog';
import { GitStatusBadges } from '@/components/project/GitStatusBadges';
import { GitBranchesPopup } from '@/components/project/GitBranchesPopup';
import { GitBranchesTooltipContent } from '@/components/project/GitBranchesTooltipContent';
import { Tooltip } from '@/components/ui/Tooltip';
import { showErrorNotification, showInfoNotification } from '@/utils/notifications';
⋮----
enum PullStrategy {
  Merge = 'merge',
  Rebase = 'rebase',
}
⋮----
const slugifyBranchName = (name: string) =>
⋮----
const trimBranchNameSeparators = (name: string) =>
⋮----
const truncateBranchName = (name: string) =>
⋮----
type Props = {
  baseDir: string;
  taskId: string;
  worktreePath?: string;
  status?: TaskGitStatus | null;
  taskName?: string;
  disabled?: boolean;
  onSwitchToLocal: () => void;
  onSwitchToWorktree: () => void;
  willShowConfirmDialog?: boolean;
  onMerge: (targetBranch?: string) => void;
  onSquash: (targetBranch?: string, commitMessage?: string) => void;
  onOnlyUncommitted: () => void;
  onRebaseFromBranch: (fromBranch?: string) => void;
  onAbortRebase: () => void;
  onContinueRebase: () => void;
  onResolveConflictsWithAgent: () => void;
  onRenameBranch: (newBranchName: string) => Promise<void>;
  canAbortRebase?: boolean;
  canContinueRebase?: boolean;
  canResolveConflictsWithAgent?: boolean;
};
⋮----
const refreshOnVisible = () =>
⋮----
// The error is already logged to the task chat by the backend with a 'Resolve with AI' action
// eslint-disable-next-line no-console
⋮----
const handleToggle = () =>
⋮----
const handleCreateBranchConfirm = async () =>
⋮----
const handleNewBranchFrom = (branch: BranchInfo) =>
⋮----
const handleCreateBranchFromConfirm = async () =>
⋮----
const handleSelect = (branch: BranchInfo) =>
⋮----
const trackRecentBranch = (branchName: string) =>
⋮----
// ignore persistence errors
⋮----
const performCheckout = async (branch: BranchInfo, takeOver = false) =>
⋮----
const performMerge = async (branch: BranchInfo) =>
⋮----
const performRebase = async (branch: BranchInfo) =>
⋮----
const handleMergeIntoCurrent = (branch: BranchInfo) =>
⋮----
const handleRebaseOnto = (branch: BranchInfo) =>
⋮----
const handleRebaseWorktreeOnto = (branch: BranchInfo) =>
⋮----
const handleDelete = (branch: BranchInfo) =>
⋮----
const handleDeleteConfirm = async () =>
⋮----
const handlePullConfirm = () =>
⋮----
const handlePullStrategyChange = (value: string) =>
⋮----
const handleUpdateBranch = async (branch: BranchInfo) =>
⋮----
const handlePushConfirm = async () =>
⋮----
const handlePushCancel = () =>
⋮----
const handleBranchEditConfirm = async () =>
⋮----
const handleBranchEditCancel = () =>
⋮----
const handleSwitchToLocal = () =>
⋮----
const handleSwitchToWorktree = () =>
⋮----

⋮----
<button onClick=
⋮----
onClick=
⋮----
content=
⋮----
handleCloseDropdown();
if (rebaseBranch)
onRebaseFromBranch(rebaseBranch);
⋮----
title=
⋮----
setBranchToMerge(null);
void performMerge(branch);
⋮----
onCancel=
⋮----
setBranchToRebase(null);
void performRebase(branch);
⋮----
onConfirm=
⋮----
setShowOnlyUncommittedDialog(false);
onOnlyUncommitted();
⋮----
setShowAbortRebaseConfirm(false);
onAbortRebase();
⋮----
setShowContinueRebaseConfirm(false);
onContinueRebase();
⋮----
setShowResolveWithAgentConfirm(false);
onResolveConflictsWithAgent();
⋮----
confirmButtonText=
⋮----
label=
⋮----
<Checkbox label=

================
File: src/renderer/src/components/project/GitBranchesPopup.tsx
================
import { MouseEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { CgSpinner } from 'react-icons/cg';
import { FaChevronDown, FaChevronRight, FaFolder, FaFolderOpen, FaMagnifyingGlass } from 'react-icons/fa6';
import { IoGitBranch } from 'react-icons/io5';
import { clsx } from 'clsx';
import { BranchInfo } from '@common/types';
⋮----
type Props = {
  branches: BranchInfo[];
  currentBranch: string;
  loading: boolean;
  recentBranches: string[];
  worktreeMode?: boolean;
  onSelect: (branch: BranchInfo) => void;
  onNewBranchFrom: (branch: BranchInfo) => void;
  onUpdateBranch?: (branch: BranchInfo) => void;
  onMergeIntoCurrent: (branch: BranchInfo) => void;
  onRebaseOnto: (branch: BranchInfo) => void;
  onRebaseWorktreeOnto?: (branch: BranchInfo) => void;
  onDelete: (branch: BranchInfo) => void;
  onRequestClose?: () => void;
};
⋮----
type BranchTreeNode = {
  name: string;
  fullPath: string;
  branch?: BranchInfo;
  children: BranchTreeNode[];
};
⋮----
type RowItem =
  | { type: 'section'; label: string }
  | { type: 'branch'; branch: BranchInfo; depth: number }
  | { type: 'folder'; node: BranchTreeNode; depth: number; collapsed: boolean };
⋮----
const buildBranchTree = (branches: BranchInfo[]): BranchTreeNode[] =>
⋮----
const rowKey = (row?: RowItem) =>
⋮----
// Collapsed by default; only folders on the current branch's path start expanded
⋮----
const walk = (nodes: BranchTreeNode[], depth: number) =>
⋮----
const handleFilterChange = (value: string) =>
⋮----
const toggleFolder = (fullPath: string, currentlyCollapsed: boolean) =>
⋮----
className=
⋮----
<span className=
⋮----
const handleKeyDown = (e: KeyboardEvent) =>
⋮----
return <div key=
⋮----
<div key=
⋮----
setOpenSubmenu(null);
item.action();

================
File: src/renderer/src/components/project/GitBranchesTooltipContent.tsx
================
import { CgArrowBottomLeft, CgArrowTopRight } from 'react-icons/cg';
import { IoGitBranch } from 'react-icons/io5';
import { TiTick } from 'react-icons/ti';
import { useTranslation } from 'react-i18next';
import { GitSyncCommits } from '@common/types';
⋮----
type Props = {
  currentBranch: string;
  syncCommits: GitSyncCommits;
  worktreeBaseBranch?: string;
};
⋮----
const renderCommitList = (commits: string[]) =>

================
File: src/renderer/src/components/project/GitInitButton.tsx
================
import { useState } from 'react';
import { IoGitBranch } from 'react-icons/io5';
import { useTranslation } from 'react-i18next';
⋮----
import { useApi } from '@/contexts/ApiContext';
import { Tooltip } from '@/components/ui/Tooltip';
import { showErrorNotification, showInfoNotification } from '@/utils/notifications';
⋮----
type Props = {
  baseDir: string;
  taskId: string;
  disabled?: boolean;
  onInitialized: () => void;
};
⋮----
const handleInitialize = async () =>

================
File: src/renderer/src/components/project/GitStatusBadges.tsx
================
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { CgArrowBottomLeft, CgArrowTopRight } from 'react-icons/cg';
import { TiWarning } from 'react-icons/ti';
⋮----
import type { TaskGitStatus } from '@common/types';
⋮----
import { Tooltip } from '@/components/ui/Tooltip';
⋮----
type Props = {
  status?: TaskGitStatus | null;
  showOutgoing: boolean;
  showIncoming: boolean;
};

================
File: src/renderer/src/components/project/MobileSidebar.tsx
================
import { Dispatch, SetStateAction } from 'react';
import { motion } from 'framer-motion';
import { clsx } from 'clsx';
import { ContextFile, Mode, TaskData } from '@common/types';
import { FiChevronDown } from 'react-icons/fi';
⋮----
import { FilesContextInfoContent } from '@/components/project/FilesContextInfoContent';
⋮----
type AddFileDialogOptions = {
  readOnly: boolean;
};
⋮----
type Props = {
  showSidebar: boolean;
  setShowSidebar: (show: boolean) => void;
  baseDir: string;
  taskId: string;
  allFiles: string[];
  contextFiles: ContextFile[];
  aiderTotalCost: number;
  maxInputTokens: number;
  clearMessages: (clearContext?: boolean) => void;
  runCommand: (command: string) => void;
  resetTask: () => void;
  mode: Mode;
  setAddFileDialogOptions: Dispatch<SetStateAction<AddFileDialogOptions | null>>;
  task: TaskData;
  updateTask: (taskId: string, updates: Partial<TaskData>) => void;
  refreshAllFiles: (useGit?: boolean) => Promise<void>;
  refreshContextFiles: () => Promise<void>;
};
⋮----
export const MobileSidebar = ({
  showSidebar,
  setShowSidebar,
  baseDir,
  taskId,
  allFiles,
  contextFiles,
  aiderTotalCost,
  maxInputTokens,
  clearMessages,
  runCommand,
  resetTask,
  mode,
  setAddFileDialogOptions,
  task,
  updateTask,
  refreshAllFiles,
  refreshContextFiles,
}: Props) =>
⋮----
className=
⋮----
<div onClick=

================
File: src/renderer/src/components/project/MountedTaskView.tsx
================
import { TaskData } from '@common/types';
import { forwardRef } from 'react';
import { clsx } from 'clsx';
⋮----
import { TaskView, TaskViewRef } from '@/components/project/TaskView';
import { ExtensionsProvider } from '@/contexts/ExtensionsContext';
import { FloatingExtensionPanels } from '@/components/extensions/FloatingExtensionPanels';
import { useActiveAgentProfile } from '@/utils/agents';
⋮----
type Props = {
  projectDir: string;
  task: TaskData;
  isActive: boolean;
  isProjectActive: boolean;
  inputHistory: string[];
  shouldFocusPrompt: boolean;
  updateTask: (taskId: string, updates: Partial<TaskData>, useOptimistic?: boolean) => void;
  updateOptimisticTaskState: (taskId: string, taskState: string) => void;
  onArchiveTask?: () => void;
  onUnarchiveTask?: () => void;
  onDeleteTask?: () => void;
  onToggleTaskSidebar?: () => void;
};
⋮----
// Inactive pooled tasks are hidden with CSS (display:none) rather than unmounted or wrapped in a
// hidden <Activity>. This keeps the TaskView fully mounted with its effects/subscriptions intact,
// so switching back is instant with no message reload and no sidebar re-fetch.
⋮----
<div className=

================
File: src/renderer/src/components/project/NoProjectsOpen.tsx
================
import { useTranslation } from 'react-i18next';
⋮----
// @ts-expect-error TypeScript is not aware of asset import
import icon from '../../../../../resources/icon.png?asset';
⋮----
type Props = {
  onOpenProject: () => void;
};
⋮----
{/* eslint-disable-next-line i18next/no-literal-string -- brand name */}

================
File: src/renderer/src/components/project/OpenProjectDialog.tsx
================
import { ChangeEvent, useCallback, useEffect, useState } from 'react';
import { CgSpinner } from 'react-icons/cg';
import { FaFolder } from 'react-icons/fa';
import { useTranslation } from 'react-i18next';
import { ProjectData } from '@common/types';
import { useHotkeys } from 'react-hotkeys-hook';
⋮----
import { useConfiguredHotkeys } from '@/hooks/useConfiguredHotkeys';
import { AutocompletionInput } from '@/components/AutocompletionInput';
import { Accordion } from '@/components/common/Accordion';
import { BaseDialog } from '@/components/common/BaseDialog';
import { ConfirmDialog } from '@/components/common/ConfirmDialog';
import { IconButton } from '@/components/common/IconButton';
import { Input } from '@/components/common/Input';
import { Tabs } from '@/components/common/Tabs';
import { useApi } from '@/contexts/ApiContext';
⋮----
enum OpenProjectMode {
  Open = 'open',
  Clone = 'clone',
}
⋮----
const isValidRepositoryUrl = (url: string): boolean =>
⋮----
type Props = {
  onClose: () => void;
  onAddProject: (baseDir: string) => void;
  openProjects: ProjectData[];
};
⋮----
// eslint-disable-next-line no-console
⋮----
// Browse for folder
⋮----
const loadRecentProjects = async () =>
⋮----
const updateSuggestions = async () =>
⋮----
const updateDestinationSuggestions = async () =>
⋮----
// eslint-disable-next-line no-console
⋮----
const handleAddProject = () =>
⋮----
const handleCloneProject = async () =>
⋮----
const handleCancelClone = () =>
⋮----
// eslint-disable-next-line no-console
⋮----
const handleRepositoryUrlChange = (e: ChangeEvent<HTMLInputElement>) =>
⋮----
const handleCloneDestinationChange = (value: string, isFromSuggestion: boolean) =>
⋮----
const handleConfirm = () =>
⋮----
const handleModeChange = (tabId: string) =>
⋮----
<BaseDialog title=
⋮----
title=
⋮----
setShowSuggestions(!isFromSuggestion);
setProjectPath(value);
⋮----
placeholder=
⋮----
onAddProject(path);
onClose();

================
File: src/renderer/src/components/project/ProjectTabContent.tsx
================
import { MouseEvent } from 'react';
import { CgSpinner } from 'react-icons/cg';
import { MdClose } from 'react-icons/md';
import { clsx } from 'clsx';
⋮----
export const getProjectTabClassName = (selected: boolean): string
⋮----
type Props = {
  baseDir: string;
  isActive: boolean;
  isProcessing?: boolean;
  onClose?: () => void;
};
⋮----
const handleClose = (event: MouseEvent<HTMLDivElement>) =>
⋮----
className=

================
File: src/renderer/src/components/project/ProjectTabs.tsx
================
import { ProjectData } from '@common/types';
import { compareBaseDirs } from '@common/utils';
import { CSS } from '@dnd-kit/utilities';
import { MouseEvent, useEffect, useMemo, useRef, useState } from 'react';
import { Tab, TabGroup, TabList, Listbox, ListboxButton, ListboxOption, ListboxOptions } from '@headlessui/react';
import { clsx } from 'clsx';
import { CgSpinner } from 'react-icons/cg';
import { MdAdd, MdChevronLeft, MdChevronRight } from 'react-icons/md';
import { DndContext, closestCenter, KeyboardSensor, PointerSensor, useSensor, useSensors } from '@dnd-kit/core';
import { SortableContext, sortableKeyboardCoordinates, arrayMove, useSortable, horizontalListSortingStrategy } from '@dnd-kit/sortable';
import { useTranslation } from 'react-i18next';
import { HiChevronDown, HiCheck } from 'react-icons/hi2';
⋮----
import type { DragEndEvent } from '@dnd-kit/core';
⋮----
import { MenuOption, useContextMenu } from '@/contexts/ContextMenuContext';
import { useProjectProcessingState } from '@/stores/projectStore';
import { useResponsive } from '@/hooks/useResponsive';
import { useOS } from '@/hooks/useOS';
import { ExtensionComponentWrapper } from '@/components/extensions/ExtensionComponentWrapper';
import { getProjectTabClassName, ProjectTabContent } from '@/components/project/ProjectTabContent';
import { useApi } from '@/contexts/ApiContext';
⋮----
type Props = {
  openProjects: ProjectData[];
  activeProject: string | undefined;
  onAddProject: () => void;
  onSetActiveProject: (baseDir: string) => void;
  onCloseProject: (baseDir: string) => void;
  onCloseOtherProjects: (baseDir: string) => void;
  onCloseAllProjects: () => void;
  onReorderProjects: (projects: ProjectData[]) => void;
};
⋮----
const checkScrollButtonsVisibility = () =>
⋮----
const handleScroll = () =>
⋮----
const handleScrollLeft = () =>
⋮----
const handleScrollRight = () =>
⋮----
const handleDragEnd = (event: DragEndEvent) =>
⋮----
// useMemo for project IDs to prevent SortableContext from re-rendering unnecessarily
⋮----
onChange=

================
File: src/renderer/src/components/project/ProjectView.tsx
================
import { AutonomyMode, InputHistoryData, ProjectStartMode, TaskCreatedData, TaskData, DefaultTaskState } from '@common/types';
import { useTranslation } from 'react-i18next';
import { startTransition, useCallback, useEffect, useOptimistic, useRef, useState } from 'react';
import { useLocalStorage } from '@reactuses/core';
import { useHotkeys } from 'react-hotkeys-hook';
import { clsx } from 'clsx';
⋮----
import { COLLAPSED_WIDTH, EXPANDED_WIDTH, MIN_WIDTH, MAX_WIDTH, TaskSidebar, TaskSidebarRef } from './TaskSidebar/TaskSidebar';
⋮----
import {
  useProjectTasks,
  setProjectTasks,
  updateProjectTask,
  addProjectTask,
  removeProjectTask,
  clearProjectTasks,
  useProjectStore,
} from '@/stores/projectStore';
import { unloadTasks } from '@/stores/taskStore';
import { releaseTaskFiles, useTaskAllFiles } from '@/stores/taskFilesStore';
import { getTaskDir, getSortedVisibleTasks } from '@/utils/task-utils';
import { cleanupProjectCache, cleanupTaskCache } from '@/stores/extensionUIStore';
import { cleanupProcessingResponseMessage } from '@/hooks/useTaskResponseHandlers';
import { useSettingsStore } from '@/stores/settingsStore';
import { useProjectSettings } from '@/contexts/ProjectSettingsContext';
import { LoadingOverlay } from '@/components/common/LoadingOverlay';
import { TaskViewRef } from '@/components/project/TaskView';
import { MountedTaskView } from '@/components/project/MountedTaskView';
import { useApi } from '@/contexts/ApiContext';
import { TasksProvider } from '@/contexts/TasksContext';
import { useConfiguredHotkeys } from '@/hooks/useConfiguredHotkeys';
import { useOverlayFocusRestore } from '@/hooks/useOverlayFocusRestore';
import { useResponsive } from '@/hooks/useResponsive';
import { useBooleanState } from '@/hooks/useBooleanState';
import { showNotification } from '@/utils/browser-notifications';
import { showErrorNotification, showInfoNotification, showWarningNotification } from '@/utils/notifications';
import { ExtensionsProvider } from '@/contexts/ExtensionsContext';
import { FloatingExtensionPanels } from '@/components/extensions/FloatingExtensionPanels';
import { useFileEditorStore } from '@/stores/fileEditorStore';
import { useActiveAgentProfile } from '@/utils/agents';
import { PaletteItem, PaletteItemType, useCommandPaletteStore } from '@/stores/commandPaletteStore';
import { invokeAction, registerAction, unregisterAction } from '@/stores/actionsStore';
import { FileEditorModal } from '@/components/Workspace/FileEditorModal';
⋮----
type Props = {
  projectDir: string;
  isProjectActive?: boolean;
  initialTaskId?: string;
};
⋮----
// Keep recently-active tasks mounted (LRU) so switching back is instant. The active task is
// always at the front and therefore never evicted. In-progress tasks are never evicted
// either, so a running task stays mounted while other tasks are used.
⋮----
// eslint-disable-next-line no-console
⋮----
// Check if URL specifies a task to activate
⋮----
// Only mount/load the task when the project is currently active; startup mode is
// re-applied by the first-activation effect for background projects.
⋮----
const handleProjectStarted = () =>
⋮----
const handleTaskCreated = (
⋮----
const handleTaskInitialized = (taskData: TaskData) =>
⋮----
const handleTaskUpdated = (taskData: TaskData) =>
⋮----
const handleTaskStarted = (taskData: TaskData) =>
⋮----
const handleTaskCompleted = (taskData: TaskData) =>
⋮----
const handleTaskCancelled = (taskData: TaskData) =>
⋮----
const handleTaskDeleted = (taskData: TaskData) =>
⋮----
const handleInputHistoryUpdate = (data: InputHistoryData) =>
⋮----
// Set up listeners
⋮----
const initProject = async () =>
⋮----
// Start project
⋮----
// Load input history (may have been emitted before listener was registered)
⋮----
// Load tasks
⋮----
// Handle startup mode only for currently-active projects; background projects defer it
// to their first activation (startup-on-first-activation effect below).
⋮----
// eslint-disable-next-line no-console
⋮----
// Startup-on-first-activation: background projects do not load any task until they are first
// activated, then the configured startup mode is applied once (also serving as a self-healing
// fallback if the startup race was lost or task creation failed).
⋮----
// Track in-progress task ids so the mount pool can protect them from eviction.
⋮----
// Release backend/store state for tasks evicted from the mount pool (mirrors unmount cleanup).
⋮----
// Switch to specific task tabs (Ctrl + 1-9)
⋮----
// eslint-disable-next-line no-console
⋮----
// Task will be automatically removed via the existing handleTaskDeleted listener
⋮----
// eslint-disable-next-line no-console
⋮----
// Close current task
⋮----
// eslint-disable-next-line no-console
⋮----
// eslint-disable-next-line no-console
⋮----
// eslint-disable-next-line no-console
⋮----
// Optionally switch to the new task
⋮----
// eslint-disable-next-line no-console
⋮----

================
File: src/renderer/src/components/project/TaskBar.tsx
================
import { AgentProfile, AIDER_MODES, EditFormat, Mode, Model, ModelsData, RawModelInfo, TaskData } from '@common/types';
import { ReactNode, forwardRef, useCallback, useImperativeHandle, useMemo, useRef, useState } from 'react';
import { BsCodeSlash, BsFilter, BsLayoutSidebar } from 'react-icons/bs';
import { CgTerminal } from 'react-icons/cg';
import { GoProjectRoadmap } from 'react-icons/go';
import { IoMdClose } from 'react-icons/io';
import { MdOutlineRefresh } from 'react-icons/md';
import { VscLock, VscUnlock } from 'react-icons/vsc';
import { RiMenuUnfold4Line, RiRobot2Line } from 'react-icons/ri';
import { useTranslation } from 'react-i18next';
import { getProviderModelId } from '@common/agent';
import { clsx } from 'clsx';
import { extractProviderModel } from '@common/utils';
⋮----
import { IconButton } from '@/components/common/IconButton';
import { ModelSelector, ModelSelectorRef } from '@/components/ModelSelector';
import { showErrorNotification } from '@/utils/notifications';
import { EditFormatSelector } from '@/components/PromptField/EditFormatSelector';
import { TaskGitControls } from '@/components/project/TaskGitControls';
import { Tooltip } from '@/components/ui/Tooltip';
import { useSaveSettings, useSettingsStore } from '@/stores/settingsStore';
import { useApi } from '@/contexts/ApiContext';
import { useResponsive } from '@/hooks/useResponsive';
import { useModelProviders } from '@/contexts/ModelProviderContext';
import { ExtensionComponentWrapper } from '@/components/extensions/ExtensionComponentWrapper';
import { useAiderConnectorStatus } from '@/hooks/useAiderConnectorStatus';
⋮----
export type TaskBarRef = {
  openMainModelSelector: (model?: string) => void;
  openAgentModelSelector: (model?: string) => void;
};
⋮----
type Props = {
  baseDir: string;
  task: TaskData;
  modelsData: ModelsData | null;
  mode: Mode;
  activeAgentProfile: AgentProfile | null;
  onModelsChange?: (modelsData: ModelsData | null) => void;
  runCommand: (command: string) => void;
  onToggleSidebar: () => void;
  onToggleTaskSidebar?: () => void;
  updateTask: (taskId: string, updates: Partial<TaskData>, useOptimistic?: boolean) => void;
  onRestartAiderConnector: () => void;
};
⋮----
// Use task provider/model first, fallback to active agent profile
⋮----
// Add the currently selected model if it's not in the known list (custom model)
⋮----
// Create a custom model object for the current model
⋮----
taskModels.unshift(customModel); // Add to the beginning for visibility
⋮----
// optimistic update
⋮----
// eslint-disable-next-line no-console
⋮----
// eslint-disable-next-line no-console
⋮----
// eslint-disable-next-line no-console
⋮----
// eslint-disable-next-line no-console
⋮----
// eslint-disable-next-line no-console
⋮----
// eslint-disable-next-line no-console
⋮----
// eslint-disable-next-line no-console
⋮----
/** Format the Python installation status for display in the TaskBar. */
⋮----
return t('pythonSetup.failed',
⋮----
<div className=
⋮----
<Tooltip content=
⋮----
{/* Task sidebar toggle for mobile */}
⋮----
// Two-row layout for Agent mode with Aider tools
⋮----
{/* Row 1: AGENT */}
⋮----
{/* Row 2: AIDER */}
⋮----
// Original horizontal layout for other modes

================
File: src/renderer/src/components/project/TaskControlBar.tsx
================
import { AIDER_MODES, AutonomyMode, DEFAULT_AUTONOMY_MODE, Mode, TaskData } from '@common/types';
import { forwardRef, useImperativeHandle, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { FaInfoCircle } from 'react-icons/fa';
import { MdPlaylistRemove, MdUndo } from 'react-icons/md';
import { VscTerminal } from 'react-icons/vsc';
import { clsx } from 'clsx';
⋮----
import { AgentSelector, AgentSelectorRef } from '@/components/AgentSelector';
import { ModeSelector } from '@/components/PromptField/ModeSelector';
import { AutonomySelector } from '@/components/PromptField/AutonomySelector';
import { Button } from '@/components/common/Button';
import { ExtensionComponentWrapper } from '@/components/extensions/ExtensionComponentWrapper';
import { useProjectSettings } from '@/contexts/ProjectSettingsContext';
⋮----
export type TaskControlBarRef = {
  openAgentProfileSelector: () => void;
};
⋮----
type Props = {
  baseDir: string;
  task: TaskData;
  isActive: boolean;
  mode: Mode;
  onModeChanged: (mode: Mode) => void;
  clearMessages: () => void;
  toggleTerminal?: () => void;
  terminalVisible?: boolean;
  showTaskInfoPanel?: boolean;
  onToggleTaskInfoPanel?: () => void;
  onAutonomyModeChanged?: (mode: AutonomyMode) => void;
  canUndoContextChange?: boolean;
  onUndoContextChange?: () => void;
};
⋮----
const handleAutonomyLockChanged = (locked: boolean) =>

================
File: src/renderer/src/components/project/TaskGitControls.tsx
================
import { SwitchToLocalOptions, SwitchToWorktreeOptions, TaskData, WorkingMode, UncommittedFiles } from '@common/types';
import { useCallback, useEffect, useState, useMemo } from 'react';
import { MdClose } from 'react-icons/md';
import { CgSpinner } from 'react-icons/cg';
import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next';
⋮----
import { registerAction, unregisterAction } from '@/stores/actionsStore';
import { Button } from '@/components/common/Button';
import { IconButton } from '@/components/common/IconButton';
import { GitBranchesButton } from '@/components/project/GitBranchesButton';
import { GitInitButton } from '@/components/project/GitInitButton';
import { WorktreeRevertButton } from '@/components/project/WorktreeRevertButton';
import { BaseDialog } from '@/components/common/BaseDialog';
import { RadioButton } from '@/components/common/RadioButton';
import { useApi } from '@/contexts/ApiContext';
import { useTaskGitStatus } from '@/hooks/useTaskGitStatus';
import { useCommitChanges } from '@/hooks/useCommitChanges';
import { useProjectTasks } from '@/stores/projectStore';
⋮----
enum LocalSwitchOption {
  Merge = 'merge',
  MergeAll = 'mergeAll',
  Remove = 'remove',
}
⋮----
enum WorktreeSwitchOption {
  JustSwitch = 'justSwitch',
  CarryOverRemove = 'carryOverRemove',
  CarryOverKeep = 'carryOverKeep',
}
⋮----
type Props = {
  task: TaskData;
  onMerge: (targetBranch?: string) => void;
  onSquash: (targetBranch?: string, commitMessage?: string) => void;
  onOnlyUncommitted: () => void;
  onRebaseFromBranch: (fromBranch?: string) => void;
  onAbortRebase: () => void;
  onContinueRebase: () => void;
  onResolveConflictsWithAgent: () => void;
  onRevert: () => void;
  onRenameBranch: (newBranchName: string) => Promise<void>;
  isMerging: boolean;
};
⋮----
// eslint-disable-next-line no-console
⋮----
// eslint-disable-next-line no-console
⋮----
// eslint-disable-next-line no-console
⋮----
const performMergeAndSwitch = async () =>
⋮----
// eslint-disable-next-line no-console
⋮----
const performMergeAndSwitchAll = async () =>
⋮----
// eslint-disable-next-line no-console
⋮----
const handleLocalConfirm = async () =>
⋮----
const handleWorktreeConfirm = async () =>
⋮----
const performSwitchToWorktreeWithChanges = async (dropSourceChanges: boolean) =>
⋮----
// eslint-disable-next-line no-console
⋮----
const getWarningMessage = () =>
⋮----
const handleSwitchToLocal = () =>
⋮----
const handleSwitchToWorktree = () =>
⋮----
const handleCancelCommit = () =>
⋮----
<Button onClick=
⋮----
onChange=
⋮----
<div className="font-medium">
⋮----
title=
⋮----

================
File: src/renderer/src/components/project/TaskView.tsx
================
import {
  AIDER_MODES,
  AutonomyMode,
  DefaultTaskState,
  GroupMessage,
  isLoadingMessage,
  isLogMessage,
  isUserMessage,
  Message,
  Mode,
  Model,
  ModelsData,
  QueuedPromptData,
  TaskData,
  TodoItem,
  UserMessage,
} from '@common/types';
import {
  forwardRef,
  startTransition,
  useCallback,
  useDeferredValue,
  useEffect,
  useImperativeHandle,
  useMemo,
  useOptimistic,
  useRef,
  useState,
  useTransition,
} from 'react';
import { useTranslation } from 'react-i18next';
import { ResizableBox, ResizeCallbackData } from 'react-resizable';
import { clsx } from 'clsx';
import { getProviderModelId } from '@common/agent';
import { RiMenuUnfold4Line } from 'react-icons/ri';
import { useEvent, useLocalStorage } from '@reactuses/core';
import { useHotkeys } from 'react-hotkeys-hook';
import { v4 as uuidv4 } from 'uuid';
⋮----
import { useSidebarWidth } from './useSidebarWidth';
⋮----
import { Tooltip } from '@/components/ui/Tooltip';
import { Messages, MessagesRef } from '@/components/message/Messages';
import { VirtualizedMessages, VirtualizedMessagesRef } from '@/components/message/VirtualizedMessages';
import { useSettingsStore } from '@/stores/settingsStore';
import { useProjectSettings } from '@/contexts/ProjectSettingsContext';
import { useApi } from '@/contexts/ApiContext';
import { AddFileDialog } from '@/components/project/AddFileDialog';
import { TaskBar, TaskBarRef } from '@/components/project/TaskBar';
import { TaskControlBar, TaskControlBarRef } from '@/components/project/TaskControlBar';
import { PromptField, PromptFieldRef } from '@/components/PromptField';
import { ExtensionComponentWrapper } from '@/components/extensions/ExtensionComponentWrapper';
import { Button } from '@/components/common/Button';
import { TodoWindow } from '@/components/project/TodoWindow';
import { TerminalView, TerminalViewRef } from '@/components/terminal/TerminalView';
import { MobileSidebar } from '@/components/project/MobileSidebar';
import { FilesContextInfoContent } from '@/components/project/FilesContextInfoContent';
import { WelcomeMessage } from '@/components/project/WelcomeMessage';
⋮----
import { useResponsive } from '@/hooks/useResponsive';
import { useActiveAgentProfile } from '@/utils/agents';
import { useModelProviders } from '@/contexts/ModelProviderContext';
import { useTask } from '@/contexts/TasksContext';
import { useConfiguredHotkeys } from '@/hooks/useConfiguredHotkeys';
import { LoadingOverlay } from '@/components/common/LoadingOverlay';
import { useTaskMessages, useOptimizedTaskState, useTaskStore } from '@/stores/taskStore';
import { useTaskAllFiles, useTaskAutocompletionWords } from '@/stores/taskFilesStore';
import { getTaskDir } from '@/utils/task-utils';
import { showErrorNotification } from '@/utils/notifications';
import { useSearchText } from '@/hooks/useSearchText';
import { TaskStateActions } from '@/components/message/TaskStateActions';
import { TaskInfoPanel } from '@/components/message/TaskInfoPanel';
import { registerAction, unregisterAction } from '@/stores/actionsStore';
import { getSessionKey, useTerminalVisible, toggleTerminalVisible, setTerminalVisible } from '@/stores/terminalStore';
⋮----
type AddFileDialogOptions = {
  readOnly: boolean;
};
⋮----
export type TaskViewRef = {
  exportMessagesToImage: () => void;
  focusPromptField: () => void;
  openMainModelSelector: () => void;
  openAgentProfileSelector: () => void;
};
⋮----
type Props = {
  projectDir: string;
  task: TaskData;
  updateTask: (taskId: string, updates: Partial<TaskData>, useOptimistic?: boolean) => void;
  updateOptimisticTaskState: (taskId: string, taskState: string) => void;
  inputHistory: string[];
  isActive?: boolean;
  isProjectActive?: boolean;
  shouldFocusPrompt?: boolean;
  onArchiveTask?: () => void;
  onUnarchiveTask?: () => void;
  onDeleteTask?: () => void;
  onToggleTaskSidebar?: () => void;
};
⋮----
// On first mount, defer the heavy message list until after the shell has painted: the first
// commit renders the shell + LoadingOverlay (renderReady=false), then a rAF flips renderReady
// inside a transition so the messages render without blocking the initial load. Guarded by
// renderReady so re-running the effect (e.g. a project-level <Activity> becoming visible again)
// does not re-trigger the transition and flash the loading overlay on an already-rendered task.
⋮----
// Focus prompt field
⋮----
// This submission is an edit of a previous message
⋮----
setEditingMessageIndex(null); // Clear editing state
⋮----
// OPTIMISTIC: Add user message immediately before backend response
⋮----
// eslint-disable-next-line no-console
⋮----
// eslint-disable-next-line no-console
⋮----
// eslint-disable-next-line no-console
⋮----
// eslint-disable-next-line no-console
⋮----
// eslint-disable-next-line no-console
⋮----
// Optimistically remove the messages after it
⋮----
// If the target message is part of a group (e.g. subagent group), we need to keep
// all messages belonging to that group, since the target is the first child but the
// group has additional children that come after it in the flat messages array.
// Without this, the optimistic slice would remove the remaining group children.
⋮----
// eslint-disable-next-line no-console
⋮----
// eslint-disable-next-line no-console
⋮----
// eslint-disable-next-line no-console
⋮----
// eslint-disable-next-line no-console
⋮----
// eslint-disable-next-line no-console
⋮----
// eslint-disable-next-line no-console
⋮----
// Only update task state if we're interrupting the entire task, not a specific agent
⋮----
<div className=
⋮----

⋮----
height=

================
File: src/renderer/src/components/project/TodoListItem.tsx
================
import { KeyboardEvent, useState } from 'react';
import { clsx } from 'clsx';
import { MdEdit, MdDelete, MdCheck, MdClose, MdSwapHoriz } from 'react-icons/md';
import { useTranslation } from 'react-i18next';
import { TodoItem } from '@common/types';
⋮----
import { Checkbox } from '../common/Checkbox';
import { IconButton } from '../common/IconButton';
⋮----
type Props = {
  item: TodoItem;
  onToggle?: (name: string, completed: boolean) => void;
  onUpdate?: (name: string, updates: Partial<TodoItem>) => void;
  onDelete?: (name: string) => void;
  onHandoff?: (name: string) => void;
};
⋮----
const handleToggle = (checked: boolean) =>
⋮----
const handleEdit = () =>
⋮----
const handleSaveEdit = () =>
⋮----
const handleCancelEdit = () =>
⋮----
const handleKeyPress = (e: KeyboardEvent) =>
⋮----
const handleDelete = () =>
⋮----
const handleHandoff = () =>
⋮----
onChange=
⋮----
<span className=

================
File: src/renderer/src/components/project/TodoWindow.tsx
================
import { KeyboardEvent, useEffect, useRef, useState } from 'react';
import { MdAdd, MdExpandLess, MdOutlineChecklist, MdPlaylistRemove } from 'react-icons/md';
import { useTranslation } from 'react-i18next';
import { TodoItem } from '@common/types';
import { ResizableBox } from 'react-resizable';
⋮----
import { IconButton } from '../common/IconButton';
import { Button } from '../common/Button';
⋮----
import { TodoListItem } from './TodoListItem';
⋮----
import { useTaskTodoItems } from '@/stores/taskStore';
import { Input } from '@/components/common/Input';
⋮----
type Props = {
  taskId: string;
  onToggleTodo?: (name: string, completed: boolean) => void;
  onAddTodo?: (name: string) => void;
  onUpdateTodo?: (name: string, updates: Partial<TodoItem>) => void;
  onDeleteTodo?: (name: string) => void;
  onHandoffTodo?: (name: string) => void;
  onClearAllTodos: () => void;
};
⋮----
const handleToggleExpand = () =>
⋮----
const handleAddTodo = () =>
⋮----
const handleSaveTodo = () =>
⋮----
const handleCancelAdd = () =>
⋮----
const handleKeyPress = (e: KeyboardEvent) =>
⋮----
// Auto-collapse when all items are completed, auto-expand again when the completed set is replaced with new items
⋮----
// Keep the last non-empty evaluation so a fully-completed set followed by clearing still re-expands later
⋮----
// eslint-disable-next-line react-hooks/set-state-in-effect
⋮----
return null; // Don't render the if there are no todos
⋮----
{/* Header */}
⋮----
{/* Content */}
⋮----
{/* Add Todo Input */}
⋮----
onChange=
⋮----
placeholder=
⋮----
{/* Todo List */}
⋮----
/* Collapsed state */

================
File: src/renderer/src/components/project/useSidebarWidth.ts
================
import { useEffect, useState } from 'react';
import { useDebounceFn } from '@reactuses/core';
⋮----
import { useIndexedDB } from '@/hooks/useIndexedDB';
⋮----
export const useSidebarWidth = (projectBaseDir: string) =>
⋮----
// Sync immediate width with persisted width when it loads
⋮----
const setWidth = (newWidth: number) =>

================
File: src/renderer/src/components/project/WelcomeMessage.tsx
================
import { RiRobot2Line } from 'react-icons/ri';
import { useTranslation } from 'react-i18next';
import { FiCode, FiFile, FiLayers, FiTerminal, FiArrowRight } from 'react-icons/fi';
import { CgTerminal } from 'react-icons/cg';
import { clsx } from 'clsx';
⋮----
// @ts-expect-error TypeScript is not aware of asset import
import icon from '../../../../../resources/icon.png?asset';
⋮----
import type { Mode } from '@common/types';
⋮----
import { useExtensionComponentsWrapper } from '@/components/extensions/useExtensionComponentsWrapper';
⋮----
type Props = {
  onModeChange?: (mode: Mode) => void;
  mode?: Mode;
  projectDir?: string;
  taskId?: string;
};
⋮----
const handleModeClick = (mode: Mode) =>
⋮----
<div className=

================
File: src/renderer/src/components/project/WorktreeActionDialog.tsx
================
import { ChangeEvent, useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { BranchInfo } from '@common/types';
⋮----
import { ConfirmDialog } from '@/components/common/ConfirmDialog';
import { useApi } from '@/contexts/ApiContext';
import SearchableSelect from '@/components/common/SearchableSelect';
import { Input } from '@/components/common/Input';
⋮----
type Props = {
  baseDir: string;
  title: string;
  message?: string;
  confirmButtonText: string;
  defaultBranch?: string;
  showCommitMessage?: boolean;
  initialCommitMessage?: string;
  onCancel: () => void;
  onConfirm: (branch: string, commitMessage?: string) => void;
};
⋮----
// eslint-disable-next-line react-hooks/set-state-in-effect
⋮----
const load = async () =>
⋮----
const handleConfirm = () =>
⋮----
const handleBranchChange = (value: string) =>
⋮----
const handleCommitMessageChange = (e: ChangeEvent<HTMLInputElement>) =>
⋮----
label=

================
File: src/renderer/src/components/project/WorktreeRevertButton.tsx
================
import { useState } from 'react';
import { MdUndo } from 'react-icons/md';
import { useTranslation } from 'react-i18next';
import { useLocalStorage } from '@reactuses/core';
⋮----
import { IconButton } from '@/components/common/IconButton';
import { ConfirmDialog } from '@/components/common/ConfirmDialog';
import { Checkbox } from '@/components/common/Checkbox';
⋮----
type Props = {
  onRevert: () => void;
  disabled?: boolean;
};
⋮----
const handleRevertClick = () =>
⋮----
const handleRevertConfirm = () =>
⋮----
tooltip=
⋮----
title=
⋮----
confirmButtonText=

================
File: src/renderer/src/components/PromptField/acceptPathSegment.ts
================
import { acceptCompletion, completionStatus, selectedCompletion } from '@codemirror/autocomplete';
import { type EditorView } from '@codemirror/view';
⋮----
export const acceptPathSegment = (view: EditorView, mentionMode: boolean): boolean =>

================
File: src/renderer/src/components/PromptField/AudioAnalyzer.tsx
================
import { useEffect, useRef } from 'react';
⋮----
type Props = {
  stream: MediaStream | null;
  barCount?: number;
  width?: number;
};
⋮----
export const AudioAnalyzer = (
⋮----
// fftSize must be a power of 2 between 32 and 32768
// The number of frequency bins is fftSize / 2
// So we need fftSize to be approximately barCount * 2
⋮----
// Find the nearest power of 2
⋮----
// Clamp to valid range
⋮----
const draw = () =>
⋮----
// Try to match the theme color
// We can use the computed style of the canvas element itself or body
⋮----
// Scale the height
⋮----
// Draw bar

================
File: src/renderer/src/components/PromptField/AutoApprove.tsx
================
// This component has been replaced by AutonomySelector.tsx
// Kept as empty file to avoid breaking imports during migration

================
File: src/renderer/src/components/PromptField/AutonomySelector.tsx
================
import { memo, startTransition, useCallback, useOptimistic } from 'react';
import { useTranslation } from 'react-i18next';
import { FaRegCompass } from 'react-icons/fa';
import { LuHand, LuZap } from 'react-icons/lu';
import { VscLock, VscUnlock } from 'react-icons/vsc';
import { AutonomyMode } from '@common/types';
import { clsx } from 'clsx';
⋮----
import { IconButton } from '@/components/common/IconButton';
import { Tooltip } from '@/components/ui/Tooltip';
⋮----
type Props = {
  mode: AutonomyMode;
  locked: boolean;
  onChange?: (mode: AutonomyMode) => void;
  onLockChange?: (locked: boolean) => void;
};
⋮----
onClick=
⋮----
className=

================
File: src/renderer/src/components/PromptField/CommandSuggestion.tsx
================
import { useTranslation } from 'react-i18next';
import { ReactNode } from 'react';
⋮----
type Props = {
  command: string;
  description?: ReactNode;
};
⋮----
export const CommandSuggestion = (

================
File: src/renderer/src/components/PromptField/EditFormatSelector.tsx
================
import { forwardRef, useImperativeHandle, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { MdKeyboardArrowUp } from 'react-icons/md';
import { EditFormat } from '@common/types';
⋮----
import { useClickOutside } from '@/hooks/useClickOutside';
import { useDropdownState } from '@/hooks/useDropdownState';
⋮----
export type EditFormatSelectorRef = {
  open: () => void;
};
⋮----
// Define available edit formats based on the EditFormat type
⋮----
type Props = {
  currentFormat: EditFormat;
  onFormatChange: (format: EditFormat) => void;
};
⋮----
const handleFormatSelected = (format: EditFormat) =>
⋮----
const renderFormatItem = (format: EditFormat, index: number) => (
    <div
      key={format}
      ref={index === state.highlightedIndex ? highlightedItemRef : undefined}
      className={`flex items-center w-full hover:bg-bg-tertiary transition-colors duration-200 ${index === state.highlightedIndex ? 'bg-bg-tertiary' : 'text-text-tertiary'}`}
    >
      <button
        onClick={() => handleFormatSelected(format)}
        className={`flex-grow px-3 py-1 text-left text-xs ${format === currentFormat ? 'text-text-primary font-bold' : ''}`}
      >
        {format}
      </button>
    </div>
  );

================
File: src/renderer/src/components/PromptField/index.ts
================


================
File: src/renderer/src/components/PromptField/InputHistoryMenu.tsx
================
import { useEffect, useRef } from 'react';
⋮----
import { useClickOutside } from '@/hooks/useClickOutside';
⋮----
type Props = {
  items: string[];
  highlightedIndex: number;
  onSelect: (item: string) => void;
  onClose: () => void;
  onScrollTop?: () => void;
  keepHighlightAtTop?: boolean;
};
⋮----
const handleScroll = () =>

================
File: src/renderer/src/components/PromptField/ModeSelector.tsx
================
import { memo } from 'react';
import { Mode } from '@common/types';
⋮----
import { ItemSelector } from '@/components/common/ItemSelector';
import { useModes } from '@/hooks/useModes';
⋮----
type Props = {
  baseDir: string;
  mode: Mode;
  onModeChange: (mode: Mode) => void;
};

================
File: src/renderer/src/components/PromptField/PromptField.tsx
================
/* eslint-disable react-compiler/react-compiler */
import {
  acceptCompletion,
  autocompletion,
  completionStatus,
  type Completion,
  CompletionContext,
  type CompletionResult,
  currentCompletions,
  moveCompletionSelection,
  startCompletion,
} from '@codemirror/autocomplete';
import { EditorView, keymap } from '@codemirror/view';
import { AIDER_MODES, Mode, PromptBehavior, QueuedPromptData, QuestionData, SuggestionMode } from '@common/types';
import { githubDarkInit } from '@uiw/codemirror-theme-github';
import CodeMirror, { Annotation, Prec, type Extension, type ReactCodeMirrorRef } from '@uiw/react-codemirror';
import { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
import { useDebounce } from '@reactuses/core';
import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next';
import { BiSend } from 'react-icons/bi';
import { MdSave, MdStop, MdMic, MdMicOff, MdOutlineScheduleSend } from 'react-icons/md';
import { AiOutlineClose } from 'react-icons/ai';
import { clsx } from 'clsx';
import { parseCommandArgs, parseSkillCommand, SKILL_COMMAND_PREFIX } from '@common/utils';
import { getSubagentId } from '@common/agent';
⋮----
import { QueuedPromptsList } from './QueuedPromptsList';
import { usePromptFieldText } from './usePromptFieldText';
import { wordCompletions } from './wordCompletions';
import { acceptPathSegment } from './acceptPathSegment';
⋮----
import { useTaskQueuedPrompts } from '@/stores/taskStore';
import { InputHistoryMenu } from '@/components/PromptField/InputHistoryMenu';
import { showErrorNotification } from '@/utils/notifications';
import { useCommands } from '@/hooks/useCommands';
import { useSkills } from '@/hooks/useSkills';
import { useApi } from '@/contexts/ApiContext';
import { useAgents } from '@/contexts/AgentsContext';
import { Tooltip } from '@/components/ui/Tooltip';
import { useAudioRecorder } from '@/hooks/useAudioRecorder';
import { AudioAnalyzer } from '@/components/PromptField/AudioAnalyzer';
import { useResponsive } from '@/hooks/useResponsive';
⋮----
const ANSWERS = ['y', 'n', 'a', 'd', 'maybe']; // Added 'maybe' (Branch B)
⋮----
const isPathLike = (input: string): boolean =>
⋮----
export interface PromptFieldRef {
  focus: () => void;
  setText: (text: string) => void;
  appendText: (text: string) => void;
  setImages: (images: string[]) => void;
}
⋮----
type Props = {
  baseDir: string;
  taskId: string;
  processing: boolean;
  isActive: boolean;
  allFiles?: string[];
  words?: string[];
  inputHistory?: string[];
  openModelSelector?: (model?: string) => void;
  openAgentModelSelector?: (model?: string) => void;
  mode: Mode;
  onModeChanged: (mode: Mode) => void;
  runPrompt: (prompt: string, images?: string[]) => void;
  savePrompt: (prompt: string) => Promise<void>;
  showFileDialog: (readOnly: boolean) => void;
  addFiles?: (filePaths: string[], readOnly?: boolean) => void;
  clearMessages: () => void;
  scrapeWeb: (url: string, filePath?: string) => void;
  question?: QuestionData | null;
  answerQuestion: (answer: string) => void;
  removeQueuedPrompt?: (id: string) => void;
  sendQueuedPromptNow?: (id: string) => void;
  reorderQueuedPrompts?: (prompts: QueuedPromptData[]) => void;
  editQueuedPrompt?: (id: string, newText: string) => void;
  interruptResponse: () => void;
  runCommand: (command: string) => void;
  runTests: (testCmd?: string) => void;
  redoLastUserPrompt: () => void;
  editUserMessage: () => void;
  lastMessageIsUserMessage?: boolean;
  isEditingLastMessage?: boolean;
  canSaveEditedPrompt?: boolean;
  disabled?: boolean;
  promptBehavior: PromptBehavior;
  clearLogMessages: () => void;
  scrollToBottom?: () => void;
  onToggleTaskInfoPanel?: () => void;
  handoffConversation?: (focus?: string) => Promise<void>;
  createSubtask?: (args?: string) => void;
};
⋮----
// we need to set the text when the component mounts and the text is not empty (e.g. Activity use case)
⋮----
// eslint-disable-next-line react-hooks/exhaustive-deps
⋮----
// When recording stops, we reset after a short delay or immediately?
// We want to keep the text.
// We just need to reset the recorder state for next time.
⋮----
// eslint-disable-next-line react-hooks/exhaustive-deps
⋮----
// eslint-disable-next-line react-hooks/exhaustive-deps
⋮----
// Check if we're at a command argument position with options (before early return)
⋮----
// Handle @-based file suggestions (exclusive)
⋮----
// Handle command suggestions
⋮----
// Suggest skills after /skill:
⋮----
// Add custom and extension commands to the list
⋮----
// Re-open the completion menu to show skill names right after accepting /skill:
⋮----
// Ensure cursor is at the end after setting text
⋮----
// Ensure cursor is at the end after appending text
⋮----
url = commandArgs; // Only URL provided
⋮----
filePath = undefined; // If only spaces after URL, treat as no filePath
⋮----
const requiresConfirmation = (command: string): boolean =>
⋮----
// Skip auto-invoke for commands that need additional arguments
⋮----
// Check if it's a custom or extension command
⋮----
const handleSavePrompt = async () =>
⋮----
// Check if it's a custom or extension command
⋮----
// On desktop, Shift+Enter inserts a new line
⋮----
// On mobile, Enter inserts a new line (default behavior)
// On desktop, Enter submits the prompt
⋮----
return false; // Allow default behavior (new line)
⋮----
// Desktop behavior: submit or handle special cases
⋮----
// only strip a non-empty path segment; ignore command words like "/add"
⋮----
onClick=
⋮----
ref=
⋮----
className=
⋮----
onClose=

================
File: src/renderer/src/components/PromptField/QueuedPromptsList.tsx
================
import { KeyboardEvent, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { BiSend } from 'react-icons/bi';
import { MdDragIndicator, MdEdit, MdImage, MdPlaylistRemove, MdSave } from 'react-icons/md';
import { closestCenter, DndContext, type DragEndEvent, KeyboardSensor, PointerSensor, useSensor, useSensors } from '@dnd-kit/core';
import { arrayMove, SortableContext, sortableKeyboardCoordinates, useSortable, verticalListSortingStrategy } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { QueuedPromptData } from '@common/types';
⋮----
import { Tooltip } from '@/components/ui/Tooltip';
⋮----
type QueuedPromptsListProps = {
  queuedPrompts: QueuedPromptData[];
  onRemove?: (id: string) => void;
  onSendNow?: (id: string) => void;
  onReorder?: (prompts: QueuedPromptData[]) => void;
  onEdit?: (id: string, newText: string) => void;
};
⋮----
type SortableQueuedPromptItemProps = {
  prompt: QueuedPromptData;
  onRemove?: (id: string) => void;
  onSendNow?: (id: string) => void;
  onEdit?: (id: string, newText: string) => void;
};
⋮----
const handleStartEdit = () =>
⋮----
const handleSave = () =>
⋮----
const handleCancel = () =>
⋮----
const handleKeyDown = (e: KeyboardEvent) =>
⋮----
<Tooltip content=
⋮----
<button onClick=
⋮----
onClick=
⋮----
const handleDragEnd = (event: DragEndEvent) =>

================
File: src/renderer/src/components/PromptField/usePromptFieldText.ts
================
import { useCallback } from 'react';
⋮----
import { useIndexedDB } from '@/hooks/useIndexedDB';
⋮----
export const usePromptFieldText = (baseDir: string, taskId: string, onLoad?: (value: string) => void) =>

================
File: src/renderer/src/components/PromptField/wordCompletions.ts
================
import { type CompletionResult } from '@codemirror/autocomplete';
⋮----
export const wordCompletions = (from: number, words: string[], files: string[]): CompletionResult => (

================
File: src/renderer/src/components/readonly/ReadonlyHeader.tsx
================
import { ReadonlyProjectData } from '@common/types';
import { useTranslation } from 'react-i18next';
import { clsx } from 'clsx';
⋮----
// @ts-expect-error TypeScript is not aware of asset import
import icon from '../../../../../resources/icon.png?asset';
⋮----
import { ExtensionComponentWrapper } from '@/components/extensions/ExtensionComponentWrapper';
import { getProjectTabClassName, ProjectTabContent } from '@/components/project/ProjectTabContent';
⋮----
type Props = {
  projects: ReadonlyProjectData[];
  projectDir: string;
  onSelectProject: (projectDir: string) => void;
};
⋮----
{/* eslint-disable-next-line i18next/no-literal-string -- brand name */}
⋮----
className=

================
File: src/renderer/src/components/readonly/ReadonlyProjectView.tsx
================
import { useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Message, TaskData, TaskStateData } from '@common/types';
⋮----
import { ReadonlyTaskSidebar } from '@/components/readonly/ReadonlyTaskSidebar';
import { ReadonlyTaskView } from '@/components/readonly/ReadonlyTaskView';
import { LoadingOverlay } from '@/components/common/LoadingOverlay';
import { ExtensionsProvider } from '@/contexts/ExtensionsContext';
import { useReadonlyApi } from '@/contexts/ReadonlyApiContext';
import { convertTaskStateMessages } from '@/utils/task-messages';
import { useResponsive } from '@/hooks/useResponsive';
import { useBooleanState } from '@/hooks/useBooleanState';
import { applyReadonlyEvent, applyReadonlyLogEvent, applyReadonlyTaskListEvent, isReadonlyTaskListEvent } from '@/utils/readonly-events';
⋮----
type Props = {
  projectDir: string;
  selectedTaskId?: string;
  onSelectTask: (taskId: string) => void;
};
⋮----
const load = async () =>
⋮----
// Resync after a socket reconnect: events emitted while disconnected are lost forever,
// so refetch the task list and the selected task state.
⋮----
const handleSelectTask = (taskId: string) =>
⋮----
messages=

================
File: src/renderer/src/components/readonly/ReadonlyTaskSidebar.tsx
================
import { useState } from 'react';
import { TaskData } from '@common/types';
⋮----
import { TaskSidebar } from '@/components/project/TaskSidebar/TaskSidebar';
⋮----
type Props = {
  tasks: TaskData[];
  selectedTaskId?: string;
  projectDir: string;
  onSelectTask: (taskId: string) => void;
  isMobile?: boolean;
  onClose?: () => void;
};
⋮----
const handleToggleCollapse = ()

================
File: src/renderer/src/components/readonly/ReadonlyTaskView.tsx
================
import { DefaultTaskState, Message, TaskData, TaskStateData } from '@common/types';
import { useTranslation } from 'react-i18next';
import { MdMenu } from 'react-icons/md';
⋮----
import { Messages } from '@/components/message/Messages';
import { ExtensionComponentWrapper } from '@/components/extensions/ExtensionComponentWrapper';
import { convertTaskStateMessages } from '@/utils/task-messages';
import { useSettingsStore } from '@/stores/settingsStore';
⋮----
type Props = {
  projectDir: string;
  task: TaskData;
  state: TaskStateData;
  logMessages?: Message[];
  onToggleTaskSidebar?: () => void;
};
⋮----
<button type="button" className="p-1 text-text-muted hover:text-text-primary" onClick=

================
File: src/renderer/src/components/settings/agent/sections/ProfileGeneralSection.tsx
================
import { AgentProfile, ContextCompactionType, SettingsData } from '@common/types';
import { useTranslation } from 'react-i18next';
import { FaTimes } from 'react-icons/fa';
import { clsx } from 'clsx';
⋮----
import { Slider } from '@/components/common/Slider';
import { Input } from '@/components/common/Input';
import { Button } from '@/components/common/Button';
import { IconButton } from '@/components/common/IconButton';
import { InfoIcon } from '@/components/common/InfoIcon';
import { Checkbox } from '@/components/common/Checkbox';
import { Tooltip } from '@/components/ui/Tooltip';
⋮----
type Props = {
  profile: AgentProfile;
  settings: SettingsData;
  onSettingChange: <K extends keyof AgentProfile>(field: K, value: AgentProfile[K]) => void;
};
⋮----
const renderCompactionTypeButton = (type: ContextCompactionType, label: string, tooltip: string) =>
⋮----
const handleClick = () =>
⋮----
className=
⋮----
<span>
⋮----
{/* Temperature Column */}
⋮----
onClick=
⋮----
{/* Max Tokens Column */}
⋮----
onChange=
⋮----
{/* Auto-compact Threshold Percentage */}
⋮----
<span className="whitespace-nowrap">
⋮----
{/* Auto-compact Threshold Tokens */}
⋮----
{/* Compaction Type */}
⋮----

================
File: src/renderer/src/components/settings/agent/sections/ProfilePromptsSection.tsx
================
import { AgentProfile } from '@common/types';
import { useTranslation } from 'react-i18next';
⋮----
import { AgentRules } from '../AgentRules';
import { SystemPromptEditor } from '../SystemPromptEditor';
⋮----
import { InfoIcon } from '@/components/common/InfoIcon';
⋮----
type Props = {
  profile: AgentProfile;
  onSettingChange: <K extends keyof AgentProfile>(field: K, value: AgentProfile[K]) => void;
};
⋮----
<span>

================
File: src/renderer/src/components/settings/agent/sections/ProfileSubagentsSection.tsx
================
import { useState } from 'react';
import { AgentProfile, ContextMemoryMode, InvocationMode } from '@common/types';
import { useTranslation } from 'react-i18next';
import Sketch from '@uiw/react-color-sketch';
⋮----
import { SystemPromptEditor } from '../SystemPromptEditor';
⋮----
import { Checkbox } from '@/components/common/Checkbox';
import { Select } from '@/components/common/Select';
import { TextArea } from '@/components/common/TextArea';
import { InfoIcon } from '@/components/common/InfoIcon';
⋮----
type Props = {
  profile: AgentProfile;
  availableSubagents: AgentProfile[];
  onSettingChange: <K extends keyof AgentProfile>(field: K, value: AgentProfile[K]) => void;
};
⋮----
const handleAllowAllSubagentsChange = (checked: boolean) =>
⋮----
const handleSubagentAllowedChange = (subagentId: string, checked: boolean) =>
⋮----
const handleToggleColorPicker = () =>
⋮----
{/* Using subagents */}
⋮----
<span>
⋮----
{/* Acting as subagent */}
⋮----
onClick=

================
File: src/renderer/src/components/settings/agent/sections/ProfileToolsSection.tsx
================
import { AgentProfile, ExtensionToolInfo, GenericTool, McpServerConfig, ToolApprovalState } from '@common/types';
import { useTranslation } from 'react-i18next';
import { FaPlug } from 'react-icons/fa';
import {
  AIDER_TOOL_ADD_CONTEXT_FILES,
  AIDER_TOOL_DESCRIPTIONS,
  AIDER_TOOL_DROP_CONTEXT_FILES,
  AIDER_TOOL_GET_CONTEXT_FILES,
  AIDER_TOOL_GROUP_NAME,
  AIDER_TOOL_RUN_PROMPT,
  POWER_TOOL_BASH,
  POWER_TOOL_DESCRIPTIONS,
  POWER_TOOL_FETCH,
  POWER_TOOL_FILE_EDIT,
  POWER_TOOL_FILE_READ,
  POWER_TOOL_FILE_WRITE,
  POWER_TOOL_GLOB,
  POWER_TOOL_GREP,
  POWER_TOOL_GROUP_NAME,
  POWER_TOOL_SEMANTIC_SEARCH,
  TODO_TOOL_CLEAR_ITEMS,
  TODO_TOOL_DESCRIPTIONS,
  TODO_TOOL_GET_ITEMS,
  TODO_TOOL_GROUP_NAME,
  TODO_TOOL_SET_ITEMS,
  TODO_TOOL_UPDATE_ITEM_COMPLETION,
  TASKS_TOOL_CREATE_TASK,
  TASKS_TOOL_DELETE_TASK,
  TASKS_TOOL_DESCRIPTIONS,
  TASKS_TOOL_GET_TASK,
  TASKS_TOOL_GET_TASK_MESSAGE,
  TASKS_TOOL_GROUP_NAME,
  TASKS_TOOL_LIST_TASKS,
  TASKS_TOOL_SEARCH_TASK,
  MEMORY_TOOL_DELETE,
  MEMORY_TOOL_DESCRIPTIONS,
  MEMORY_TOOL_GROUP_NAME,
  MEMORY_TOOL_LIST,
  MEMORY_TOOL_UPDATE,
  MEMORY_TOOL_RETRIEVE,
  MEMORY_TOOL_STORE,
  SKILLS_TOOL_GROUP_NAME,
} from '@common/tools';
⋮----
import { McpServerItem } from '../../mcp/McpServerItem';
import { GenericToolGroupItem } from '../GenericToolGroupItem';
⋮----
import { Button } from '@/components/common/Button';
⋮----
type Props = {
  profile: AgentProfile;
  mcpServers: Record<string, McpServerConfig>;
  extensionToolsInfo: ExtensionToolInfo[];
  onSettingChange: <K extends keyof AgentProfile>(field: K, value: AgentProfile[K]) => void;
  onApprovalChange: (toolId: string | string[], approval: ToolApprovalState) => void;
  onToggleServerEnabled: (serverKey: string, checked: boolean) => void;
  onOpenMcpServers?: () => void;
};
⋮----
const handleExtEnabledChange = (enabled: boolean) =>

================
File: src/renderer/src/components/settings/agent/AgentProfileItem.tsx
================
import { AgentProfile } from '@common/types';
import { BiCopy, BiCut, BiTrash } from 'react-icons/bi';
import { clsx } from 'clsx';
import { useTranslation } from 'react-i18next';
⋮----
import { VerticalDotsMenu, type MenuOption } from '@/components/common/VerticalDotsMenu';
⋮----
type Props = {
  profile: AgentProfile;
  isSelected: boolean;
  onClick: (id: string) => void;
  onCopy?: (profile: AgentProfile) => void;
  onCut?: (profile: AgentProfile) => void;
  onDelete?: (profile: AgentProfile) => void;
  isCut?: boolean;
  isDefaultProfile?: boolean;
};
⋮----
export const AgentProfileItem = (
⋮----
onClick=

================
File: src/renderer/src/components/settings/agent/AgentRules.tsx
================
import { Trans, useTranslation } from 'react-i18next';
import { AgentProfile } from '@common/types';
⋮----
import { TextArea } from '@/components/common/TextArea';
import { CodeInline } from '@/components/common/CodeInline';
⋮----
type AgentRulesProps = {
  profile: AgentProfile;
  handleProfileSettingChange: <K extends keyof AgentProfile>(field: K, value: AgentProfile[K]) => void;
};

================
File: src/renderer/src/components/settings/agent/AgentSettings.tsx
================
import { AgentProfile, McpServersData, Model, ProjectData, SettingsData, ToolApprovalState } from '@common/types';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { v4 as uuidv4 } from 'uuid';
import { FaChevronLeft, FaChevronRight, FaPaste, FaPlus } from 'react-icons/fa';
import { DEFAULT_AGENT_PROFILE, getProviderModelId, DEFAULT_AGENT_PROFILES } from '@common/agent';
import { BiReset, BiTrash } from 'react-icons/bi';
import { closestCenter, DndContext, type DragEndEvent, KeyboardSensor, PointerSensor, useSensor, useSensors } from '@dnd-kit/core';
import { arrayMove, SortableContext, sortableKeyboardCoordinates, verticalListSortingStrategy } from '@dnd-kit/sortable';
import { useTranslation } from 'react-i18next';
import { AnimatePresence, motion } from 'framer-motion';
import { clsx } from 'clsx';
import { RiMenuUnfold4Line } from 'react-icons/ri';
import { HiXMark } from 'react-icons/hi2';
⋮----
import { SortableAgentProfileItem } from './SortableAgentProfileItem';
import { ProfileGeneralSection } from './sections/ProfileGeneralSection';
import { ProfilePromptsSection } from './sections/ProfilePromptsSection';
import { ProfileToolsSection } from './sections/ProfileToolsSection';
import { ProfileSubagentsSection } from './sections/ProfileSubagentsSection';
⋮----
import { useBooleanState } from '@/hooks/useBooleanState';
import { useExtensionTools } from '@/hooks/useExtensionTools';
import { useResponsive } from '@/hooks/useResponsive';
import { useMcpServers } from '@/contexts/McpServersContext';
import { getPathBasename } from '@/utils/path-utils';
import { IconButton } from '@/components/common/IconButton';
import { Button } from '@/components/common/Button';
import { ModelSelector } from '@/components/ModelSelector';
import { Input } from '@/components/common/Input';
import { Tabs } from '@/components/common/Tabs';
import { useModelProviders } from '@/contexts/ModelProviderContext';
import { showErrorNotification } from '@/utils/notifications';
⋮----
enum ProfileTab {
  General = 'general',
  Prompts = 'prompts',
  Tools = 'tools',
  Subagents = 'subagents',
}
⋮----
type Props = {
  settings: SettingsData;
  setSettings: (settings: SettingsData) => void;
  agentProfiles: AgentProfile[];
  setAgentProfiles: (profiles: AgentProfile[]) => void;
  initialProfileId?: string;
  openProjects?: ProjectData[];
  selectedProfileContext?: 'global' | string;
  mcpServersData?: McpServersData;
  onOpenMcpServers?: (context?: string) => void;
};
⋮----
// Profile context state for project-level profiles
⋮----
// Sync internal profileContext with selectedProfileContext prop
⋮----
// eslint-disable-next-line react-hooks/set-state-in-effect
⋮----
// Update contextIndex to match the selected context
⋮----
// Keep latest values in refs so the effect can read them without depending on them
⋮----
// Handle initial profile ID - set context and select profile
⋮----
// Set the profile context based on the initial profile's projectDir
⋮----
// eslint-disable-next-line react-hooks/set-state-in-effect
⋮----
// Update contextIndex to match the target context
⋮----
// Select the initial profile
⋮----
// Context navigation logic
const navigateContext = (direction: 'prev' | 'next') =>
⋮----
// Filter profiles based on current context
⋮----
// Calculate missing default profiles (only for global context)
⋮----
// Profiles that can act as subagents for the selected profile
⋮----
// Get context display name
const getContextDisplayName = () =>
⋮----
// Update filtered profiles when context changes
⋮----
// eslint-disable-next-line react-hooks/set-state-in-effect
⋮----
const handleDragEnd = (event: DragEndEvent) =>
⋮----
// Get the actual indices in the full agentProfiles array
⋮----
// Update order using the new API
⋮----
// useMemo for project IDs to prevent SortableContext from re-rendering unnecessarily
⋮----
const handleCreateNewProfile = () =>
⋮----
}, 0); // Focus the input after the state update
⋮----
const handleDeleteProfile = (agentProfileId: string | null) =>
⋮----
const isDefaultProfile = (profileId: string | null): boolean =>
⋮----
const handleResetProfile = () =>
⋮----
const handleRestoreProfile = (profileId: string) =>
⋮----
const handleCopyProfile = (profile: AgentProfile) =>
⋮----
const handleCutProfile = (profile: AgentProfile) =>
⋮----
const handlePasteProfile = () =>
⋮----
// If this was a cut operation, remove the original and add new one
⋮----
// For copy operation, just add the new profile
⋮----
const handleProfileSettingChange = <K extends keyof AgentProfile>(field: K, value: AgentProfile[K]) =>
⋮----
const handleProfileChange = (profile: AgentProfile) =>
⋮----
const handleAddPreferredModel = (modelId: string) =>
⋮----
const handleModelChange = (model: Model | null) =>
⋮----
// Validate provider
⋮----
// Update profile
⋮----
// Add to preferred models
⋮----
// Add custom model if not in list
⋮----
const handleToggleServerEnabled = (serverKey: string, checked: boolean) =>
⋮----
const handleToolApprovalChange = (toolId: string | string[], approval: ToolApprovalState) =>
⋮----
const handleTabChange = (tabId: string) =>
⋮----
const handleSelectProfile = (id: string) =>
⋮----
{/* Profile Context Header */}
⋮----
onClick=
⋮----
onDelete=
⋮----
{/* Missing Default Profiles Section - Only shown in global context */}
⋮----
{/* Left List Pane */}
⋮----
{/* Mobile list drawer */}
⋮----
{/* Right Details Pane */}
⋮----
{/* Profile Header */}
⋮----
{/* Tab Content */}

================
File: src/renderer/src/components/settings/agent/BashToolItem.tsx
================
import { useTranslation } from 'react-i18next';
import { AgentProfile, BashToolSettings, GenericTool, ToolApprovalState } from '@common/types';
import { POWER_TOOL_BASH, POWER_TOOL_GROUP_NAME, TOOL_GROUP_NAME_SEPARATOR } from '@common/tools';
⋮----
import { GenericToolItem } from './GenericToolItem';
⋮----
import { Input } from '@/components/common/Input';
import { InfoIcon } from '@/components/common/InfoIcon';
⋮----
type Props = {
  tool: GenericTool;
  profile: AgentProfile;
  onApprovalChange?: (toolId: string, approval: ToolApprovalState) => void;
  onProfileChange: (field: keyof AgentProfile, value: AgentProfile[keyof AgentProfile]) => void;
};
⋮----
<span className="text-2xs">

================
File: src/renderer/src/components/settings/agent/GenericToolGroupItem.tsx
================
import { AgentProfile, GenericTool, ToolApprovalState } from '@common/types';
import { useTranslation } from 'react-i18next';
import { POWER_TOOL_BASH, TOOL_GROUP_NAME_SEPARATOR } from '@common/tools';
⋮----
import { GenericToolItem } from './GenericToolItem';
import { BashToolItem } from './BashToolItem';
⋮----
import { Accordion } from '@/components/common/Accordion';
import { Checkbox } from '@/components/common/Checkbox';
⋮----
type Props = {
  name: string;
  displayName?: string;
  tools: GenericTool[];
  profile: AgentProfile;
  onApprovalChange: (toolId: string, approval: ToolApprovalState) => void;
  onProfileChange: (field: keyof AgentProfile, value: AgentProfile[keyof AgentProfile]) => void;
  enabled?: boolean;
  onEnabledChange?: (enabled: boolean) => void;
};
⋮----
<Accordion title=

================
File: src/renderer/src/components/settings/agent/GenericToolItem.tsx
================
import { useTranslation } from 'react-i18next';
import { ReactNode } from 'react';
import { AgentProfile, GenericTool, ToolApprovalState } from '@common/types';
import { TOOL_GROUP_NAME_SEPARATOR } from '@common/tools';
⋮----
import { Select } from '@/components/common/Select';
import { InfoIcon } from '@/components/common/InfoIcon';
⋮----
type Props = {
  tool: GenericTool;
  profile: AgentProfile;
  onApprovalChange?: (toolId: string, approval: ToolApprovalState) => void;
  renderToolSettings?: () => ReactNode;
};
⋮----
// Default to 'Always' if approvals are not being managed in this context
⋮----
const handleApprovalChange = (value: string) =>
⋮----
{/* Conditionally render the approval select only if onApprovalChange is provided */}

================
File: src/renderer/src/components/settings/agent/SortableAgentProfileItem.tsx
================
import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { AgentProfile } from '@common/types';
⋮----
import { AgentProfileItem } from './AgentProfileItem';
⋮----
type SortableAgentProfileItemProps = {
  profile: AgentProfile;
  isSelected: boolean;
  onClick: (id: string) => void;
  onCopy?: (profile: AgentProfile) => void;
  onCut?: (profile: AgentProfile) => void;
  onDelete?: (profile: AgentProfile) => void;
  isCut?: boolean;
  isDefaultProfile?: boolean;
};
⋮----
export const SortableAgentProfileItem = (

================
File: src/renderer/src/components/settings/agent/SystemPromptEditor.tsx
================
import { ChangeEvent, ReactNode, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { SYSTEM_PROMPT_PLACEHOLDERS, SystemPromptPlaceholder, formatSystemPromptPlaceholder } from '@common/system-prompt-placeholders';
⋮----
import { TextArea } from '@/components/common/TextArea';
import { Tooltip } from '@/components/ui/Tooltip';
⋮----
type Props = {
  value: string;
  onChange: (value: string) => void;
  label?: ReactNode;
  placeholder?: string;
  className?: string;
  info?: ReactNode;
};
⋮----
export const SystemPromptEditor = (
⋮----
const handleChange = (e: ChangeEvent<HTMLTextAreaElement>) =>
⋮----
const handleInsertPlaceholder = (placeholderName: SystemPromptPlaceholder) =>

================
File: src/renderer/src/components/settings/mcp/McpOAuthControls.tsx
================
import { McpOAuthStatus, type McpServerConfig } from '@common/types';
import { clsx } from 'clsx';
import { useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
⋮----
import { Button } from '@/components/common/Button';
import { useApi } from '@/contexts/ApiContext';
⋮----
type Props = {
  serverName: string;
  config: McpServerConfig;
  refreshTrigger: number;
  onAuthenticated: () => void;
  onDisconnected: () => void;
  onStatusChange: (status: McpOAuthStatus) => void;
  borderless?: boolean;
};
⋮----
const loadStatus = async () =>
⋮----
const handleConnect = async () =>
⋮----
const handleDisconnect = async () =>
⋮----
<div className=

================
File: src/renderer/src/components/settings/mcp/McpServerForm.tsx
================
import { McpServerConfig } from '@common/types';
import { useState, useMemo, ChangeEvent } from 'react';
import { z } from 'zod/v3';
import { useTranslation } from 'react-i18next';
import { MdInfoOutline } from 'react-icons/md';
⋮----
import { TextArea } from '@/components/common/TextArea';
import { Button } from '@/components/common/Button';
import { Tooltip } from '@/components/ui/Tooltip';
⋮----
export type McpServer = {
  name: string;
  config: McpServerConfig;
};
⋮----
type Props = {
  onSave: (servers: Record<string, McpServerConfig>) => void;
  onCancel: () => void;
  servers?: McpServer[];
};
⋮----
// If multiple servers, merge them into a single object
⋮----
// Try to parse as JSON, or as a "bare" object (without enclosing {})
const parseConfig = (text: string) =>
⋮----
// Try as full JSON first
⋮----
// Try as "bare" object: wrap in braces and parse
⋮----
const handleAddServer = () =>
⋮----
// Accept both { mcpServers: {...} } and just { ... }
⋮----
const handleChange = (e: ChangeEvent<HTMLTextAreaElement>) =>
⋮----
placeholder=

================
File: src/renderer/src/components/settings/mcp/McpServerItem.tsx
================
import { McpOAuthStatus, McpServerConfig, McpTool, ToolApprovalState } from '@common/types';
import { extractIpcErrorMessage } from '@common/utils';
import { useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { FaPencilAlt, FaTrash } from 'react-icons/fa';
import { TOOL_GROUP_NAME_SEPARATOR } from '@common/tools';
⋮----
import { McpToolItem } from './McpToolItem';
import { McpOAuthControls } from './McpOAuthControls';
⋮----
import { Select } from '@/components/common/Select';
import { Accordion } from '@/components/common/Accordion';
import { IconButton } from '@/components/common/IconButton';
import { Checkbox } from '@/components/common/Checkbox';
import { useApi } from '@/contexts/ApiContext';
import { Tooltip } from '@/components/ui/Tooltip';
⋮----
type Props = {
  serverName: string;
  config: McpServerConfig;
  onRemove?: () => void;
  onEdit?: () => void;
  toolApprovals?: Record<string, ToolApprovalState>;
  onApprovalChange?: (toolId: string | string[], approval: ToolApprovalState) => void;
  reloadTrigger?: number;
  enabled?: boolean;
  onEnabledChange?: (enabled: boolean) => void;
  projectDir?: string;
};
⋮----
// eslint-disable-next-line no-console
⋮----
const getApproval = (tool: McpTool) => toolApprovals?.[`$
⋮----
<Accordion title=

================
File: src/renderer/src/components/settings/mcp/McpServerSidebarItem.tsx
================
import { BiPencil, BiRefresh, BiTrash } from 'react-icons/bi';
import { clsx } from 'clsx';
import { useTranslation } from 'react-i18next';
⋮----
import { VerticalDotsMenu, type MenuOption } from '@/components/common/VerticalDotsMenu';
import { Tooltip } from '@/components/ui/Tooltip';
⋮----
type Props = {
  serverName: string;
  isSelected: boolean;
  error?: string | null;
  onClick: (name: string) => void;
  onRefresh: (name: string) => void;
  onEdit: (name: string) => void;
  onRemove: (name: string) => void;
};
⋮----
onClick=

================
File: src/renderer/src/components/settings/mcp/McpServerToolsPanel.tsx
================
import { McpOAuthStatus, McpServerConfig, McpTool } from '@common/types';
import { clsx } from 'clsx';
import { useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { FaPencilAlt, FaSyncAlt, FaTrash } from 'react-icons/fa';
⋮----
import { McpOAuthControls } from './McpOAuthControls';
import { McpToolCard } from './McpToolCard';
⋮----
import { IconButton } from '@/components/common/IconButton';
⋮----
type Props = {
  serverName: string;
  config: McpServerConfig;
  tools: McpTool[] | null;
  loading: boolean;
  error: string | null;
  onReload: () => void;
  onEdit: () => void;
  onRemove: () => void;
};
⋮----
// A late tools-load failure with an auth error means OAuth discovery has completed in the main
// process; re-fetch the OAuth status so the Connect control appears without reselecting the server.
⋮----
// eslint-disable-next-line react-hooks/set-state-in-effect
⋮----
{/* Server header */}
⋮----
icon=
⋮----
{/* Tools content */}

================
File: src/renderer/src/components/settings/mcp/McpSettings.tsx
================
import { Dispatch, SetStateAction, useCallback, useEffect, useMemo, useState } from 'react';
import { McpServerConfig, McpServersData, McpTool, ProjectData } from '@common/types';
import { extractIpcErrorMessage } from '@common/utils';
import { useTranslation } from 'react-i18next';
import { FaChevronLeft, FaChevronRight, FaPencilAlt, FaPlus, FaSyncAlt } from 'react-icons/fa';
import { AnimatePresence, motion } from 'framer-motion';
import { RiMenuUnfold4Line } from 'react-icons/ri';
import { HiXMark } from 'react-icons/hi2';
⋮----
import { McpServer, McpServerForm } from './McpServerForm';
import { McpServerSidebarItem } from './McpServerSidebarItem';
import { McpServerToolsPanel } from './McpServerToolsPanel';
⋮----
import { useMcpServers } from '@/contexts/McpServersContext';
import { useApi } from '@/contexts/ApiContext';
import { useResponsive } from '@/hooks/useResponsive';
import { useBooleanState } from '@/hooks/useBooleanState';
import { getPathBasename } from '@/utils/path-utils';
import { Button } from '@/components/common/Button';
import { IconButton } from '@/components/common/IconButton';
⋮----
type ServerToolsState = {
  tools: McpTool[] | null;
  loading: boolean;
  error: string | null;
};
⋮----
type Props = {
  mcpServers?: McpServersData;
  setMcpServers?: Dispatch<SetStateAction<McpServersData>>;
  openProjects?: ProjectData[];
  selectedMcpContext?: string;
};
⋮----
// eslint-disable-next-line react-hooks/set-state-in-effect
⋮----
const navigateContext = (direction: 'prev' | 'next') =>
⋮----
const getContextDisplayName = () =>
⋮----
// Fall back to the first server when the selection is invalid or missing
⋮----
// Load tools for all servers in the current context to reflect their status
⋮----
const handleReload = () =>
⋮----
const handleRemove = (name: string) =>
⋮----
const handleSave = (savedServers: Record<string, McpServerConfig>) =>
⋮----
const handleCancel = () =>
⋮----
isEditingServersConfig
? Object.entries(servers).map(([name, config]) => (
⋮----
setSelectedServerName(name);
if (isMobile)
⋮----
{/* Context switcher */}
⋮----
onClick=
⋮----
{/* Server list */}
⋮----
onEdit=
⋮----
{/* Sidebar actions */}
⋮----
<Button onClick=
⋮----
{/* Left List Pane */}
⋮----
{/* Mobile list drawer */}
⋮----
{/* Right Details Pane */}

================
File: src/renderer/src/components/settings/mcp/McpToolCard.tsx
================
import { useTranslation } from 'react-i18next';
import { McpTool } from '@common/types';
⋮----
type Props = {
  tool: McpTool;
};
⋮----
export const McpToolCard = (

================
File: src/renderer/src/components/settings/mcp/McpToolItem.tsx
================
import { useTranslation } from 'react-i18next';
import { McpTool, ToolApprovalState } from '@common/types';
import { TOOL_GROUP_NAME_SEPARATOR } from '@common/tools';
⋮----
import { Select } from '@/components/common/Select';
import { InfoIcon } from '@/components/common/InfoIcon';
⋮----
type Props = {
  tool: McpTool;
  serverName: string;
  toolApprovals?: Record<string, ToolApprovalState>;
  onApprovalChange?: (toolId: string, approval: ToolApprovalState) => void;
};
⋮----
// Default to 'Always' if approvals are not being managed in this context
⋮----
const handleApprovalChange = (value: string) =>
⋮----
{/* Conditionally render the approval select only if onApprovalChange is provided */}

================
File: src/renderer/src/components/settings/AboutSettings.tsx
================
import { useTranslation } from 'react-i18next';
import { toast } from 'react-toastify';
import { FaFolderOpen, FaSyncAlt } from 'react-icons/fa';
import { TbLogs } from 'react-icons/tb';
import { SettingsData } from '@common/types';
⋮----
import { Button } from '@/components/common/Button';
import { Checkbox } from '@/components/common/Checkbox';
import { Section } from '@/components/common/Section';
import { useVersions } from '@/hooks/useVersions';
import { useApi } from '@/contexts/ApiContext';
⋮----
type Props = {
  settings: SettingsData;
  setSettings: (settings: SettingsData) => void;
  onShowLogs?: () => void;
};
⋮----
const openLogsDirectory = async () =>
⋮----
// eslint-disable-next-line no-console
⋮----
const handleDownloadUpdate = async () =>
⋮----
// eslint-disable-next-line no-console
⋮----
const handleTelemetryEnabledChange = (checked: boolean) =>
⋮----
label=
⋮----
onChange=
⋮----
<Section title=
⋮----
<Checkbox label=

================
File: src/renderer/src/components/settings/AiderSettings.tsx
================
import { useState } from 'react';
import { HiEye } from 'react-icons/hi';
import { Trans, useTranslation } from 'react-i18next';
import { SettingsData } from '@common/types';
⋮----
import { Button } from '@/components/common/Button';
import { Input } from '@/components/common/Input';
import { Section } from '@/components/common/Section';
import { TextArea } from '@/components/common/TextArea';
import { Checkbox } from '@/components/common/Checkbox';
import { CodeInline } from '@/components/common/CodeInline';
⋮----
type Props = {
  settings: SettingsData;
  setSettings: (settings: SettingsData) => void;
  initialShowEnvVars?: boolean;
};
⋮----
label=
⋮----
setSettings({
                ...settings,
                aider: {
                  ...settings.aider,
                  autoCommits: checked,
                },
              });

================
File: src/renderer/src/components/settings/ExtensionCard.tsx
================
import { useState } from 'react';
import ReactMarkdown from 'react-markdown';
import { useTranslation } from 'react-i18next';
import { FaChevronDown, FaCog, FaDownload, FaSync } from 'react-icons/fa';
import { clsx } from 'clsx';
⋮----
import type { AvailableExtension, InstalledExtension } from '@common/types';
⋮----
import { Button } from '@/components/common/Button';
import { IconButton } from '@/components/common/IconButton';
import { Toggle } from '@/components/common/Toggle';
import { MARKDOWN_COMPONENTS, REMARK_PLUGINS } from '@/components/message/utils';
import { Tooltip } from '@/components/ui/Tooltip';
import { useOS } from '@/hooks/useOS';
import { ExtensionSettingsDialog } from '@/components/settings/ExtensionSettingsDialog';
⋮----
// Helper to normalize extension data
const getExtensionData = (extension: InstalledExtension | AvailableExtension) =>
⋮----
type Props = {
  extension: InstalledExtension | AvailableExtension;
  isInstalled?: boolean;
  isDisabled?: boolean;
  isUninstalling?: boolean;
  isInstalling?: boolean;
  isUpdating?: boolean;
  isReloading?: boolean;
  hasUpdate?: boolean;
  installedFilePath?: string;
  onToggle?: (extensionFilePath: string, isDisabled: boolean) => void;
  onUninstall?: (exensionFilePath: string) => void;
  onInstall?: (extension: AvailableExtension) => void;
  onUpdate?: () => void;
  onReload?: (extensionFilePath: string) => void;
};
⋮----
const handleToggleExpand = () =>
⋮----
const handleToggleDisabled = () =>
⋮----
const handleUninstall = () =>
⋮----
const handleInstall = () =>
⋮----
const handleUpdate = () =>
⋮----
const handleReload = () =>
⋮----
const handleOpenSettings = () =>
⋮----
const handleCloseSettings = () =>
⋮----
className=
⋮----
{/* Icon */}
⋮----
{/* Info */}
⋮----
<h4 className=
⋮----
<p className=
⋮----
{/* Actions */}
⋮----
icon=
⋮----
{/* Expandable README section */}
⋮----
{/* Chevron button at bottom center */}
⋮----
<FaChevronDown className=
⋮----
{/* README content */}

================
File: src/renderer/src/components/settings/ExtensionSettingsDialog.tsx
================
import { useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
⋮----
import type { ExtensionConfigComponent } from '@common/types';
⋮----
import StringToReactComponent from '@/components/extensions/StringToReactComponent';
import { useApi } from '@/contexts/ApiContext';
import { useExtensions } from '@/contexts/ExtensionsContext';
import { useReactIcons } from '@/utils/extension-icons';
import { ConfirmDialog } from '@/components/common/ConfirmDialog';
import { LoadingOverlay } from '@/components/common/LoadingOverlay';
import { showErrorNotification, showSuccessNotification } from '@/utils/notifications';
⋮----
type Props = {
  extensionId: string;
  extensionName: string;
  projectDir?: string;
  onClose: () => void;
};
⋮----
export const ExtensionSettingsDialog = (
⋮----
// Load component definition and initial config on mount
⋮----
const loadConfig = async () =>
⋮----
title=
⋮----
cancelButtonText=

================
File: src/renderer/src/components/settings/ExtensionsSettings.tsx
================
import { AvailableExtension, InstalledExtension, ProjectData, SettingsData } from '@common/types';
import { Activity, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useMount } from '@reactuses/core';
import { useTranslation } from 'react-i18next';
import { FaChevronDown, FaChevronLeft, FaChevronRight, FaPlus, FaSearch, FaSync, FaTrash } from 'react-icons/fa';
import { AIDER_DESK_EXTENSIONS_REPO_URL } from '@common/extensions';
import { clsx } from 'clsx';
⋮----
import { useApi } from '@/contexts/ApiContext';
import { getPathBasename } from '@/utils/path-utils';
import { Button } from '@/components/common/Button';
import { IconButton } from '@/components/common/IconButton';
import { Input } from '@/components/common/Input';
import { showErrorNotification, showSuccessNotification } from '@/utils/notifications';
import { LoadingOverlay } from '@/components/common/LoadingOverlay';
import { Select } from '@/components/common/Select';
import { ExtensionCard } from '@/components/settings/ExtensionCard';
⋮----
// Helper to format repository URL for display
const formatRepositoryName = (url: string): string =>
⋮----
// Check if it's a GitHub URL
⋮----
// Extract pathname and remove leading/trailing slashes
⋮----
// Remove .git suffix if present
⋮----
// Extract only organization/repository (first two segments)
⋮----
// For non-GitHub URLs, return the full URL
⋮----
// If URL parsing fails, return original
⋮----
enum Tab {
  Available = 'available',
  Installed = 'installed',
}
⋮----
enum ExtensionSort {
  MostInstalled = 'most-installed',
  Name = 'name',
}
⋮----
type Props = {
  settings: SettingsData;
  setSettings: (settings: SettingsData) => void;
  openProjects?: ProjectData[];
  selectedProjectContext?: 'global' | string;
};
⋮----
const handleTabChange = (tab: Tab) =>
⋮----
const handleAvailableSortChange = (value: string) =>
⋮----
// Track repositories that are currently being loaded
⋮----
// Context state for project-level extensions
⋮----
// Sync internal profileContext with selectedProjectContext prop
⋮----
// Context navigation logic
const navigateContext = (direction: 'prev' | 'next') =>
⋮----
// Get context display name
const getContextDisplayName = () =>
⋮----
const handleRefresh = async () =>
⋮----
// Re-load installed extensions when project context changes
⋮----
// Load available extensions on initial mount only
⋮----
const handleToggleDisabled = (extensionFilePath: string, isCurrentlyDisabled: boolean) =>
⋮----
const handleUninstall = async (extensionFilePath: string) =>
⋮----
const handleInstall = async (extension: AvailableExtension) =>
⋮----
const handleUpdate = async (extension: AvailableExtension) =>
⋮----
const handleReload = async (extensionFilePath: string) =>
⋮----
const handleAddRepository = async () =>
⋮----
// Update settings first
⋮----
// Fetch extensions from the new repository (fetchOnly=true to bypass all caching)
⋮----
// Add new extensions to existing list
⋮----
const handleRemoveRepository = (repositoryUrl: string) =>
⋮----
// Remove extensions from the removed repository from UI
⋮----
// Filter by context: in Global context, hide project-level extensions
// In project context, show only extensions for that specific project
⋮----
// In global context, only show global extensions (no projectDir)
⋮----
// In project context, only show extensions for this specific project
⋮----
isUninstalling=
⋮----
isReloading=
⋮----
onUpdate=
⋮----
// Group all extensions by repository URL
⋮----
// Add all available extensions to the map
⋮----
// Render extension card with appropriate controls based on installation status
⋮----
// Check if extension is installed in the current context
⋮----
// In global context, only consider global extensions
⋮----
// In project context, only consider extensions for this specific project
⋮----
// If installed, render with available extension data but installed controls
⋮----
// If not installed, render with available extension (AvailableExtension)
⋮----
const handleToggleRepository = (repositoryUrl: string) =>
⋮----
// Render repository section
⋮----
{/* Repository Header */}
⋮----
className=
⋮----
onClick=
⋮----
{/* Show loading repositories that don't have extensions yet */}
⋮----
{/* Tab Navigation */}
⋮----
{/* Search and Filters */}
⋮----
setSelectedCapabilities((prev) =>

================
File: src/renderer/src/components/settings/GeneralSettings.tsx
================
import { useTranslation } from 'react-i18next';
import { Font, FONTS, SettingsData, ProjectStartMode, SuggestionMode, Theme, THEMES, DiffViewMode, MessageViewMode, FileWatchMode } from '@common/types';
import { ChangeEvent } from 'react';
⋮----
import { Checkbox } from '../common/Checkbox';
import { RadioButton } from '../common/RadioButton';
import { Select, Option } from '../common/Select';
import { Section } from '../common/Section';
import { Slider } from '../common/Slider';
import { InfoIcon } from '../common/InfoIcon';
import { Input } from '../common/Input';
⋮----
import { LanguageSelector } from './LanguageSelector';
⋮----
type Props = {
  settings: SettingsData;
  setSettings: (settings: SettingsData) => void;
  onLanguageChange: (language: string) => void;
  onZoomChange: (zoomLevel: number) => void;
  onThemeChange: (themeName: Theme) => void;
  onFontChange: (fontName: Font) => void;
  onFontSizeChange: (fontSize: number) => void;
};
⋮----
const handleStartupModeChange = (mode: ProjectStartMode) =>
⋮----
const handleStartupModeClick = (value: string) =>
⋮----
const handleZoomChange = (value: string) =>
⋮----
const handleNotificationsEnabledChange = (checked: boolean) =>
⋮----
const handleThemeChange = (value: string) =>
⋮----
const handleFontChange = (value: string) =>
⋮----
const handleFontSizeChange = (value: string) =>
⋮----
const handleDiffViewModeChange = (value: string) =>
⋮----
const handleMessageViewModeChange = (value: string) =>
⋮----
const handleShowMessageMapChange = (checked: boolean) =>
⋮----
const handleSuggestionModeChange = (mode: SuggestionMode) =>
⋮----
const handleSuggestionModeClick = (value: string) =>
⋮----
const handleWindowTitleTemplateChange = (e: ChangeEvent<HTMLInputElement>) =>
⋮----
const handleFileWatchModeChange = (value: string) =>
⋮----
const handleSuggestionDelayChange = (delay: number) =>
⋮----
const handleCommandConfirmationChange = (command: keyof typeof settings.promptBehavior.requireCommandConfirmation, checked: boolean) =>
⋮----
label=
⋮----
setSettings(
⋮----
<Checkbox label=
⋮----
onChange=
⋮----
{/* Vim key bindings */}

================
File: src/renderer/src/components/settings/HotkeyConfig.tsx
================
import { useEffect, useRef, useState, KeyboardEvent } from 'react';
import { useTranslation } from 'react-i18next';
import { FaPlus, FaEdit, FaTimes } from 'react-icons/fa';
import { clsx } from 'clsx';
import { useRecordHotkeys } from 'react-hotkeys-hook';
⋮----
import { ConfirmDialog } from '@/components/common/ConfirmDialog';
import { Input } from '@/components/common/Input';
import { IconButton } from '@/components/common/IconButton';
⋮----
type Props = {
  label: string;
  value: string;
  onChange: (value: string) => void;
  size?: 'md' | 'sm';
};
⋮----
type RecordingDialogProps = {
  onSave: (hotkey: string) => void;
  onCancel: () => void;
  existingHotkey?: string;
};
⋮----
const handleConfirm = () =>
⋮----
title=
⋮----
value=
⋮----
// Parse the value into an array of shortcuts
⋮----
const handleAddShortcut = () =>
⋮----
const handleEditShortcut = (index: number) =>
⋮----
const handleClearShortcut = (index: number) =>
⋮----
// If only one shortcut exists, just clear it
⋮----
// Remove the shortcut at the specified index
⋮----
const handleSaveShortcut = (hotkey: string) =>
⋮----
// Edit existing shortcut
⋮----
// Add new shortcut
⋮----
const handleCancelRecording = () =>
⋮----
onClick=

================
File: src/renderer/src/components/settings/HotkeysSettings.tsx
================
import { useTranslation } from 'react-i18next';
import { HotkeyConfig, SettingsData } from '@common/types';
⋮----
import { Section } from '../common/Section';
⋮----
import { HotkeyConfig as HotkeyConfigRow } from './HotkeyConfig';
⋮----
import { Button } from '@/components/common/Button';
import { DEFAULT_HOTKEY_CONFIG } from '@/utils/hotkeys';
⋮----
type Props = {
  settings: SettingsData;
  setSettings: (settings: SettingsData) => void;
};
⋮----
const handleChange = (category: keyof HotkeyConfig, key: string, value: string) =>
⋮----
const handleReset = () =>
⋮----
const renderHotkeyConfig = (category: keyof HotkeyConfig, key: string, label: string) =>
⋮----
return <HotkeyConfigRow key=

================
File: src/renderer/src/components/settings/LanguageSelector.tsx
================
import { ReactCountryFlag } from 'react-country-flag';
import { useTranslation } from 'react-i18next';
⋮----
import Select, { Option } from '../common/Select';
⋮----
import { SUPPORTED_LANGUAGES } from '@/i18n';
⋮----
type Props = {
  language: string;
  onChange: (language: string) => void;
  hideLabel?: boolean;
  size?: 'sm' | 'md' | 'lg';
};
⋮----
export const LanguageSelector = (

================
File: src/renderer/src/components/settings/MemorySettings.tsx
================
import { AGENT_MEMORY_SCOPE_PREFIX, getAgentMemoryScopeId } from '@common/agent';
import { AgentProfile, MemoryEmbeddingProgress, MemoryEmbeddingProgressPhase, MemoryEmbeddingProvider, MemoryEntry, SettingsData } from '@common/types';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { FaFolderOpen, FaRobot, FaTrash } from 'react-icons/fa';
import { useMount } from '@reactuses/core';
⋮----
import { Checkbox } from '../common/Checkbox';
import { Select } from '../common/Select';
import { Section } from '../common/Section';
⋮----
import { useApi } from '@/contexts/ApiContext';
import { Button } from '@/components/common/Button';
import { ConfirmDialog } from '@/components/common/ConfirmDialog';
import { IconButton } from '@/components/common/IconButton';
import { CodeInline } from '@/components/common/CodeInline';
import { Slider } from '@/components/common/Slider';
import { InfoIcon } from '@/components/common/InfoIcon';
⋮----
type Props = {
  settings: SettingsData;
  setSettings: (settings: SettingsData) => void;
};
⋮----
const poll = async () =>
⋮----
const getScopeLabel = (scopeId: string): string =>
⋮----
// agent scopes with memories but no matching isolated profile (e.g. the flag was turned off) stay visible for cleanup
⋮----
const handleDeleteMemory = async () =>
⋮----
const handleDeleteScopeMemories = async () =>
⋮----
const handleEnabledChange = (enabled: boolean) =>
⋮----
const handleModelChange = (model: string) =>
⋮----
const handleProviderChange = (provider: string) =>
⋮----
const handleMaxDistanceChange = (value: number) =>
⋮----
label=
⋮----
<span>
⋮----
onClick=
⋮----
title=
⋮----
confirmButtonText=
⋮----

================
File: src/renderer/src/components/settings/NetworkSettings.tsx
================
import { ChangeEvent, useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { SettingsData, CloudflareTunnelStatus } from '@common/types';
import { clsx } from 'clsx';
import { BiCopy } from 'react-icons/bi';
⋮----
import { Input } from '../common/Input';
import { Section } from '../common/Section';
import { Button } from '../common/Button';
import { Checkbox } from '../common/Checkbox';
import { ChipListInput } from '../common/ChipListInput';
⋮----
import { useApi } from '@/contexts/ApiContext';
import { IconButton } from '@/components/common/IconButton';
import { InfoIcon } from '@/components/common/InfoIcon';
⋮----
type Props = {
  settings: SettingsData;
  setSettings: (settings: SettingsData) => void;
};
⋮----
const handleProxyEnabledChange = (checked: boolean) =>
⋮----
const handleProxyUrlChange = (e: ChangeEvent<HTMLInputElement>) =>
⋮----
const handleNoProxyChange = (e: ChangeEvent<HTMLInputElement>) =>
⋮----
const loadTunnelStatus = async () =>
⋮----
// eslint-disable-next-line no-console
⋮----
const handleServerToggle = async () =>
⋮----
// Start server with current credentials if basic auth is enabled
⋮----
// Stop server
⋮----
// Update settings to reflect the new server state
⋮----
// Handle failure (could add toast notification here)
⋮----
// Handle error (could add toast notification here)
⋮----
const handleReadonlyChange = (checked: boolean) =>
⋮----
const handleReadonlyExtensionUiChange = (checked: boolean) =>
⋮----
const handleBasicAuthEnabledChange = (checked: boolean) =>
⋮----
const handleUsernameChange = (e: ChangeEvent<HTMLInputElement>) =>
⋮----
const handlePasswordChange = (e: ChangeEvent<HTMLInputElement>) =>
⋮----
const handleCorsEnabledChange = (checked: boolean) =>
⋮----
const handleAddCorsOrigin = (origin: string) =>
⋮----
const handleRemoveCorsOrigin = (origin: string) =>
⋮----
const handleTunnelToggle = async () =>
⋮----
// Reload tunnel status
⋮----
// eslint-disable-next-line no-console
⋮----
const getButtonText = () =>
⋮----
const getTunnelButtonText = () =>
⋮----
<Checkbox label=
⋮----
placeholder=
⋮----
label=
⋮----
addLabel=
⋮----
<span className=
⋮----
onClick=
⋮----
await api.writeToClipboard(tunnelStatus.url!);
⋮----
// eslint-disable-next-line no-console
console.error('Failed to copy URL:', error);
⋮----
tooltip=

================
File: src/renderer/src/components/settings/SettingsDialog.tsx
================


================
File: src/renderer/src/components/settings/SettingsPage.tsx
================
import { McpServersData, ProjectData, ProviderProfile, SettingsData } from '@common/types';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { isEqual } from 'lodash';
import { useTranslation } from 'react-i18next';
import { useHotkeys } from 'react-hotkeys-hook';
⋮----
import { Settings } from '@/pages/Settings';
import { useSaveSettings, setFont, setFontSize, setTheme, useSettingsStore } from '@/stores/settingsStore';
import { useAgents } from '@/contexts/AgentsContext';
import { ModalOverlayLayout } from '@/components/common/ModalOverlayLayout';
import { useApi } from '@/contexts/ApiContext';
import { Button } from '@/components/common/Button';
import { showErrorNotification, showSuccessNotification } from '@/utils/notifications';
⋮----
type Props = {
  onClose: () => void;
  initialPageId?: string;
  initialOptions?: Record<string, unknown>;
  openProjects?: ProjectData[];
  onShowLogs?: () => void;
};
⋮----
const loadProviders = async () =>
⋮----
// eslint-disable-next-line no-console
⋮----
const loadMcpServers = async () =>
⋮----
// eslint-disable-next-line no-console
⋮----
const performSave = async (): Promise<boolean> =>
⋮----
// eslint-disable-next-line no-console
⋮----
// Save MCP server changes
⋮----
// eslint-disable-next-line no-console
⋮----
// Save agent profile changes
⋮----
// Find profiles that were added, updated, or deleted
⋮----
// Handle deleted profiles
⋮----
// Handle added and updated profiles
⋮----
// New profile
⋮----
// Updated profile - check if it actually changed
⋮----
// Update profile order if needed
⋮----
// eslint-disable-next-line no-console
⋮----
// eslint-disable-next-line no-console
⋮----
const handleSave = () =>
⋮----
const handleLanguageChange = (language: string) =>
⋮----
const handleZoomChange = (zoomLevel: number) =>
⋮----
<ModalOverlayLayout title=

================
File: src/renderer/src/components/settings/TaskSettings.tsx
================
import { useCallback } from 'react';
import { Mode, SettingsData, ContextCompactionType, Model, WorkingMode } from '@common/types';
import { useTranslation } from 'react-i18next';
import { AiFillFolderOpen } from 'react-icons/ai';
import { IoGitBranch } from 'react-icons/io5';
import { getProviderModelId } from '@common/agent';
⋮----
import { Checkbox } from '../common/Checkbox';
import { Section } from '../common/Section';
import { InfoIcon } from '../common/InfoIcon';
import { Select, Option } from '../common/Select';
import { Slider } from '../common/Slider';
import { ModelSelectorWrapper } from '../common/ModelSelectorWrapper';
import { ItemConfig, ItemSelector } from '../common/ItemSelector';
import { ChipListInput } from '../common/ChipListInput';
⋮----
import { useModes } from '@/hooks/useModes';
import { Input } from '@/components/common/Input';
import { Tooltip } from '@/components/ui/Tooltip';
⋮----
type Props = {
  settings: SettingsData;
  setSettings: (settings: SettingsData) => void;
};
⋮----
const handleSmartTaskStateChange = (checked: boolean) =>
⋮----
const handleAutoGenerateTaskNameChange = (checked: boolean) =>
⋮----
const handleShowTaskStateActionsChange = (checked: boolean) =>
⋮----
const handleAddSymlinkFolder = (folder: string) =>
⋮----
const handleRemoveSymlinkFolder = (folder: string) =>
⋮----
const handleWorktreePostCreateCommandChange = (command: string) =>
⋮----
const handleCompactingThresholdPercentageChange = (value: number) =>
⋮----
const handleCompactingThresholdTokensChange = (value: string) =>
⋮----
const handleCompactionTypeChange = (value: ContextCompactionType) =>
⋮----
const handleTaskNameModelChange = (model: Model | null) =>
⋮----
const handleTaskStateModelChange = (model: Model | null) =>
⋮----
const handleCommitMessageModelChange = (model: Model | null) =>
⋮----
const handleDefaultTaskModeChange = (mode: string) =>
⋮----
const handleDefaultProjectModeChange = (mode: string) =>
⋮----
const handleWorktreeBranchPrefixChange = (value: string) =>
⋮----
const handleRenameBranchOnNameGenerationChange = (checked: boolean) =>
⋮----
<Section title=
⋮----
<label className="text-xs text-text-primary font-medium">
⋮----
onChange=
⋮----
onClick=

================
File: src/renderer/src/components/settings/VoiceSettings.tsx
================
import { useEffect, useMemo, useState } from 'react';
import { useLocalStorage } from '@reactuses/core';
import { ProviderProfile } from '@common/types';
import {
  DEFAULT_VOICE_SYSTEM_INSTRUCTIONS,
  GeminiProvider,
  GeminiVoiceModel,
  OpenAiProvider,
  OpenAiVoiceModel,
  isGeminiProvider,
  isOpenAiProvider,
} from '@common/agent';
import { useTranslation } from 'react-i18next';
⋮----
import { useModelProviders } from '@/contexts/ModelProviderContext';
import { Section } from '@/components/common/Section';
import { Select, Option } from '@/components/common/Select';
import { Input } from '@/components/common/Input';
import { TextArea } from '@/components/common/TextArea';
import { Slider } from '@/components/common/Slider';
import { InfoIcon } from '@/components/common/InfoIcon';
⋮----
type AudioDeviceOption = {
  label: string;
  value: string;
};
⋮----
type SupportedVoiceProviderName = 'openai' | 'gemini';
⋮----
type LanguageOption = {
  label: string;
  value: string;
};
⋮----
type Props = {
  providers?: ProviderProfile[];
  setProviders?: (providers: ProviderProfile[]) => void;
  initialProviderId?: string;
};
⋮----
const handleSelectProvider = (providerId: string) =>
⋮----
const handleUpdateProfile = (profile: ProviderProfile) =>
⋮----
const handleIdleTimeoutChangeSeconds = (seconds: number) =>
⋮----
// eslint-disable-next-line react-hooks/set-state-in-effect
⋮----
const loadDevices = async () =>
⋮----
const handleDeviceChange = () =>
⋮----
const handleInputDeviceChange = (value: string) =>
⋮----
const handleSystemInstructionsChange = (value: string) =>
⋮----
// eslint-disable-next-line react-hooks/set-state-in-effect
⋮----
const handleOpenAiModelChange = (value: string) =>
⋮----
const handleOpenAiLanguageChange = (value: string) =>
⋮----
const handleGeminiModelChange = (value: string) =>
⋮----
const handleGeminiTemperatureChange = (value: number) =>
⋮----
// eslint-disable-next-line react-hooks/set-state-in-effect
⋮----
const handleModelChange = (value: string) =>
⋮----
const handleIdleTimeoutChange = (value: string) =>
⋮----
const handleTemperatureChange = (value: number) =>
⋮----
<Select label=
⋮----
label=
⋮----
<span>

================
File: src/renderer/src/components/terminal/Terminal.scss
================
.ghostty-terminal-container {
  @apply scrollbar-thin scrollbar-track-bg-primary scrollbar-thumb-bg-secondary-light hover:scrollbar-thumb-bg-tertiary;

  canvas {
    @apply px-3;
  }

  textarea {
    position: fixed !important;
    left: -9999px !important;
    top: -9999px !important;
    opacity: 0 !important;
    resize: none !important;
    border: none !important;
    padding: 0 !important;
    margin: 0 !important;
    outline: none !important;
    color: transparent !important;
    background: transparent !important;
    caret-color: transparent !important;
    text-shadow: none !important;
    -webkit-text-fill-color: transparent !important;
  }
}

================
File: src/renderer/src/components/terminal/Terminal.tsx
================
import { forwardRef, useEffect, useImperativeHandle, useRef, useState, useCallback, ChangeEvent } from 'react';
import { Ghostty, Terminal as GhosttyTerminal, FitAddon } from 'ghostty-web';
import { TerminalData, TerminalExitData } from '@common/types';
import { clsx } from 'clsx';
import { useTranslation } from 'react-i18next';
⋮----
import { useApi } from '@/contexts/ApiContext';
import { setTabPtyId } from '@/stores/terminalStore';
⋮----
export type TerminalRef = {
  focus: () => void;
  clear: () => void;
  resize: () => void;
  getOutput: () => string;
};
⋮----
type Props = {
  sessionKey: string;
  tabId: string;
  baseDir: string;
  taskId: string;
  visible: boolean;
  ptyId: string | null;
  className?: string;
  onClose: () => void;
};
⋮----
const useIsTouchDevice = (): boolean =>
⋮----
const listener = (e: MediaQueryListEvent) =>
⋮----
// Load the ghostty WASM module once per app, not once per terminal instance
⋮----
const loadGhostty = (): Promise<InstanceType<typeof Ghostty>> =>
⋮----
// Initialize ghostty-web WASM once
⋮----
// eslint-disable-next-line no-console
⋮----
// Initialize terminal emulator once - its lifetime is independent of the PTY session
⋮----
// Handle terminal input
⋮----
// Handle terminal resize
⋮----
// Create the PTY process when visible and no session is attached
⋮----
const createTerminal = async () =>
⋮----
// eslint-disable-next-line no-console
⋮----
// Attach to the PTY session: reset the emulator, replay the recent output,
// and only then subscribe to live events so no output is rendered twice
⋮----
const handleTerminalData = (data: TerminalData) =>
⋮----
const handleTerminalExit = (data: TerminalExitData) =>
⋮----
const subscribe = () =>
⋮----
const attach = async () =>
⋮----
// eslint-disable-next-line no-console
⋮----
// A freshly created PTY has an empty buffer - subscribe directly without a replay round trip
⋮----
// Handle resize when visibility changes
⋮----
// Touch input: forward soft keyboard input to the PTY
⋮----
const handleKeyDown = (e: KeyboardEvent) =>
⋮----
const handleBeforeInput = (e: InputEvent) =>
⋮----
const handleTouchInputChange = (e: ChangeEvent<HTMLTextAreaElement>) =>
⋮----
const handleContainerPointerDown = () =>
⋮----
const handleTerminalFocus = () =>
⋮----
const handleTerminalBlur = () =>
⋮----
<div className=

================
File: src/renderer/src/components/terminal/TerminalView.tsx
================
import { useRef, useEffect, forwardRef, useImperativeHandle, useCallback } from 'react';
import { IoMdAdd, IoMdClose, IoMdRemove } from 'react-icons/io';
import { BiCopy } from 'react-icons/bi';
import { clsx } from 'clsx';
import { useTranslation } from 'react-i18next';
⋮----
import { Terminal as TerminalComponent, TerminalRef } from '@/components/terminal/Terminal';
import { IconButton } from '@/components/common/IconButton';
import {
  getSessionKey,
  useTerminalStore,
  useTerminalTabs,
  useActiveTerminalTabId,
  addTerminalTab,
  removeTerminalTab,
  setActiveTerminalTab,
} from '@/stores/terminalStore';
import { useApi } from '@/contexts/ApiContext';
⋮----
export type TerminalViewRef = {
  resize: () => void;
};
⋮----
type Props = {
  baseDir: string;
  taskId: string;
  visible: boolean;
  className?: string;
  onClose: () => void;
  onCopyOutput?: (output: string) => void;
};
⋮----
// Resize all terminal instances
⋮----
// Create a new terminal tab
⋮----
// Close a terminal tab and kill its PTY process
const closeTerminalTab = (tabId: string) =>
⋮----
// Close the tab when its PTY exits (e.g. shell ended via Ctrl+D or exit command)
⋮----
// Ensure there is at least one tab when the terminal view becomes visible
⋮----
// Only react to visibility changes, not to tab count changes
⋮----
// Focus the active terminal when the terminal view becomes visible
⋮----
// Small delay to ensure terminal is rendered
⋮----
// Handle copying terminal output
const handleCopyOutput = () =>
⋮----
<div className=
{/* Tab bar */}
⋮----
onClick=
⋮----
{/* Terminal content */}
⋮----
onClose=

================
File: src/renderer/src/components/ui/Tooltip.tsx
================
import {
  CSSProperties,
  FocusEvent,
  PointerEvent,
  ReactElement,
  Ref,
  ReactNode,
  cloneElement,
  isValidElement,
  useCallback,
  useEffect,
  useRef,
  useState,
  useSyncExternalStore,
} from 'react';
import { clsx } from 'clsx';
⋮----
type TooltipSide = 'top' | 'right' | 'bottom' | 'left';
type TooltipAlign = 'start' | 'center' | 'end';
⋮----
type TooltipState = {
  content: ReactNode;
  anchorEl: Element;
  side: TooltipSide;
  align: TooltipAlign;
  maxWidth: number | string;
};
⋮----
const emitChange = () =>
⋮----
export const showTooltip = (next: Omit<TooltipState, 'side' | 'align'> &
⋮----
export const hideTooltip = (anchorEl?: Element) =>
⋮----
const subscribe = (listener: () => void) =>
⋮----
const getSnapshot = ()
const getServerSnapshot = ()
⋮----
type Placement = {
  top: number;
  left: number;
  side: TooltipSide;
  arrowOffset: number;
};
⋮----
const computePosition = (anchorRect: DOMRect, side: TooltipSide, align: TooltipAlign, bubbleSize:
⋮----
const placementFor = (s: TooltipSide):
⋮----
// Flip to the opposite side when the bubble would overflow the viewport.
⋮----
type ArrowProps = { side: TooltipSide; offset: number };
⋮----
{/* Outer triangle with border color */}
⋮----
{/* Inner triangle with background color to create border effect */}
⋮----
const handleReposition = ()
⋮----
type TooltipProps = {
  content: ReactNode;
  children: ReactNode;
  side?: TooltipSide;
  align?: TooltipAlign;
  delayDuration?: number;
  maxWidth?: number | string;
};
⋮----
export const TooltipProvider = ({ children, ...props }: TooltipPrimitive.TooltipProviderProps) => (
  <TooltipPrimitive.Provider delayDuration={200} skipDelayDuration={100} {...props}>
    <TooltipHost />
    {children}
  </TooltipPrimitive.Provider>
);
⋮----
export const Tooltip = (
⋮----
const showFromEvent = (e: PointerEvent<HTMLElement> | FocusEvent<HTMLElement>) =>
⋮----
const handleChildPointerEnter = (e: PointerEvent<HTMLElement>) =>
⋮----
const handleChildPointerLeave = (e: PointerEvent<HTMLElement>) =>
⋮----
const handleChildFocus = (e: FocusEvent<HTMLElement>) =>
⋮----
const handleChildBlur = (e: FocusEvent<HTMLElement>) =>
⋮----
// Handlers only touch anchorRef at event time; the rule cannot see this through cloneElement's props argument.
// eslint-disable-next-line react-hooks/refs

================
File: src/renderer/src/components/usage/CacheHitRateTrendChart.tsx
================
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts';
import { UsageDataRow } from '@common/types';
⋮----
import { formatDateByGroup, GroupBy } from './utils';
⋮----
type Props = {
  data: UsageDataRow[];
  groupBy: GroupBy;
};
⋮----
const formatPercentage = (value: number) => `$

================
File: src/renderer/src/components/usage/CacheSavingsChart.tsx
================
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts';
import { UsageDataRow, Model } from '@common/types';
import { getProviderModelId } from '@common/agent';
⋮----
import { formatDateByGroup, GroupBy } from './utils';
⋮----
import { useModelProviders } from '@/contexts/ModelProviderContext';
⋮----
type ChartDataPoint = {
  date: string;
  paid: number;
  savings: number;
};
⋮----
type Props = {
  data: UsageDataRow[];
  groupBy: GroupBy;
};
⋮----
const getCacheReadCostBreakdown = (model: Model | undefined):
⋮----
const formatCurrency = (value: number) =>
⋮----
const seriesLabel = (name: string)
⋮----
<Legend formatter=

================
File: src/renderer/src/components/usage/CostBreakdownChart.tsx
================
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts';
import { UsageDataRow } from '@common/types';
⋮----
import { generateColorPalette, formatDateByGroup, GroupBy } from './utils';
⋮----
type CostChartDataPoint = {
  date: string;
  [projectKey: string]: string | number; // Dynamic project keys for stacked data
};
⋮----
[projectKey: string]: string | number; // Dynamic project keys for stacked data
⋮----
type Props = {
  data: UsageDataRow[];
  groupBy: GroupBy;
};
⋮----
// Process data for stacked bar chart (aggregate by day and project)
⋮----
// First pass: collect all projects and aggregate data
⋮----
// Convert to array format for Recharts
⋮----
// Generate consistent colors for projects
const projects = Array.from(projectSet).sort(); // Sort for consistency
⋮----
const formatCurrency = (value: number) =>
⋮----
// Limit legend items to prevent overcrowding
⋮----
{/* Render remaining projects without legend */}

================
File: src/renderer/src/components/usage/MessageBreakdownChart.tsx
================
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts';
import { UsageDataRow } from '@common/types';
⋮----
import { generateColorPalette, formatDateByGroup, GroupBy } from './utils';
⋮----
type MessageChartDataPoint = {
  date: string;
  [projectKey: string]: string | number; // Dynamic project keys for stacked data
};
⋮----
[projectKey: string]: string | number; // Dynamic project keys for stacked data
⋮----
type Props = {
  data: UsageDataRow[];
  groupBy: GroupBy;
};
⋮----
// Process data for stacked bar chart (aggregate by day and project)
⋮----
// First pass: collect all projects and aggregate data
⋮----
dateMap.set(projectDisplayName, currentCount + 1); // Each row is one message
⋮----
// Convert to array format for Recharts
⋮----
// Generate consistent colors for projects
const projects = Array.from(projectSet).sort(); // Sort for consistency
⋮----
const formatCount = (value: number) =>
⋮----
// Limit legend items to prevent overcrowding
⋮----
{/* Render remaining projects without legend */}

================
File: src/renderer/src/components/usage/ModelUsageDistributionChart.tsx
================
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { PieChart, Pie, Cell, ResponsiveContainer, Tooltip, Legend } from 'recharts';
import { UsageDataRow } from '@common/types';
⋮----
import { generateColorPalette } from './utils';
⋮----
type ModelDistributionDataPoint = {
  model: string;
  totalTokens: number;
  cost: number;
  percentage: number;
};
⋮----
type Props = {
  data: UsageDataRow[];
};
⋮----
// Process data for pie chart (aggregate by model)
⋮----
// First pass: aggregate data by model
⋮----
// Calculate total tokens for percentage calculation
⋮----
// Convert to array format for Recharts
⋮----
.sort((a, b) => b.totalTokens - a.totalTokens); // Sort by usage (descending)
⋮----
// Generate consistent colors for models
⋮----
const formatTokens = (value: number) =>
⋮----
const formatCurrency = (value: number) =>
⋮----
const formatPercentage = (value: number) =>
⋮----
<Cell key=
⋮----
// Limit legend items to prevent overcrowding
⋮----
{/** @ts-expect-error: payload has model */}

================
File: src/renderer/src/components/usage/TokenUsageTrendChart.tsx
================
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts';
import { UsageDataRow } from '@common/types';
⋮----
import { formatDateByGroup, GroupBy } from './utils';
⋮----
type ChartDataPoint = {
  date: string;
  inputTokens: number;
  outputTokens: number;
  cacheReadTokens: number;
  cacheWriteTokens: number;
};
⋮----
type Props = {
  data: UsageDataRow[];
  groupBy: GroupBy;
};
⋮----
// Process data for stacked composition chart (aggregate by period)
⋮----
const formatTokens = (value: number) =>
⋮----
const seriesLabel = (name: string) =>

================
File: src/renderer/src/components/usage/UsageDashboard.tsx
================
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { FaSync, FaTable, FaChartBar } from 'react-icons/fa';
import { CgSpinner } from 'react-icons/cg';
import { UsageDataRow } from '@common/types';
import { clsx } from 'clsx';
⋮----
import { Select } from '../common/Select';
import { ModalOverlayLayout } from '../common/ModalOverlayLayout';
⋮----
import { UsageTable } from './UsageTable';
import { TokenUsageTrendChart } from './TokenUsageTrendChart';
import { CostBreakdownChart } from './CostBreakdownChart';
import { MessageBreakdownChart } from './MessageBreakdownChart';
import { ModelUsageDistributionChart } from './ModelUsageDistributionChart';
import { CacheHitRateTrendChart } from './CacheHitRateTrendChart';
import { CacheSavingsChart } from './CacheSavingsChart';
import { GroupBy } from './utils';
⋮----
import { useApi } from '@/contexts/ApiContext';
import { DatePicker } from '@/components/common/DatePicker';
import { MultiSelect } from '@/components/common/MultiSelect';
import { IconButton } from '@/components/common/IconButton';
⋮----
type Props = {
  onClose: () => void;
};
⋮----
enum DatePeriod {
  All = 'all',
  ThisMonth = 'thisMonth',
  Today = 'today',
  Custom = 'custom',
}
⋮----
enum ViewTab {
  Table = 'table',
  Charts = 'charts',
}
⋮----
const handlePeriodChange = (period: DatePeriod) =>
⋮----
// This month
⋮----
const handleDateRangeChange = (update: [Date | null, Date | null]) =>
⋮----
// Treat empty selections as "all selected"
⋮----
const handleRefresh = () =>
⋮----
<ModalOverlayLayout title=
⋮----
label=
⋮----
className=
⋮----
{/* Tab Navigation */}
⋮----
onClick=

================
File: src/renderer/src/components/usage/UsageTable.tsx
================
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { UsageDataRow } from '@common/types';
⋮----
import { formatDateByGroup, GroupBy } from './utils';
⋮----
import { Column, FooterColumn, Table } from '@/components/common/Table';
⋮----
type Props = {
  data: UsageDataRow[];
  groupBy: GroupBy;
};
⋮----
const getCacheHitRate = (row: UsageDataRow) =>

================
File: src/renderer/src/components/usage/utils.ts
================
// Generate a consistent color palette for charts
export const generateColorPalette = (count: number): string[] =>
⋮----
'#3b82f6', // blue
'#ef4444', // red
'#10b981', // green
'#f59e0b', // amber
'#8b5cf6', // purple
'#ec4899', // pink
'#06b6d4', // cyan
'#84cc16', // lime
'#f97316', // orange
'#6366f1', // indigo
⋮----
// Generate additional colors by varying hue
⋮----
const hue = (i * 137.508) % 360; // Golden angle approximation for good distribution
⋮----
export enum GroupBy {
  Year = 'year',
  Month = 'month',
  Day = 'day',
  Hour = 'hour',
}
⋮----
export const formatDateByGroup = (timestamp: string, groupBy: GroupBy): string =>

================
File: src/renderer/src/components/Workspace/CommentsPanel.tsx
================
import { ChangeEvent, memo, useCallback, useState } from 'react';
import { HiPencil, HiTrash } from 'react-icons/hi';
import { RiMenuUnfold4Line } from 'react-icons/ri';
import { useTranslation } from 'react-i18next';
import { useLocalStorage } from '@reactuses/core';
import { clsx } from 'clsx';
⋮----
import { IconButton } from '@/components/common/IconButton';
import { TextArea } from '@/components/common/TextArea';
import { Checkbox } from '@/components/common/Checkbox';
import { Button } from '@/components/common/Button';
⋮----
type PendingComment = {
  id: string;
  filePath: string;
  lineNumber: number;
  comment: string;
};
⋮----
type Props = {
  pendingComments: PendingComment[];
  onRemoveComment: (id: string) => void;
  onUpdateComment: (id: string, comment: string) => void;
  onSubmitAll: () => void;
  isSubmitting: boolean;
  createNewTask: boolean;
  onCreateNewTaskChange: (value: boolean) => void;
};
⋮----
onClick=
⋮----
<div className=
⋮----
{/* Panel header */}
⋮----
icon=
⋮----
onStartEdit=
⋮----
{/* Submit change requests */}
⋮----
<Checkbox label=

================
File: src/renderer/src/components/Workspace/ContextFilesSection.tsx
================
import { ContextFile, Mode, OS, TokensCost } from '@common/types';
import { MouseEvent, memo, useCallback, useEffect, useMemo, useState } from 'react';
import { HiOutlineTrash, HiPlus } from 'react-icons/hi';
import { useTranslation } from 'react-i18next';
⋮----
import { WorkspaceSection } from './WorkspaceSection';
import { EmptyContextInfo } from './EmptyContextInfo';
import { createFileTree } from './types';
⋮----
import type { TreeItem } from './types';
⋮----
import { Tooltip } from '@/components/ui/Tooltip';
import { useFileEditorStore } from '@/stores/fileEditorStore';
⋮----
type Props = {
  mode: Mode;
  baseDir: string;
  taskId: string;
  userContextFiles: ContextFile[];
  isOpen: boolean;
  totalStats: { additions: number; deletions: number };
  fileTokensInfo?: Record<string, TokensCost> | null;
  os: OS | null;
  contextFilesMap: Map<string, ContextFile>;
  showFileDialog: () => void;
  onDropAllFiles: () => void;
  onToggle: () => void;
  onFilePreviewClick?: (filePath: string) => void;
  onDropFile: (item: TreeItem) => (e: MouseEvent<HTMLButtonElement>) => void;
  editMode?: boolean;
  isHidden?: boolean;
  onToggleHidden?: () => void;
  showBorderTop?: boolean;
};
⋮----
const expandFolders = (treeData: Record<string, TreeItem>, files: ContextFile[]) =>
⋮----
const checkChild = (childKey: string | number) =>
⋮----
<Tooltip content=

================
File: src/renderer/src/components/Workspace/ContextFilesSectionInner.tsx
================
// This file is deprecated. Use UserContextFilesSection.tsx instead.

================
File: src/renderer/src/components/Workspace/DiffFileItem.tsx
================
import { useCallback, useEffect, useMemo, useRef, useState, MouseEvent } from 'react';
import { HiChevronDown } from 'react-icons/hi';
import { AnimatePresence, motion } from 'framer-motion';
import { MdOutlineCommit, MdUnfoldLess, MdUnfoldMore } from 'react-icons/md';
import { CgSpinner } from 'react-icons/cg';
import { RiAlertLine } from 'react-icons/ri';
import { DiffViewMode, UpdatedFile } from '@common/types';
import { useTranslation } from 'react-i18next';
import { clsx } from 'clsx';
⋮----
import { Tooltip } from '@/components/ui/Tooltip';
import { Button } from '@/components/common/Button';
import { IconButton } from '@/components/common/IconButton';
import { PierreDiffViewer, PierreLineClickInfo, type DiffComment } from '@/components/common/DiffViewer';
import { useUpdatedFileDiff } from '@/hooks/useUpdatedFileDiff';
import { useUpdatedFileContents } from '@/hooks/useUpdatedFileContents';
⋮----
type Props = {
  file: UpdatedFile;
  index: number;
  baseDir: string;
  taskId: string;
  diffViewMode: DiffViewMode;
  selectedLineNumber?: number | null;
  onLineClick: (lineInfo: PierreLineClickInfo, filePath: string) => void;
  comments?: DiffComment[];
  onEditComment?: (info: { commentId: string; viewportRect: { top: number; left: number } }) => void;
  onRemoveComment?: (commentId: string) => void;
  stickyHeader?: boolean;
};
⋮----
export const DiffFileItem = ({
  file,
  index,
  baseDir,
  taskId,
  diffViewMode,
  selectedLineNumber,
  onLineClick,
  comments,
  onEditComment,
  onRemoveComment,
  stickyHeader,
}: Props) =>
⋮----
// Full file context with expandable unchanged regions, loaded on demand via the header button
⋮----
// Diffs are fetched lazily only when the file item scrolls into the viewport
⋮----
handleToggle();
⋮----
tooltip=

================
File: src/renderer/src/components/Workspace/DiffFilesSidebar.tsx
================
import { ContextFile, UpdatedFile } from '@common/types';
import { useCallback, useMemo, useState } from 'react';
import { MdOutlineCommit } from 'react-icons/md';
import { useTranslation } from 'react-i18next';
import { useDebounce, useLocalStorage } from '@reactuses/core';
import { ResizableBox } from 'react-resizable';
⋮----
import { SidebarGroupTree } from './SidebarGroupTree';
import { createFileTree, normalizePath } from './types';
import { UNCOMMITTED_GROUP_ID } from './group-files';
⋮----
import type { DiffModalGroup } from './UpdatedFilesDiffModal';
import type { GroupTree } from './types';
⋮----
import { Input } from '@/components/common/Input';
import { Tooltip } from '@/components/ui/Tooltip';
⋮----
type Props = {
  groups: DiffModalGroup[];
  currentFile: UpdatedFile | null;
  onFileSelect: (file: UpdatedFile) => void;
  selectedFilePaths: Set<string>;
  onToggleFileSelection: (filePath: string, selected: boolean) => void;
  onToggleFolderSelection: (filePaths: string[], selected: boolean) => void;
};
⋮----
export const DiffFilesSidebar = (
⋮----
onChange=
⋮----
currentFilePath=

================
File: src/renderer/src/components/Workspace/EmptyContextInfo.tsx
================
import { AIDER_MODES, Mode } from '@common/types';
import { useTranslation } from 'react-i18next';
⋮----
type Props = {
  mode: Mode;
};

================
File: src/renderer/src/components/Workspace/fileEditorInlineRequests.tsx
================
import { createRoot, type Root } from 'react-dom/client';
import { Decoration, EditorView, WidgetType } from '@codemirror/view';
import { StateField, type Text } from '@codemirror/state';
⋮----
import { InlineChangeRequest } from './InlineChangeRequest';
⋮----
import type { Extension } from '@uiw/react-codemirror';
⋮----
type ChangeRequest = {
  id: string;
  position: number;
  comment: string;
};
⋮----
type Options = {
  changeRequests: ChangeRequest[];
  editLabel: string;
  deleteLabel: string;
  onEdit: (id: string, rect: DOMRect) => void;
  onRemove: (id: string) => void;
};
⋮----
class InlineChangeRequestWidget extends WidgetType
⋮----
constructor(
    private readonly changeRequest: ChangeRequest,
    private readonly editLabel: string,
    private readonly deleteLabel: string,
    private readonly onEdit: Options['onEdit'],
    private readonly onRemove: Options['onRemove'],
)
⋮----
eq(other: WidgetType): boolean
⋮----
toDOM(): HTMLElement
⋮----
onEdit=
⋮----
onRemove=
⋮----
destroy(dom: HTMLElement): void
⋮----
ignoreEvent(): boolean
⋮----
const createDecorations = (document: Text, options: Options) =>
⋮----
export const createInlineChangeRequestsExtension = (options: Options): Extension =>

================
File: src/renderer/src/components/Workspace/FileEditorModal.tsx
================
import { type ComponentProps, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { AiOutlineLoading3Quarters } from 'react-icons/ai';
import { HiOutlineExclamation } from 'react-icons/hi';
import { MdLibraryAddCheck, MdSave } from 'react-icons/md';
import { useHotkeys, useHotkeysContext } from 'react-hotkeys-hook';
import { LanguageDescription } from '@codemirror/language';
import { languages } from '@codemirror/language-data';
import { openSearchPanel } from '@codemirror/search';
import { Prec } from '@codemirror/state';
import { EditorView, GutterMarker, gutter, lineNumbers } from '@codemirror/view';
import { githubDarkInit } from '@uiw/codemirror-theme-github';
import CodeMirror, { type Extension } from '@uiw/react-codemirror';
⋮----
import { remapLineAnchors } from './fileEditorUtils';
import { createInlineChangeRequestsExtension } from './fileEditorInlineRequests';
import { CommentsPanel } from './CommentsPanel';
import { FileEditorTabs } from './FileEditorTabs';
⋮----
import { Button } from '@/components/common/Button';
import { ConfirmDialog } from '@/components/common/ConfirmDialog';
import { DiffLineCommentPanel } from '@/components/common/DiffViewer';
import { ModalOverlayLayout } from '@/components/common/ModalOverlayLayout';
import { useFileEditorStore } from '@/stores/fileEditorStore';
import { useApi } from '@/contexts/ApiContext';
import { showErrorNotification, showSuccessNotification, showWarningNotification } from '@/utils/notifications';
⋮----
type PendingComment = {
  id: string;
  filePath: string;
  lineNumber: number;
  position: number;
  comment: string;
};
⋮----
type ActiveLineInfo = {
  lineNumber: number;
  position: number;
  viewportRect: { top: number; left: number };
};
⋮----
type EditCommentInfo = {
  commentId: string;
  viewportRect: { top: number; left: number };
  initialText: string;
};
⋮----
type TabState = {
  savedContent: string | null;
  editorContent: string | null;
  isLoading: boolean;
  error: string | null;
};
⋮----
type TabViewState = {
  anchor: number;
  head: number;
  scrollTop: number;
};
⋮----
type LineSelectHandler = (lineNumber: number, position: number, rect: DOMRect) => void;
⋮----
const createInitialTabState = (isLoading: boolean): TabState => (
⋮----
class ChangeRequestGutterMarker extends GutterMarker
⋮----
constructor(
    private readonly lineNumber: number,
    private readonly position: number,
    private readonly label: string,
    private readonly onSelect: LineSelectHandler,
)
⋮----
eq(other: GutterMarker): boolean
⋮----
toDOM(): HTMLElement
⋮----
type Props = {
  baseDir: string;
  onClose: () => void;
};
⋮----
const fetchLanguageSupport = async () =>
⋮----
const getLineNumber = (position: number)
const mapPosition = (position: number)
⋮----
const renderLoading = () => (
    <div className="flex items-center justify-center h-full">
      <AiOutlineLoading3Quarters className="w-8 h-8 text-text-muted animate-spin" />
    </div>
  );
⋮----
<ModalOverlayLayout title=
⋮----
title=

================
File: src/renderer/src/components/Workspace/FileEditorTabs.tsx
================
import { type MouseEvent, type ReactNode } from 'react';
import { useTranslation } from 'react-i18next';
import { IoMdClose } from 'react-icons/io';
import { clsx } from 'clsx';
⋮----
export type OpenEditorFile = {
  path: string;
  taskId: string;
};
⋮----
type Props = {
  openFiles: OpenEditorFile[];
  activeFilePath: string | null;
  dirtyPaths: Set<string>;
  actions?: ReactNode;
  onSelect: (path: string) => void;
  onClose: (path: string) => void;
};
⋮----
const handleSelect = () =>
⋮----
const handleCloseClick = (event: MouseEvent<HTMLButtonElement>) =>
⋮----
const handleAuxClick = (event: MouseEvent<HTMLDivElement>) =>
⋮----
className=
⋮----
onMouseDown=
aria-label=

================
File: src/renderer/src/components/Workspace/fileEditorUtils.ts
================
type LineAnchor = {
  position: number;
  lineNumber: number;
};
⋮----
export const remapLineAnchors = <T extends LineAnchor>(
  anchors: T[],
  mapPosition: (position: number) => number,
  getLineNumber: (position: number) => number,
): T[]

================
File: src/renderer/src/components/Workspace/group-files.ts
================
import { UpdatedFile } from '@common/types';
⋮----
import { createFileTree, normalizePath } from './types';
⋮----
import type { DiffModalGroup } from './UpdatedFilesDiffModal';
⋮----
// Sort files to match the sidebar tree order (folders first, alphabetical within each folder, depth-first)
export const sortFilesByTreeOrder = (files: UpdatedFile[]): UpdatedFile[] =>
⋮----
const walk = (nodeId: string): void =>
⋮----
export const groupFilesByCommit = (files: UpdatedFile[]): DiffModalGroup[] =>
⋮----
// Oldest commits first (reverse of backend order), then uncommitted last

================
File: src/renderer/src/components/Workspace/index.ts
================


================
File: src/renderer/src/components/Workspace/InlineChangeRequest.tsx
================
import { type MouseEvent } from 'react';
import { HiPencil, HiTrash } from 'react-icons/hi';
⋮----
type Props = {
  comment: string;
  editLabel: string;
  deleteLabel: string;
  onEdit: (rect: DOMRect) => void;
  onRemove: () => void;
};
⋮----
export const InlineChangeRequest = (
⋮----
const handleEdit = (event: MouseEvent<HTMLButtonElement>) =>
⋮----
const handleRemove = (event: MouseEvent<HTMLButtonElement>) =>
⋮----
const handleMouseDown = (event: MouseEvent<HTMLButtonElement>) =>

================
File: src/renderer/src/components/Workspace/ProjectFilesSection.tsx
================
import { ContextFile, OS, TokensCost } from '@common/types';
import { MouseEvent, memo, useCallback, useMemo, useState } from 'react';
import { HiX } from 'react-icons/hi';
import { BiCollapseVertical, BiExpandVertical } from 'react-icons/bi';
import { MdOutlineSearch, MdOutlineRefresh } from 'react-icons/md';
import { FaGitSquare } from 'react-icons/fa';
import { useTranslation } from 'react-i18next';
import { useDebounce, useLocalStorage } from '@reactuses/core';
import { AnimatePresence, motion } from 'framer-motion';
import { clsx } from 'clsx';
⋮----
import { WorkspaceSection } from './WorkspaceSection';
import { normalizePath, createFileTree } from './types';
⋮----
import type { TreeItem } from './types';
⋮----
import { Tooltip } from '@/components/ui/Tooltip';
import { Input } from '@/components/common/Input';
import { useFileEditorStore } from '@/stores/fileEditorStore';
⋮----
type Props = {
  baseDir: string;
  taskId: string;
  allFiles: string[];
  isOpen: boolean;
  totalStats: { additions: number; deletions: number };
  fileTokensInfo?: Record<string, TokensCost> | null;
  os: OS | null;
  contextFilesMap: Map<string, ContextFile>;
  visitedSections: Set<string>;
  refreshAllFiles: (useGit?: boolean) => Promise<void>;
  onToggle: () => void;
  onDropFile: (item: TreeItem) => (e: MouseEvent<HTMLButtonElement>) => void;
  onAddFile: (item: TreeItem) => (event: MouseEvent<HTMLButtonElement>) => void;
  editMode?: boolean;
  isHidden?: boolean;
  onToggleHidden?: () => void;
  showBorderTop?: boolean;
};
⋮----
// eslint-disable-next-line no-console
⋮----
<FaGitSquare className=
⋮----
onFileDiffClick=

================
File: src/renderer/src/components/Workspace/RulesSection.tsx
================
import { ContextFile } from '@common/types';
import { Activity, useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { MdOutlinePublic, MdOutlineRefresh } from 'react-icons/md';
import { RiRobot2Line } from 'react-icons/ri';
import { VscFileCode } from 'react-icons/vsc';
import { motion } from 'framer-motion';
import { clsx } from 'clsx';
⋮----
import { SectionHeader } from './SectionHeader';
import { SectionLoading } from './SectionLoading';
⋮----
import { Tooltip } from '@/components/ui/Tooltip';
import { TriStateCheckbox } from '@/components/common/TriStateCheckbox';
import { useFileEditorStore } from '@/stores/fileEditorStore';
import { useProjectSettings } from '@/contexts/ProjectSettingsContext';
⋮----
const getFileName = (filePath: string) =>
⋮----
// eslint-disable-next-line no-console
⋮----
className=
⋮----
<SectionLoading label=

================
File: src/renderer/src/components/Workspace/SectionContent.tsx
================
import { ContextFile, OS, TokensCost, UpdatedFile } from '@common/types';
import { Dispatch, MouseEvent, ReactNode, RefObject, SetStateAction } from 'react';
import { motion } from 'framer-motion';
import { useTranslation } from 'react-i18next';
⋮----
import { Tree } from './Tree';
import { SectionLoading } from './SectionLoading';
⋮----
import type { SectionType, TreeItem } from './types';
⋮----
type Props = {
  section: SectionType;
  treeData: Record<string, TreeItem>;
  expandedItems: string[];
  setExpandedItems: Dispatch<SetStateAction<string[]>>;
  contextFilesMap: Map<string, ContextFile>;
  updatedFiles: UpdatedFile[];
  fileTokensInfo?: Record<string, TokensCost> | null;
  os: OS | null;
  searchField?: ReactNode;
  emptyContent?: ReactNode;
  isLoading?: boolean;
  disabledRuleFiles?: string[];
  onToggleRuleFile?: (filePaths: string[], disabled: boolean) => void;
  onFileDiffClick: (file: UpdatedFile) => void;
  onFilePreviewClick?: (filePath: string) => void;
  onAddFileToGit?: (filePath: string) => void;
  addingFilesToGit?: Set<string>;
  onRevertFile: (filePath: string) => void;
  onDropFile: (item: TreeItem) => (e: MouseEvent<HTMLButtonElement>) => void;
  onAddFile: (item: TreeItem) => (event: MouseEvent<HTMLButtonElement>) => void;
  scrollContainerRef?: RefObject<HTMLDivElement | null>;
};
⋮----
<SectionLoading label=

================
File: src/renderer/src/components/Workspace/SectionHeader.tsx
================
import { HiChevronDown } from 'react-icons/hi';
import { motion } from 'framer-motion';
import { clsx } from 'clsx';
import { MdDragIndicator } from 'react-icons/md';
import { RiEyeLine, RiEyeOffLine } from 'react-icons/ri';
⋮----
import type { SectionType } from './types';
⋮----
type Props = {
  section: SectionType;
  title: string;
  count: number;
  totalCount?: number;
  isOpen: boolean;
  totalStats: { additions: number; deletions: number };
  actions?: React.ReactNode;
  onToggle: () => void;
  editMode?: boolean;
  isHidden?: boolean;
  onToggleHidden?: () => void;
};
⋮----
className=

================
File: src/renderer/src/components/Workspace/SectionLoading.tsx
================
import { CgSpinner } from 'react-icons/cg';
⋮----
type Props = {
  label: string;
};
⋮----
export const SectionLoading = ({ label }: Props) => (
  <div className="absolute inset-0 flex items-center justify-center">
    <div className="flex items-center gap-1.5 text-2xs text-text-secondary">
      <CgSpinner className="w-4 h-4 text-text-muted animate-spin" />
      {label}
    </div>
  </div>
);

================
File: src/renderer/src/components/Workspace/SidebarGroupTree.tsx
================
import { UpdatedFile } from '@common/types';
import { MouseEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { HiChevronDown, HiChevronRight } from 'react-icons/hi';
import { RiAlertLine } from 'react-icons/ri';
import { useTranslation } from 'react-i18next';
import { useTree } from '@headless-tree/react';
import { hotkeysCoreFeature, propMemoizationFeature, syncDataLoaderFeature } from '@headless-tree/core';
import { clsx } from 'clsx';
⋮----
import { normalizePath, INDENT_PX } from './types';
⋮----
import type { TreeItem, GroupTree } from './types';
import type { ItemInstance } from '@headless-tree/core';
⋮----
import { Tooltip } from '@/components/ui/Tooltip';
import { TriStateCheckbox, type TriState } from '@/components/common/TriStateCheckbox';
⋮----
const collectDescendantFilePaths = (treeData: Record<string, TreeItem>, nodeId: string): string[] =>
⋮----
type SidebarGroupTreeProps = {
  groupTree: GroupTree;
  showCheckboxes: boolean;
  currentFilePath?: string;
  selectedFilePaths: Set<string>;
  onFileSelect: (file: UpdatedFile) => void;
  onToggleFileSelection: (filePath: string, selected: boolean) => void;
  onToggleFolderSelection: (filePaths: string[], selected: boolean) => void;
};
⋮----
// useTree keeps a mutable instance whose results (tree.getItems(), item.isExpanded())
// React Compiler cannot track — memoizing this component defers expand/collapse
// re-commits until an unrelated prop change busts the cache.
⋮----
const handleClick = () =>
⋮----
const handleToggleSelection = () =>
⋮----
const handleChevronClick = (e: MouseEvent<HTMLSpanElement>) =>
⋮----
<div key=

================
File: src/renderer/src/components/Workspace/SkillsSection.tsx
================
import { SkillDefinition } from '@common/types';
import { Activity, useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { HiX } from 'react-icons/hi';
import { MdOutlinePublic, MdOutlineRefresh, MdOutlineSmartToy, MdOutlineTouchApp } from 'react-icons/md';
import { RiPlayCircleLine, RiRocketLine, RiRobot2Line } from 'react-icons/ri';
import { VscFileCode } from 'react-icons/vsc';
import { motion } from 'framer-motion';
import { clsx } from 'clsx';
⋮----
import { SectionHeader } from './SectionHeader';
import { SectionLoading } from './SectionLoading';
⋮----
import { Tooltip } from '@/components/ui/Tooltip';
import { TriStateCheckbox } from '@/components/common/TriStateCheckbox';
import { useApi } from '@/contexts/ApiContext';
import { useProjectSettings } from '@/contexts/ProjectSettingsContext';
import { useFileEditorStore } from '@/stores/fileEditorStore';
⋮----
className=
⋮----
<SectionLoading label=
⋮----
if (skill.dirPath)
openFile(baseDir, `$
⋮----

⋮----
onClick=

================
File: src/renderer/src/components/Workspace/Tree.tsx
================
import { ContextFile, OS, TokensCost, UpdatedFile } from '@common/types';
import { Dispatch, MouseEvent, RefObject, SetStateAction, useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
import { useTree } from '@headless-tree/react';
import { hotkeysCoreFeature, propMemoizationFeature, syncDataLoaderFeature } from '@headless-tree/core';
import { useVirtualizer } from '@tanstack/react-virtual';
⋮----
import { MemoizedTreeItemRenderer } from './TreeItemRenderer';
⋮----
import type { Virtualizer } from '@tanstack/react-virtual';
import type { SectionType, TreeItem } from './types';
⋮----
type SharedRowProps = {
  section: SectionType;
  treeData: Record<string, TreeItem>;
  contextFilesMap: Map<string, ContextFile>;
  updatedFiles: UpdatedFile[];
  fileTokensInfo?: Record<string, TokensCost> | null;
  os: OS | null;
  disabledRuleFiles?: string[];
  onToggleRuleFile?: (filePaths: string[], disabled: boolean) => void;
  onFileDiffClick: (file: UpdatedFile) => void;
  onFilePreviewClick?: (filePath: string) => void;
  onAddFileToGit?: (filePath: string) => void;
  addingFilesToGit?: Set<string>;
  onRevertFile: (filePath: string) => void;
  onDropFile: (item: TreeItem) => (e: MouseEvent<HTMLButtonElement>) => void;
  onAddFile: (item: TreeItem) => (event: MouseEvent<HTMLButtonElement>) => void;
};
⋮----
type TreeProps = SharedRowProps & {
  expandedItems: string[];
  setExpandedItems: Dispatch<SetStateAction<string[]>>;
  scrollContainerRef?: RefObject<HTMLDivElement | null>;
};
⋮----
// useVirtualizer returns a stateful instance with methods, which React Compiler cannot memoize;
// Tree is skipped by the compiler, rows are already memoized via MemoizedTreeItemRenderer.
// eslint-disable-next-line react-hooks/incompatible-library
⋮----
key=
⋮----
itemInstanceProps.ref?.(el);

================
File: src/renderer/src/components/Workspace/TreeItemRenderer.tsx
================
import { ContextFile, OS, TokensCost, UpdatedFile } from '@common/types';
import { memo, MouseEvent, useCallback, useMemo } from 'react';
import { HiChevronDown, HiChevronRight, HiPlus, HiX } from 'react-icons/hi';
import { MdUndo, MdOutlinePublic } from 'react-icons/md';
import { TbPencilOff } from 'react-icons/tb';
import { RiAlertLine, RiRobot2Line } from 'react-icons/ri';
import { VscFileCode } from 'react-icons/vsc';
import { useTranslation } from 'react-i18next';
import { twMerge } from 'tailwind-merge';
⋮----
import { INDENT_PX, normalizePath } from './types';
⋮----
import type { SectionType, TreeItem } from './types';
⋮----
import { Tooltip } from '@/components/ui/Tooltip';
import { TriState, TriStateCheckbox } from '@/components/common/TriStateCheckbox';
⋮----
type Props = {
  item: TreeItem;
  title: string;
  level: number;
  isExpanded: boolean;
  onToggleFolder: (itemId: string | number) => void;
  type: SectionType;
  treeData: Record<string, TreeItem>;
  contextFilesMap: Map<string, ContextFile>;
  updatedFiles: UpdatedFile[];
  fileTokensInfo?: Record<string, TokensCost> | null;
  os: OS | null;
  disabledRuleFiles?: string[];
  onToggleRuleFile?: (filePaths: string[], disabled: boolean) => void;
  onFileDiffClick: (file: UpdatedFile) => void;
  onFilePreviewClick?: (filePath: string) => void;
  onAddFileToGit?: (filePath: string) => void;
  addingFilesToGit?: Set<string>;
  onRevertFile: (filePath: string) => void;
  onDropFile: (item: TreeItem) => (e: MouseEvent<HTMLButtonElement>) => void;
  onAddFile: (item: TreeItem) => (event: MouseEvent<HTMLButtonElement>) => void;
};
⋮----
const getDescendantRuleFilesLocal = (treeData: Record<string, TreeItem>, nodeId: string | number): string[] =>
⋮----
const renderChevron = () =>
⋮----
const renderTitle = () =>
⋮----
const renderFile = () =>
⋮----
<span className=
⋮----

⋮----
<button onClick=

================
File: src/renderer/src/components/Workspace/types.ts
================
import { ContextFile, UpdatedFile } from '@common/types';
⋮----
import type { DiffModalGroup } from './UpdatedFilesDiffModal';
⋮----
export interface TreeItem {
  index: string | number;
  isFolder?: boolean;
  children?: (string | number)[];
  data: string;
  file?: ContextFile;
}
⋮----
export type SectionType = 'updated' | 'project' | 'context' | 'rules' | 'skills';
⋮----
export type GroupTree = {
  group: DiffModalGroup;
  treeData: Record<string, TreeItem>;
  files: UpdatedFile[];
};
⋮----
export const normalizePath = (path: string): string =>
⋮----
export const createFileTree = (files: ContextFile[], rootId = 'root') =>

================
File: src/renderer/src/components/Workspace/UpdatedFilesDiffModal.tsx
================
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { HiChevronLeft, HiChevronRight, HiSparkles, HiViewList } from 'react-icons/hi';
import { MdClose, MdOutlineCommit, MdUndo, MdUnfoldLess, MdUnfoldMore } from 'react-icons/md';
import { CgSpinner } from 'react-icons/cg';
import { useTranslation } from 'react-i18next';
import { useHotkeys } from 'react-hotkeys-hook';
import { useLocalStorage } from '@reactuses/core';
import { DiffViewMode, UpdatedFile } from '@common/types';
import { clsx } from 'clsx';
⋮----
import { sortFilesByTreeOrder } from './group-files';
⋮----
import { ModalOverlayLayout } from '@/components/common/ModalOverlayLayout';
import { IconButton } from '@/components/common/IconButton';
import { ConfirmDialog } from '@/components/common/ConfirmDialog';
import { DiffLineCommentPanel, PierreDiffViewer, PierreLineClickInfo, type DiffComment } from '@/components/common/DiffViewer';
import { CompactSelect } from '@/components/common/CompactSelect';
import { TextArea } from '@/components/common/TextArea';
import { Checkbox } from '@/components/common/Checkbox';
import { Button } from '@/components/common/Button';
import { Tooltip } from '@/components/ui/Tooltip';
import { DiffFileItem } from '@/components/Workspace/DiffFileItem';
import { DiffFilesSidebar } from '@/components/Workspace/DiffFilesSidebar';
import { CommentsPanel } from '@/components/Workspace/CommentsPanel';
import { useApi } from '@/contexts/ApiContext';
import { useCommitChanges } from '@/hooks/useCommitChanges';
import { useUpdatedFileDiff } from '@/hooks/useUpdatedFileDiff';
import { useUpdatedFileContents } from '@/hooks/useUpdatedFileContents';
⋮----
type PendingComment = {
  id: string;
  filePath: string;
  lineNumber: number;
  lineKey: string;
  comment: string;
};
⋮----
export type DiffModalGroup = {
  id: string | null;
  commitHash?: string;
  commitMessage?: string;
  files: UpdatedFile[];
};
⋮----
type Props = {
  groups: DiffModalGroup[];
  initialFile: UpdatedFile | null;
  onClose: () => void;
  baseDir: string;
  taskId: string;
  openInWindowUrl?: string;
  openInWindowTitle?: string;
};
⋮----
// Full file context for the current file (single-file view), loaded on demand via the header button.
// Tracks which file is expanded so switching files collapses automatically.
⋮----
// Lazily fetched diff for the current file (single-file view)
⋮----
// Sort each group's files to match the sidebar tree order so the all-files view,
// navigation and scroll spy agree with the sidebar
⋮----
// Flatten groups into a single file list for navigation
⋮----
// Files eligible for commit: only uncommitted files (committed files are shown for reference only)
⋮----
// Files selected for commit: all committable files minus explicitly deselected ones
⋮----
// Reset selection to all files whenever the committable file list changes (e.g. after a commit)
⋮----
// Find the position of a file in the flat list, matching by path and commitHash.
// When commitHash is undefined/null, the file belongs to the uncommitted group.
⋮----
// Derive current position from the active file object
⋮----
// Cumulative file counts per group for rendering offsets in all-files view
⋮----
// Derive current group by matching commitHash — uncommitted when commitHash is absent
⋮----
// eslint-disable-next-line no-console
⋮----
// eslint-disable-next-line no-console
⋮----
// eslint-disable-next-line no-console
⋮----
// Allow empty message only when amending
⋮----
// eslint-disable-next-line no-console
⋮----
// Strip the Electron IPC wrapper prefix so only the actual backend error is shown
⋮----
// Place the file header right below the sticky group header at the
// top of the scroll view (viewport-rect based, so it is stable even
// for deep nesting)
const computeTargetTop = (): number =>
⋮----
// Pin the file header at the top while diffs above it are lazily
// loading (they grow the content and would otherwise push the
// target file down). The pin is cancelled as soon as the user
// scrolls manually.
⋮----
const cancelPin = () =>
⋮----
const pinScroll = () =>
⋮----
// Tracks which file is expanded so switching files collapses automatically.
⋮----
// Sync currentFile when initialFile prop changes (user clicks a different file)
⋮----
// Scroll to the active file when in view-all mode
⋮----
// Scroll spy: update currentFile based on scroll position in all-files view
⋮----
const handleScroll = () =>
⋮----
// Whether data is in flat mode (single group with no commit hashes)
⋮----
// Render a group section header for the all-files view
⋮----
// Skip rendering header for flat mode single group
⋮----
title=
⋮----
{/* Main content area: file sidebar on left (full height), header + diff viewer center, comments panel right */}
⋮----
{/* Group badge in single-file mode */}
⋮----
className=
⋮----
tooltip=
⋮----
{/* Diff viewer with its own scroll - scrollbar right next to the file */}
⋮----
comments=
⋮----
{/* Right side: Comments panel */}
⋮----
{/* Footer */}
⋮----
{/* Commit section */}
⋮----
isGeneratingMessage
⋮----

================
File: src/renderer/src/components/Workspace/UpdatedFilesSection.tsx
================
import { ContextFile, OS, TokensCost, UpdatedFile, UpdatedFilesGroupMode } from '@common/types';
import { Activity, MouseEvent, ReactNode, memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { HiChevronDown } from 'react-icons/hi';
import { MdDragIndicator, MdOutlineDifference, MdOutlineRefresh } from 'react-icons/md';
import { motion } from 'framer-motion';
import { clsx } from 'clsx';
import { useTranslation } from 'react-i18next';
import { TbGitCommit, TbGitPullRequestDraft } from 'react-icons/tb';
import { RiEyeLine, RiEyeOffLine } from 'react-icons/ri';
⋮----
import { createFileTree, normalizePath } from './types';
import { SectionLoading } from './SectionLoading';
import { SectionContent } from './SectionContent';
import { UpdatedFilesDiffModal } from './UpdatedFilesDiffModal';
import { groupFilesByCommit, UNCOMMITTED_GROUP_ID } from './group-files';
⋮----
import type { DiffModalGroup } from './UpdatedFilesDiffModal';
import type { SectionType, TreeItem } from './types';
⋮----
import { Tooltip } from '@/components/ui/Tooltip';
import { ConfirmDialog } from '@/components/common/ConfirmDialog';
import { useApi } from '@/contexts/ApiContext';
import { useProjectSettings } from '@/contexts/ProjectSettingsContext';
import { encodeBaseDir, ROUTES, URL_PARAMS } from '@/utils/routes';
⋮----
interface CommitGroup {
  id: string;
  commitHash?: string;
  commitMessage?: string;
  files: UpdatedFile[];
  additions: number;
  deletions: number;
}
⋮----
type UpdatedSectionHeaderProps = {
  isOpen: boolean;
  title: string;
  totalStats: { additions: number; deletions: number };
  updatedActions: ReactNode;
  onToggle: () => void;
  editMode?: boolean;
  isHidden?: boolean;
  onToggleHidden?: () => void;
};
⋮----
className=
⋮----
// Group files by commit hash; preserve commit order from backend (newest first in array),
// then reverse for display (oldest first). Uncommitted always last.
⋮----
// Extend DiffModalGroup with per-group stats for the section's tree/header UI
⋮----
// Build tree data for each group — use 'root' as root key since SectionContent expects it
⋮----
// Build tree data for flat mode
⋮----
// Auto-expand folder items in each group's tree when tree data changes
⋮----
// Auto-expand folders in flat tree when tree data changes
⋮----
// eslint-disable-next-line no-console
⋮----
// eslint-disable-next-line no-console
⋮----
// eslint-disable-next-line no-console
⋮----
// Optimistically mark the file as tracked; authoritative data arrives via the updatedFilesUpdated event
⋮----
// eslint-disable-next-line no-console
⋮----
// eslint-disable-next-line no-console
⋮----
{/* Outer section header */}
⋮----
{/* Content area */}
⋮----
<SectionLoading label=
⋮----
{renderCommitGroupHeader(group, isGroupExpanded)}

                          {isGroupExpanded && tree && (
                            <SectionContent
                              section={'updated' as SectionType}
                              treeData={tree}
                              expandedItems={expanded}
setExpandedItems=
⋮----
onClose=
⋮----
openInWindowTitle=
⋮----
title=

================
File: src/renderer/src/components/Workspace/Workspace.css
================
.context-dimmed {
⋮----
@apply text-text-muted;
⋮----
body.theme-light .context-dimmed {
⋮----
.workspace-root.drag-over {
⋮----
@apply bg-bg-tertiary;
⋮----
@tailwind utilities;

================
File: src/renderer/src/components/Workspace/Workspace.tsx
================
import { ContextFile, Mode, TokensCost } from '@common/types';
import { DragEvent, Fragment, MouseEvent, ReactNode, useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useLocalStorage } from '@reactuses/core';
import { RiMenuUnfold4Line, RiListSettingsLine } from 'react-icons/ri';
import { closestCenter, DndContext, type DragEndEvent, KeyboardSensor, PointerSensor, useSensor, useSensors } from '@dnd-kit/core';
import { SortableContext, useSortable, verticalListSortingStrategy } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { clsx } from 'clsx';
import { MdDone } from 'react-icons/md';
import { AnimatePresence } from 'framer-motion';
⋮----
import { ContextFilesSection } from './ContextFilesSection';
import { UpdatedFilesSection } from './UpdatedFilesSection';
import { ProjectFilesSection } from './ProjectFilesSection';
import { RulesSection } from './RulesSection';
import { SkillsSection } from './SkillsSection';
import { normalizePath } from './types';
⋮----
import type { SectionType, TreeItem } from './types';
⋮----
import { Tooltip } from '@/components/ui/Tooltip';
import { useOS } from '@/hooks/useOS';
import { useApi } from '@/contexts/ApiContext';
import { useProjectSettings } from '@/contexts/ProjectSettingsContext';
⋮----
type SortableSectionWrapperProps = {
  id: SectionType;
  editMode: boolean;
  isHidden: boolean;
  children: ReactNode;
};
⋮----
// eslint-disable-next-line react-hooks/set-state-in-effect

================
File: src/renderer/src/components/Workspace/WorkspaceSection.tsx
================
import { ContextFile, OS, TokensCost, UpdatedFile } from '@common/types';
import { Activity, Dispatch, MouseEvent, ReactNode, SetStateAction } from 'react';
import { motion } from 'framer-motion';
import { clsx } from 'clsx';
⋮----
import { SectionHeader } from './SectionHeader';
import { SectionContent } from './SectionContent';
⋮----
import type { SectionType, TreeItem } from './types';
⋮----
type Props = {
  section: SectionType;
  title: string;
  count: number;
  isOpen: boolean;
  totalStats: { additions: number; deletions: number };
  treeData: Record<string, TreeItem>;
  expandedItems: string[];
  setExpandedItems: Dispatch<SetStateAction<string[]>>;
  contextFilesMap: Map<string, ContextFile>;
  updatedFiles: UpdatedFile[];
  fileTokensInfo?: Record<string, TokensCost> | null;
  os: OS | null;
  actions?: ReactNode;
  searchField?: ReactNode;
  emptyContent?: ReactNode;
  isLoading?: boolean;
  showBorderTop?: boolean;
  disabledRuleFiles?: string[];
  totalRuleCount?: number;
  editMode?: boolean;
  isHidden?: boolean;
  onToggle: () => void;
  onToggleHidden?: () => void;
  onToggleRuleFile?: (filePaths: string[], disabled: boolean) => void;
  onFileDiffClick: (file: UpdatedFile) => void;
  onFilePreviewClick?: (filePath: string) => void;
  onRevertFile: (filePath: string) => void;
  onDropFile: (item: TreeItem) => (e: MouseEvent<HTMLButtonElement>) => void;
  onAddFile: (item: TreeItem) => (event: MouseEvent<HTMLButtonElement>) => void;
};
⋮----
export const WorkspaceSection = ({
  section,
  title,
  count,
  isOpen,
  totalStats,
  treeData,
  expandedItems,
  setExpandedItems,
  contextFilesMap,
  updatedFiles,
  fileTokensInfo,
  os,
  actions,
  searchField,
  emptyContent,
  isLoading,
  showBorderTop = false,
  disabledRuleFiles,
  totalRuleCount,
  editMode,
  isHidden,
  onToggle,
  onToggleHidden,
  onToggleRuleFile,
  onFileDiffClick,
  onFilePreviewClick,
  onRevertFile,
  onDropFile,
  onAddFile,
}: Props) =>
⋮----
className=

================
File: src/renderer/src/components/AutocompletionInput.tsx
================
import { useState, useRef, useEffect, ReactNode, ClipboardEvent, KeyboardEvent } from 'react';
import { useTranslation } from 'react-i18next';
import { createPortal } from 'react-dom';
import { clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
⋮----
import { KeyboardKeys } from '@/constants/keyboardKeys';
⋮----
type Props = {
  value: string;
  suggestions: string[];
  onChange: (value: string, isFromSuggestion: boolean, isMultiSelect?: boolean) => void;
  placeholder?: string;
  className?: string;
  inputClassName?: string;
  label?: ReactNode;
  rightElement?: ReactNode;
  autoFocus?: boolean;
  onPaste?: (pastedText: string) => Promise<boolean>;
  onSubmit?: (isMultiSelect?: boolean) => void;
};
⋮----
const handleKeyDown = (e: KeyboardEvent) =>
⋮----
const handleOnPaste = async (e: ClipboardEvent<HTMLInputElement>) =>
⋮----
// Prevent default immediately to stop browser from processing the paste
⋮----
// If onPaste didn't handle the paste (returned false), manually insert the text
⋮----
zIndex: 1000, // Ensure it's above other elements
⋮----
placeholder=
⋮----
{/* eslint-disable-next-line react-hooks/refs */}

================
File: src/renderer/src/components/CostInfo.tsx
================
import { Mode, TaskData, TokensInfoData } from '@common/types';
import { ReactNode, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { IoChevronDown, IoChevronUp, IoClose } from 'react-icons/io5';
import { MdOutlineRefresh, MdSettingsBackupRestore } from 'react-icons/md';
⋮----
import { Tooltip } from '@/components/ui/Tooltip';
import { TokenUsageBar } from '@/components/TokenUsageBar';
import { ExtensionComponentWrapper } from '@/components/extensions/ExtensionComponentWrapper';
⋮----
type Props = {
  tokensInfo?: TokensInfoData | null;
  aiderTotalCost: number;
  clearMessages?: () => void;
  refreshRepoMap?: () => void;
  resetTask?: () => void;
  maxInputTokens?: number;
  mode: Mode;
  task: TaskData;
  updateTask: (taskId: string, updates: Partial<TaskData>) => void;
};
⋮----
onClick=
⋮----

================
File: src/renderer/src/components/ModelSelector.tsx
================
import { forwardRef, useImperativeHandle, useRef, KeyboardEvent, MouseEvent, useOptimistic, startTransition, useState, useEffect } from 'react';
import { createPortal } from 'react-dom';
import { useTranslation } from 'react-i18next';
import { MdClose, MdKeyboardArrowUp, MdKeyboardReturn } from 'react-icons/md';
import { useDebounce } from '@reactuses/core';
import { twMerge } from 'tailwind-merge';
import { Model, ProviderProfile } from '@common/types';
import { getProviderModelId } from '@common/agent';
import { extractProviderModel } from '@common/utils';
⋮----
import { useClickOutside } from '@/hooks/useClickOutside';
import { useDropdownState } from '@/hooks/useDropdownState';
import { KeyboardKeys } from '@/constants/keyboardKeys';
⋮----
export type ModelSelectorRef = {
  open: (model?: string) => void;
};
⋮----
type Props = {
  className?: string;
  models: Model[];
  selectedModelId?: string | null;
  onChange: (model: Model | null) => void;
  preferredModelIds: string[];
  removePreferredModel?: (modelId: string) => void;
  providers: ProviderProfile[];
  popupPlacement?: 'top' | 'bottom';
  labelOnNull?: string;
  usePortal?: boolean;
};
⋮----
const getProviderName = (providerId: string) =>
⋮----
// Group filtered models by providerId
⋮----
// eslint-disable-next-line react-hooks/set-state-in-effect
⋮----
const onModelSelected = (model: Model | null) =>
⋮----
const onModelSelectorSearchInputKeyDown = (e: KeyboardEvent<HTMLInputElement>) =>
⋮----
// Check if inherit option is selected
⋮----
const handleRemovePreferredModel = (e: MouseEvent) =>
⋮----
placeholder=
⋮----
onChange=
⋮----

⋮----
className=

================
File: src/renderer/src/components/TelemetryInfoDialog.tsx
================
import { useState } from 'react';
import { Trans, useTranslation } from 'react-i18next';
⋮----
import { BaseDialog } from '@/components/common/BaseDialog';
import { Button } from '@/components/common/Button';
import { Checkbox } from '@/components/common/Checkbox';
import { useSaveSettings, useSettingsStore } from '@/stores/settingsStore';
⋮----
const handleOk = async () =>
⋮----
title=
⋮----

================
File: src/renderer/src/components/TokenUsageBar.tsx
================
import { AIDER_MODES, Mode, TaskData, TokensInfoData } from '@common/types';
import { MouseEvent as ReactMouseEvent, TouchEvent as ReactTouchEvent, useCallback, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useDebounceFn } from '@reactuses/core';
import { clsx } from 'clsx';
import { FaExclamationTriangle } from 'react-icons/fa';
⋮----
import { useSettingsStore } from '@/stores/settingsStore';
import { formatHumanReadable } from '@/utils/string-utils';
import { useActiveAgentProfile } from '@/utils/agents';
import { Tooltip } from '@/components/ui/Tooltip';
⋮----
const getDefaultThresholdTokens = (task: TaskData, thresholdConfig:
⋮----
type Props = {
  task: TaskData;
  tokensInfo?: TokensInfoData | null;
  maxInputTokens?: number;
  mode: Mode;
  updateTask: (taskId: string, updates: Partial<TaskData>) => void;
};
⋮----
// eslint-disable-next-line react-hooks/set-state-in-effect
⋮----
className=
⋮----

================
File: src/renderer/src/constants/keyboardKeys.ts
================


================
File: src/renderer/src/contexts/AgentsContext.tsx
================
import { createContext, useContext, useEffect, useMemo, useOptimistic, startTransition, ReactNode, useCallback, useState } from 'react';
import { AgentProfile } from '@common/types';
⋮----
import { useApi } from '@/contexts/ApiContext';
⋮----
export interface AgentsContextType {
  profiles: AgentProfile[];
  loading: boolean;
  error: string | null;
  getProfiles: (projectDir: string) => AgentProfile[];
  createProfile: (profile: AgentProfile, projectDir?: string) => Promise<void>;
  updateProfile: (profile: AgentProfile, projectDir?: string) => Promise<void>;
  deleteProfile: (profileId: string, projectDir?: string) => Promise<void>;
  refreshProfiles: (projectDir?: string) => Promise<void>;
  updateProfilesOrder: (agentProfiles: AgentProfile[]) => Promise<void>;
}
⋮----
interface AgentsProviderProps {
  children: ReactNode;
}
⋮----
// Register event listener for agent profiles updates
⋮----
export const useAgents = (): AgentsContextType =>

================
File: src/renderer/src/contexts/ApiContext.tsx
================
import { createContext, useEffect, useMemo, ReactNode, useContext } from 'react';
import { ApplicationAPI } from '@common/api';
⋮----
import { BrowserApi } from '@/api/browser-api';
⋮----
type Props = { children: ReactNode };
⋮----
export const useOptionalApi = (): ApplicationAPI | undefined
export const useIsReadonlyView = (): boolean
⋮----
export const useApi = (): ApplicationAPI =>

================
File: src/renderer/src/contexts/CommandPaletteContext.tsx
================


================
File: src/renderer/src/contexts/ContextMenuContext.tsx
================
import { createContext, ReactNode, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { ContextMenuParams } from '@common/types';
⋮----
import { useClickOutside } from '@/hooks/useClickOutside';
import { useApi } from '@/contexts/ApiContext';
⋮----
type ContextMenuContextType = {
  showMenu: (x: number, y: number, options: MenuOption[], targetElement?: Element | null) => void;
  hideMenu: () => void;
};
⋮----
export type MenuOption = {
  label: string;
  action: () => void;
  disabled?: boolean;
};
⋮----
type MenuState = {
  x: number;
  y: number;
  isVisible: boolean;
  options: MenuOption[];
  targetElement?: Element | null;
};
⋮----
type Props = {
  children: ReactNode;
};
⋮----
export const useContextMenu = () =>
⋮----
const handleContextMenu = (params: ContextMenuParams) =>
⋮----
// Capture the target element when context menu is triggered
⋮----
// Show copy link option if right-clicked on a link
⋮----
// eslint-disable-next-line no-console
⋮----
// Always show copy option
⋮----
// If there's selected text, copy that
⋮----
// Otherwise, use the stored target element
⋮----
// Handle input elements
⋮----
// Handle other elements with text content
⋮----
// eslint-disable-next-line no-console

================
File: src/renderer/src/contexts/DiffsWorkerPoolContext.tsx
================
import { type ReactNode, useEffect, useMemo } from 'react';
import { WorkerPoolContextProvider, useWorkerPool } from '@pierre/diffs/react';
import { isCodeEditorDarkTheme } from '@common/types';
import Worker from '@pierre/diffs/worker/worker.js?worker';
⋮----
import type { Theme } from '@common/types';
⋮----
import { useSettingsStore } from '@/stores/settingsStore';
⋮----
const workerFactory = ()
⋮----
const getDiffsTheme = (theme: Theme | null)
⋮----
const DiffsThemeSync = () =>
⋮----
type Props = { children: ReactNode };

================
File: src/renderer/src/contexts/DiffViewerContext.tsx
================
import { createContext, useContext } from 'react';
⋮----
import { File } from '@/components/common/DiffViewer/utils';
⋮----
type DiffContextValue = {
  language: string;
  fileStatus: File['type'];
};
⋮----
export const useDiffContext = () =>

================
File: src/renderer/src/contexts/ExtensionApiContext.tsx
================
import { ExtensionDisplayAPI } from '@common/api';
import { createContext, ReactNode, useContext } from 'react';
⋮----
import { useApi } from '@/contexts/ApiContext';
⋮----
type Props = {
  api: ExtensionDisplayAPI;
  children: ReactNode;
};
⋮----
export const useExtensionApi = (): ExtensionDisplayAPI =>

================
File: src/renderer/src/contexts/ExtensionsContext.tsx
================
import { createContext, ReactNode, useContext, useEffect, useMemo } from 'react';
import { UIComponentProps, UIComponents } from '@common/extensions';
import { AgentProfile, TaskData, Message } from '@common/types';
⋮----
import { iconPackStubs, useReactIcons } from '@/utils/extension-icons';
import { useOptionalModelProviders } from '@/contexts/ModelProviderContext';
import { ConfirmDialog } from '@/components/common/ConfirmDialog';
import { LoadingOverlay } from '@/components/common/LoadingOverlay';
import { ModelSelectorWrapper } from '@/components/common/ModelSelectorWrapper';
import { Tooltip } from '@/components/ui/Tooltip';
import { Chip } from '@/components/common/Chip';
import { DatePicker } from '@/components/common/DatePicker';
import { Slider } from '@/components/common/Slider';
import { MultiSelect } from '@/components/common/MultiSelect';
import { RadioButton } from '@/components/common/RadioButton';
import { IconButton } from '@/components/common/IconButton';
import { TextArea } from '@/components/common/TextArea';
import { Select } from '@/components/common/Select';
import { Input } from '@/components/common/Input';
import { Checkbox } from '@/components/common/Checkbox';
import { Button } from '@/components/common/Button';
import { initExtensionLibraryLoader } from '@/utils/extension-library-loader';
import { ModalOverlayLayout } from '@/components/common/ModalOverlayLayout';
import { CodeBlock } from '@/components/common/CodeBlock';
import { ExpandableMessageBlock } from '@/components/message/ExpandableMessageBlock';
⋮----
type ExtensionsContextValue = {
  projectDir?: string;
  task?: TaskData;
  agentProfile?: AgentProfile;
  messages?: Message[];
  message?: Message;
  activateTask?: (taskId: string) => void;
};
⋮----
type Props = ExtensionsContextValue & {
  children: ReactNode;
  activateTask?: (taskId: string) => void;
};
⋮----
// eslint-disable-next-line no-console
⋮----
const useExtensionsContext = (): ExtensionsContextValue =>
⋮----
type ExtensionsHookResult = {
  componentProps: UIComponentProps;
};
⋮----
export const useExtensions = (): ExtensionsHookResult =>

================
File: src/renderer/src/contexts/FileEditorContext.tsx
================
import { createContext, type ReactNode, useCallback, useContext, useEffect, useMemo, useState } from 'react';
import { useLocalStorage } from '@reactuses/core';
⋮----
export type OpenEditorFile = {
  path: string;
  taskId: string;
};
⋮----
type FileEditorContextValue = {
  openFiles: OpenEditorFile[];
  activeFilePath: string | null;
  isEditorOpen: boolean;
  openFile: (path: string, taskId: string) => void;
  closeTab: (path: string) => void;
  closeTabs: (paths: string[]) => void;
  setActiveFile: (path: string) => void;
  openEditor: () => void;
  closeEditor: () => void;
};
⋮----
type Props = { projectDir: string; children: ReactNode };
⋮----
export const useFileEditor = () =>

================
File: src/renderer/src/contexts/McpServersContext.tsx
================
import { createContext, useContext, useEffect, useMemo, useCallback, useState, ReactNode } from 'react';
import { McpServerConfig } from '@common/types';
⋮----
import { useApi } from '@/contexts/ApiContext';
⋮----
export interface McpServersContextType {
  globalServers: Record<string, McpServerConfig>;
  projectServers: Record<string, Record<string, McpServerConfig>>;
  loading: boolean;
  error: string | null;
  getScopedServers: (projectDir?: string) => Record<string, McpServerConfig>;
  getMergedServers: (projectDir?: string) => Record<string, McpServerConfig>;
  refreshServers: () => Promise<void>;
  addServer: (name: string, config: McpServerConfig, projectDir?: string) => Promise<void>;
  updateServer: (oldName: string, name: string, config: McpServerConfig, projectDir?: string) => Promise<void>;
  removeServer: (name: string, projectDir?: string) => Promise<void>;
  replaceServers: (servers: Record<string, McpServerConfig>, projectDir?: string) => Promise<void>;
  reloadServers: (projectDir?: string, force?: boolean) => Promise<void>;
}
⋮----
interface McpServersProviderProps {
  children: ReactNode;
}
⋮----
export const useMcpServers = (): McpServersContextType =>

================
File: src/renderer/src/contexts/ModelProviderContext.tsx
================
import { createContext, ReactNode, useCallback, useContext, useEffect, useMemo, useState, useOptimistic, startTransition } from 'react';
import { Model, ProviderProfile } from '@common/types';
⋮----
import { useApi } from '@/contexts/ApiContext';
⋮----
export type ModelProviderContextType = {
  refresh: () => void;
  models: Model[];
  providers: ProviderProfile[];
  saveProvider: (profile: ProviderProfile) => Promise<void>;
  deleteProvider: (profileId: string) => Promise<void>;
  upsertModel: (providerId: string, modelId: string, model: Model) => Promise<void>;
  deleteModel: (providerId: string, modelId: string) => Promise<void>;
  updateModels: (modelUpdates: Array<{ providerId: string; modelId: string; model: Model }>) => Promise<void>;
  modelsLoading: boolean;
  providersLoading: boolean;
  errors: Record<string, string>;
};
⋮----
type Props = { children: ReactNode };
⋮----
// eslint-disable-next-line no-console
⋮----
// eslint-disable-next-line no-console
⋮----
// Create optimistic updates
⋮----
const unsupportedReadonlyOperation = async (): Promise<void> =>
⋮----
export const useOptionalModelProviders = (): ModelProviderContextType | null =>
⋮----
export const useModelProviders = (): ModelProviderContextType =>

================
File: src/renderer/src/contexts/ProjectSettingsContext.tsx
================
import { ProjectSettings } from '@common/types';
import { createContext, ReactNode, startTransition, useCallback, useContext, useEffect, useMemo, useOptimistic, useState } from 'react';
⋮----
import { useApi } from '@/contexts/ApiContext';
⋮----
type ProjectSettingsContextType = {
  projectSettings: ProjectSettings | null;
  saveProjectSettings: (settings: Partial<ProjectSettings>) => Promise<void>;
};
⋮----
type ProjectSettingsProviderProps = {
  baseDir: string;
  children: ReactNode;
};
⋮----
// Optimistically update the state
⋮----
setProjectSettings(updatedSettings); // Ensure state is in sync with backend
⋮----
// eslint-disable-next-line no-console
⋮----
const loadSettings = async () =>
⋮----
// eslint-disable-next-line no-console
⋮----
export const useProjectSettings = () =>

================
File: src/renderer/src/contexts/ReadonlyApiContext.tsx
================
import { ApplicationAPI } from '@common/api';
import { createContext, ReactNode, useContext, useEffect, useMemo } from 'react';
⋮----
import { ReadonlyBrowserApi } from '@/api/readonly-browser-api';
import { ExtensionApiProvider } from '@/contexts/ExtensionApiContext';
import { ApiContext, ReadonlyViewContext } from '@/contexts/ApiContext';
⋮----
type Props = {
  projectDir: string;
  children: ReactNode;
};
⋮----
export const ReadonlyApiProvider = (
⋮----
export const useReadonlyApi = (): ReadonlyBrowserApi =>

================
File: src/renderer/src/contexts/TasksContext.tsx
================
import { createContext, memo, ReactNode, useCallback, useContext, useEffect, useMemo, useRef } from 'react';
import { usePrevious } from '@reactuses/core';
import { DefaultTaskState, TaskData, TodoItem, ModelsData, isLoadingMessage, Message } from '@common/types';
⋮----
import { setMessages, setTodoItems, setAiderModelsData, updateTaskState, unloadTask, useTaskStore, useTaskLoaded } from '@/stores/taskStore';
import { setProjectTasks, useProjectStore } from '@/stores/projectStore';
import { useApi } from '@/contexts/ApiContext';
import { releaseTaskFiles } from '@/stores/taskFilesStore';
import { cleanupTaskCache } from '@/stores/extensionUIStore';
import { cleanupProcessingResponseMessage, useTaskResponseHandlers } from '@/hooks/useTaskResponseHandlers';
import { useTaskToolHandlers } from '@/hooks/useTaskToolHandlers';
import { useTaskLogHandlers } from '@/hooks/useTaskLogHandlers';
import { useTaskCommandHandlers } from '@/hooks/useTaskCommandHandlers';
import { useTaskDataHandlers } from '@/hooks/useTaskDataHandlers';
import { useTaskMessageHandlers } from '@/hooks/useTaskMessageHandlers';
import { useTaskLifecycleHandlers } from '@/hooks/useTaskLifecycleHandlers';
import { useTaskActions } from '@/hooks/useTaskActions';
⋮----
const TASK_INACTIVITY_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
const INACTIVITY_CHECK_INTERVAL_MS = 60 * 1000; // Check every minute
⋮----
interface TaskEventSubscriberProps {
  baseDir: string;
  taskId: string;
  state?: string;
}
⋮----
export interface TaskContextType {
  loadTask: (taskId: string) => void;
  clearSession: (taskId: string, messagesOnly: boolean) => void;
  resetTask: (taskId: string) => void;
  restartAiderConnector: (taskId: string) => void;
  setMessages: (taskId: string, updateMessages: (prevState: Message[]) => Message[]) => void;
  setTodoItems: (taskId: string, updateTodoItems: (prev: TodoItem[]) => TodoItem[]) => void;
  setAiderModelsData: (taskId: string, modelsData: ModelsData | null) => void;
  answerQuestion: (taskId: string, answer: string) => void;
  interruptResponse: (taskId: string, interruptId?: string) => void;
  updateTaskAgentProfile: (taskId: string, agentProfileId: string, provider: string, model: string) => void;
  refreshAllFiles: (taskId: string, useGit?: boolean) => Promise<void>;
  refreshContextFiles: (taskId: string) => Promise<void>;
  markTaskActive: (taskId: string) => void;
}
⋮----
type Props = {
  baseDir: string;
  tasks: TaskData[];
  activeTaskId?: string | null;
  children: ReactNode;
};
⋮----
// Resync after a socket reconnect: events emitted while disconnected are lost forever, so
// refetch task metadata and reload already-loaded tasks to bring the UI back in sync.
⋮----
// eslint-disable-next-line no-console
⋮----
export const useTask = (): TaskContextType =>

================
File: src/renderer/src/hooks/useAiderConnectorStatus.ts
================
import { useEffect, useMemo, useState } from 'react';
import { AiderConnectorStatus } from '@common/types';
⋮----
import { useApi } from '@/contexts/ApiContext';
⋮----
/**
 * Hook to track Aider connector status.
 *
 * Covers both Python dependencies installation (global) and per-task connector
 * lifecycle (starting-connector → ready). Pass baseDir/taskId to receive
 * task-scoped connector events.
 *
 * Status merge strategy: Python installation progress always takes priority
 * over connector startup, so users never lose visibility into install progress.
 */
export const useAiderConnectorStatus = (baseDir?: string, taskId?: string) =>
⋮----
// Fetch initial Python status
⋮----
// If IPC not available yet, stay idle
⋮----
// Listen for status changes — route to the correct state slot
⋮----
// Merge: Python install progress takes priority over connector startup

================
File: src/renderer/src/hooks/useAudioRecorder.ts
================
import { useCallback, useRef, useState } from 'react';
⋮----
import { useApi } from '@/contexts/ApiContext';
import { useModelProviders } from '@/contexts/ModelProviderContext';
import { createGeminiVoiceProvider, createOpenAIVoiceProvider } from '@/voice';
import { VoiceProvider, VoiceSession, VoiceSessionState } from '@/voice/types';
⋮----
export interface UseAudioRecorderType {
  isRecording: boolean;
  isProcessing: boolean;
  startRecording: () => Promise<void>;
  stopRecording: () => Promise<void>;
  transcription: string;
  error: string | null;
  resetTranscription: () => void;
  voiceAvailable: boolean;
  mediaDevicesAvailable: boolean;
  mediaStream: MediaStream | null;
}
⋮----
export const useAudioRecorder = (): UseAudioRecorderType =>
⋮----
// Future providers can be added here
⋮----
// Create voice session first.
// On macOS this is also where we trigger the OS-level microphone permission prompt.
⋮----
// eslint-disable-next-line no-console
⋮----
// Create and configure voice provider
⋮----
// Start voice session
⋮----
// eslint-disable-next-line no-console

================
File: src/renderer/src/hooks/useBooleanState.ts
================
import { useState, useCallback } from 'react';
⋮----
export const useBooleanState = (initialState = false) =>

================
File: src/renderer/src/hooks/useClickOutside.ts
================
import { useEffect, RefObject } from 'react';
⋮----
type RefType =
  | RefObject<HTMLElement | null>
  | RefObject<HTMLDivElement | null>
  | RefObject<HTMLButtonElement | null>
  | RefObject<HTMLAnchorElement | null>
  | RefObject<HTMLInputElement | null>;
⋮----
export const useClickOutside = (refs: RefType | RefType[], handler: (event: MouseEvent) => void, enabled = true) =>
⋮----
const listener = (event: MouseEvent) =>
⋮----
// Check if the click is outside all provided refs
⋮----
// Ignore clicks on null refs or if the ref doesn't contain the target

================
File: src/renderer/src/hooks/useCommandPaletteHotkeys.ts
================
import { useHotkeys } from 'react-hotkeys-hook';
⋮----
import { useConfiguredHotkeys } from '@/hooks/useConfiguredHotkeys';
import { useCommandPaletteStore } from '@/stores/commandPaletteStore';
⋮----
export const useCommandPaletteHotkeys = () =>

================
File: src/renderer/src/hooks/useCommands.ts
================
import { Command } from '@common/types';
import { useEffect, useState } from 'react';
⋮----
import { useApi } from '@/contexts/ApiContext';
⋮----
export const useCommands = (baseDir: string) =>
⋮----
// Load initial commands
⋮----
// Listen for commands updates

================
File: src/renderer/src/hooks/useCommitChanges.ts
================
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
⋮----
import { useIsCommitting, useCommitStore } from '@/stores/commitStore';
import { useApi } from '@/contexts/ApiContext';
import { showErrorNotification, showSuccessNotification } from '@/utils/notifications';
⋮----
type UseCommitChanges = {
  isCommitting: boolean;
  commit: (message: string, amend: boolean, skipGitHooks: boolean, filePaths?: string[]) => Promise<void>;
  cancelCommit: () => void;
};
⋮----
export const useCommitChanges = (baseDir: string, taskId: string): UseCommitChanges =>
⋮----
// eslint-disable-next-line no-console

================
File: src/renderer/src/hooks/useConfiguredHotkeys.ts
================
import { useMemo } from 'react';
⋮----
import { useSettingsStore } from '@/stores/settingsStore';
import { getHotkeys } from '@/utils/hotkeys';
⋮----
export const useConfiguredHotkeys = () =>

================
File: src/renderer/src/hooks/useCustomModes.ts
================
import { useEffect, useState } from 'react';
⋮----
import type { ModeDefinition } from '@common/types';
⋮----
import { useApi } from '@/contexts/ApiContext';
⋮----
export const useCustomModes = (baseDir?: string) =>

================
File: src/renderer/src/hooks/useDropdownState.ts
================
import { useCallback, useState } from 'react';
⋮----
export interface DropdownStateOptions<T> {
  initialState: T;
  onCloseReset?: Partial<T>;
}
⋮----
export const useDropdownState = <T extends Record<string, unknown>>(

================
File: src/renderer/src/hooks/useEffectiveEnvironmentVariable.ts
================
import { EnvironmentVariable } from '@common/types';
import { useEffect, useState } from 'react';
⋮----
import { useApi } from '@/contexts/ApiContext';
⋮----
type UseEffectiveEnvironmentVariableResult = {
  environmentVariable: EnvironmentVariable | null;
  loading: boolean;
  error: string | null;
};
⋮----
export const useEffectiveEnvironmentVariable = (key: string, baseDir?: string): UseEffectiveEnvironmentVariableResult =>
⋮----
const fetchEnvironmentVariable = async () =>

================
File: src/renderer/src/hooks/useExtensionTools.ts
================
import { useEffect } from 'react';
import { ExtensionToolInfo } from '@common/types';
⋮----
import { useApi } from '@/contexts/ApiContext';
import { useExtensionToolsInfo, useExtensionToolsStore } from '@/stores/extensionToolsStore';
⋮----
export const useExtensionTools = (projectDir?: string): ExtensionToolInfo[] =>

================
File: src/renderer/src/hooks/useIndexedDB.ts
================
import { useState, useEffect, useCallback, useRef } from 'react';
⋮----
import { uiPreferencesDB } from '@/utils/indexedDB';
⋮----
interface UseIndexedDBOptions<T> {
  ttlDays?: number;
  onLoad?: (value: T) => void;
  onExpired?: () => void;
  onError?: (error: Error) => void;
}
⋮----
export const useIndexedDB = <T>(
  storeName: string,
  key: string,
  defaultValue: T,
  options: UseIndexedDBOptions<T> = {},
): [T, (value: T) => Promise<void>, boolean] =>
⋮----
// Clear expired entries first
⋮----
// Load the specific value
⋮----
// eslint-disable-next-line no-console
⋮----
// eslint-disable-next-line no-console
⋮----
// Initialize on mount

================
File: src/renderer/src/hooks/useLmStudioModels.ts
================
import { useEffect, useState } from 'react';
import axios from 'axios';
⋮----
export const useLmStudioModels = (baseUrl: string) =>
⋮----
const loadModels = async () =>
⋮----
const normalized = baseUrl.replace(/\/+$/g, ''); // Remove all trailing slashes

================
File: src/renderer/src/hooks/useModes.ts
================
import { ElementType, useMemo } from 'react';
import { AiOutlineFileSearch } from 'react-icons/ai';
import { CgTerminal } from 'react-icons/cg';
import { FaRegQuestionCircle } from 'react-icons/fa';
import { FiLayers } from 'react-icons/fi';
import { RiRobot2Line } from 'react-icons/ri';
import { GoProjectRoadmap } from 'react-icons/go';
import { TbTargetArrow } from 'react-icons/tb';
import { Mode } from '@common/types';
⋮----
import { useReactIcons, type ReactIconsMap } from '@/utils/extension-icons';
import { useCustomModes } from '@/hooks/useCustomModes';
import { ItemConfig } from '@/components/common/ItemSelector';
⋮----
const getIconComponent = (iconName: string | undefined, iconPacks: ReactIconsMap | null): ElementType =>
⋮----
export const useModes = (baseDir?: string): ItemConfig<Mode>[] =>

================
File: src/renderer/src/hooks/useOllamaModels.ts
================
import { useEffect, useState } from 'react';
import axios from 'axios';
⋮----
export const useOllamaModels = (baseUrl: string) =>
⋮----
const loadModels = async () =>
⋮----
let normalized = baseUrl.replace(/\/+$/, ''); // Remove all trailing slashes

================
File: src/renderer/src/hooks/useOpenRouterModels.ts
================
import { useEffect, useState } from 'react';
⋮----
interface OpenRouterModel {
  id: string;
  // others not needed
}
⋮----
// others not needed
⋮----
interface OpenRouterResponse {
  data: OpenRouterModel[];
}
⋮----
export const useOpenRouterModels = (apiKey: string) =>
⋮----
const fetchModels = async () =>
⋮----
// eslint-disable-next-line no-console

================
File: src/renderer/src/hooks/useOS.ts
================
import { OS } from '@common/types';
import { useEffect, useState } from 'react';
⋮----
import { useApi } from '@/contexts/ApiContext';
⋮----
export const useOS = (): OS | null =>
⋮----
const fetchOS = async () =>
⋮----
// eslint-disable-next-line no-console
⋮----
setOS(null); // Set to null or a default/unknown state in case of error

================
File: src/renderer/src/hooks/useOverlayFocusRestore.ts
================
import { useEffect, useRef } from 'react';
⋮----
import { useOverlayStore } from '@/stores/overlayStore';
⋮----
const isEditableElement = (element: Element | null): element is HTMLElement
⋮----
export const useOverlayRegistration = (id: string, isOpen: boolean) =>
⋮----
export const useOverlayFocusRestore = (focus: () => void, enabled: boolean) =>

================
File: src/renderer/src/hooks/usePaletteCommands.ts
================
import { useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { UI_ACTIONS } from '@common/ui-actions';
⋮----
import { useConfiguredHotkeys } from '@/hooks/useConfiguredHotkeys';
import { invokeAction, registerAction, unregisterAction } from '@/stores/actionsStore';
import { openSettingsPage } from '@/stores/settingsNavigationStore';
import { PaletteItem, PaletteItemType, useCommandPaletteStore } from '@/stores/commandPaletteStore';
import { useTaskSidebarStore } from '@/stores/taskSidebarStore';
⋮----
const buildShortcuts = (hotkeys: ReturnType<typeof useConfiguredHotkeys>): Record<string, string> => (
⋮----
export const usePaletteCommands = () =>

================
File: src/renderer/src/hooks/useParsedContent.ts
================
import { useMemo } from 'react';
⋮----
import { parseMessageContent } from '@/components/message/utils';
⋮----
export const useParsedContent = (
  baseDir: string,
  content: string | null | undefined,
  allFiles: string[],
  renderMarkdown?: boolean,
  renderThinking?: boolean,
  reasoning?: string | null,
) =>

================
File: src/renderer/src/hooks/usePythonInstallStatus.ts
================
// This file was removed — use useAiderConnectorStatus instead

================
File: src/renderer/src/hooks/useRefractorLanguage.ts
================
import { useCallback, useEffect, useState } from 'react';
⋮----
import { refractor } from '@/utils/highlighter';
import { getLangLoader } from '@/utils/refractor-languages';
⋮----
export const useRefractorLanguage = (language: string): boolean =>
⋮----
// Registration may fail if the grammar has unmet dependencies
⋮----
// Suppress the unused variable warning — registeredVersion is the re-render trigger

================
File: src/renderer/src/hooks/useRequestyModels.ts
================
import { useEffect, useState } from 'react';
⋮----
interface OpenRouterModel {
  id: string;
  // others not needed
}
⋮----
// others not needed
⋮----
interface OpenRouterResponse {
  data: OpenRouterModel[];
}
⋮----
export const useRequestyModels = (apiKey: string) =>
⋮----
const fetchModels = async () =>
⋮----
// eslint-disable-next-line no-console

================
File: src/renderer/src/hooks/useResponsive.ts
================
import { useState, useEffect } from 'react';
import { debounce } from 'lodash';
⋮----
export const useResponsive = () =>
⋮----
const checkScreenSize = () =>

================
File: src/renderer/src/hooks/useScrollingPaused.ts
================
import { TouchEvent, UIEvent, useCallback, useState, WheelEvent } from 'react';
⋮----
interface UseScrollingPausedOptions {
  onAutoScroll: () => void;
  threshold?: number;
  bottomThreshold?: number;
}
⋮----
interface UseScrollingPausedReturn {
  scrollingPaused: boolean;
  setScrollingPaused: (paused: boolean) => void;
  scrollToBottom: () => void;
  eventHandlers: {
    onScroll: (e: UIEvent<HTMLDivElement>) => void;
    onWheel: (e: WheelEvent<HTMLDivElement>) => void;
    onTouchStart: (e: TouchEvent<HTMLDivElement>) => void;
    onTouchMove: (e: TouchEvent<HTMLDivElement>) => void;
  };
}
⋮----
export const useScrollingPaused = (
⋮----
// Check if content is smaller than scroll area (no scrollbar needed)
⋮----
// If content is smaller than area, never pause scrolling
⋮----
// Only pause when scrolling up (negative deltaY)
⋮----
// Detect swipe up (touch moving up, so current Y is less than start Y)
⋮----
// threshold to avoid accidental triggers

================
File: src/renderer/src/hooks/useSearchText.tsx
================
import { useState, useRef, useEffect, useCallback, useLayoutEffect } from 'react';
import { useTranslation } from 'react-i18next';
import Mark from 'mark.js';
import { IoIosArrowUp, IoIosArrowDown, IoMdClose } from 'react-icons/io';
import { clsx } from 'clsx';
⋮----
import { Input } from '@/components/common/Input';
import { IconButton } from '@/components/common/IconButton';
⋮----
// Remove highlight from previous element
⋮----
const handleKeyDown = (event: KeyboardEvent) =>
⋮----
// Ignore the event when disabled without preventing default, so the
// enabled (visible) instance can still handle it.
⋮----
// Clear previous search when opening
⋮----
markInstance.current.unmark(); // Remove previous highlights
⋮----
setCurrentElementIndex(-1); // Reset index
⋮----
navigateToElement(0); // Navigate to the first result
⋮----
// eslint-disable-next-line react-hooks/set-state-in-effect
⋮----
// eslint-disable-next-line
⋮----
<div className=

================
File: src/renderer/src/hooks/useSkills.ts
================
import { SkillDefinition } from '@common/types';
import { useEffect, useState } from 'react';
⋮----
import { useApi } from '@/contexts/ApiContext';
⋮----
export const useSkills = (baseDir: string, taskId?: string) =>
⋮----
// Load initial skills
⋮----
// Listen for skills updates

================
File: src/renderer/src/hooks/useTaskActions.ts
================
import { useCallback, useMemo } from 'react';
⋮----
import { updateTaskState, clearSession, setMessages } from '@/stores/taskStore';
import { convertTaskStateMessages } from '@/utils/task-messages';
import { setTaskAllFiles } from '@/stores/taskFilesStore';
import { useProjectStore } from '@/stores/projectStore';
import { getTaskDir } from '@/utils/task-utils';
import { useApi } from '@/contexts/ApiContext';
⋮----
type UseTaskActionsParams = {
  baseDir: string;
};
⋮----
export const useTaskActions = (
⋮----
// eslint-disable-next-line no-console

================
File: src/renderer/src/hooks/useTaskCommandHandlers.ts
================
import { useCallback, useEffect } from 'react';
import { v4 as uuidv4 } from 'uuid';
⋮----
import type { CommandOutputData, CommandOutputMessage, Message } from '@common/types';
⋮----
import { useApi } from '@/contexts/ApiContext';
import { setMessages, touchTaskActivity } from '@/stores/taskStore';
⋮----
const isCommandOutputMessage = (message: Message): message is CommandOutputMessage =>
⋮----
export const useTaskCommandHandlers = (baseDir: string, taskId: string) =>

================
File: src/renderer/src/hooks/useTaskDataHandlers.ts
================
import { useCallback, useEffect } from 'react';
⋮----
import type {
  AutocompletionData,
  ContextFilesUpdatedData,
  ContextInfoData,
  ModelsData,
  QuestionData,
  QueuedPromptsUpdatedData,
  TokensInfoData,
} from '@common/types';
⋮----
import { useApi } from '@/contexts/ApiContext';
import { useProjectStore } from '@/stores/projectStore';
import { setTaskAllFiles, setTaskAutocompletionWords } from '@/stores/taskFilesStore';
import { updateTaskState, setQuestion, setTokensInfo, setAiderModelsData, setQueuedPrompts, touchTaskActivity } from '@/stores/taskStore';
import { getTaskDir } from '@/utils/task-utils';
⋮----
const getTaskDirById = (baseDir: string, taskId: string) =>
⋮----
export const useTaskDataHandlers = (baseDir: string, taskId: string) =>

================
File: src/renderer/src/hooks/useTaskGitStatus.ts
================
import { useCallback, useEffect, useRef, useState } from 'react';
import { TaskGitStatus } from '@common/types';
⋮----
import { useApi } from '@/contexts/ApiContext';
⋮----
type UseTaskGitStatus = {
  taskGitStatus: TaskGitStatus | null;
  refreshStatus: () => void;
};
⋮----
export const useTaskGitStatus = (projectDir: string, taskId: string): UseTaskGitStatus =>

================
File: src/renderer/src/hooks/useTaskLifecycleHandlers.ts
================
import { useCallback, useEffect } from 'react';
⋮----
import type { ClearTaskData } from '@common/types';
⋮----
import { useApi } from '@/contexts/ApiContext';
import { clearSession, setMessages } from '@/stores/taskStore';
⋮----
export const useTaskLifecycleHandlers = (baseDir: string, taskId: string) =>

================
File: src/renderer/src/hooks/useTaskLogHandlers.ts
================
import { useCallback, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { v4 as uuidv4 } from 'uuid';
import { isLogMessage } from '@common/types';
⋮----
import type { LogData, LogMessage, LoadingMessage, Message } from '@common/types';
⋮----
import { useApi } from '@/contexts/ApiContext';
import { setMessages, touchTaskActivity } from '@/stores/taskStore';
⋮----
const isLoadingMessage = (message: Message): message is LoadingMessage =>
⋮----
export const useTaskLogHandlers = (baseDir: string, taskId: string) =>

================
File: src/renderer/src/hooks/useTaskMessageHandlers.ts
================
import { useCallback, useEffect } from 'react';
⋮----
import type { UserMessageData, MessageRemovedData, UserMessage } from '@common/types';
⋮----
import { useApi } from '@/contexts/ApiContext';
import { setMessages, touchTaskActivity } from '@/stores/taskStore';
⋮----
export const useTaskMessageHandlers = (baseDir: string, taskId: string) =>

================
File: src/renderer/src/hooks/useTaskResponseHandlers.ts
================
import { useCallback, useEffect } from 'react';
import { v4 as uuidv4 } from 'uuid';
import { isResponseMessage } from '@common/types';
⋮----
import type { ResponseChunkData, ResponseCompletedData, Message, ReflectedMessage, ResponseMessage } from '@common/types';
⋮----
import { useApi } from '@/contexts/ApiContext';
import { setMessages, touchTaskActivity } from '@/stores/taskStore';
⋮----
export const cleanupProcessingResponseMessage = (taskId: string) =>
⋮----
export const useTaskResponseHandlers = (baseDir: string, taskId: string) =>

================
File: src/renderer/src/hooks/useTaskToolHandlers.ts
================
import { useCallback, useEffect } from 'react';
import { TODO_TOOL_CLEAR_ITEMS, TODO_TOOL_GET_ITEMS, TODO_TOOL_GROUP_NAME, TODO_TOOL_SET_ITEMS, TODO_TOOL_UPDATE_ITEM_COMPLETION } from '@common/tools';
⋮----
import type { TodoItem, ToolData, ToolInputChunkData, ToolMessage } from '@common/types';
⋮----
import { useApi } from '@/contexts/ApiContext';
import { setMessages, setTodoItems, touchTaskActivity } from '@/stores/taskStore';
⋮----
export const useTaskToolHandlers = (baseDir: string, taskId: string) =>
⋮----
// eslint-disable-next-line no-console
⋮----
const createNewToolMessage = (): ToolMessage =>

================
File: src/renderer/src/hooks/useTypingAnimation.ts
================
import { useEffect, useState } from 'react';
⋮----
export const useTypingAnimation = (text: string, typingSpeed: number = 60, pauseDuration: number = 3000) =>
⋮----
// When we reach end, wait before restarting
⋮----
// Letter-by-letter typing animation

================
File: src/renderer/src/hooks/useUpdatedFileContents.ts
================
import { useCallback, useState } from 'react';
import { UpdatedFile } from '@common/types';
⋮----
import { useApi } from '@/contexts/ApiContext';
⋮----
type LoadedContents = {
  key: string;
  data: { oldContent: string; newContent: string };
};
⋮----
/**
 * Lazily fetches full old/new file contents for expandable context diffs on demand.
 * Nothing is fetched until load() is called. Contents are tied to the file they were
 * loaded for — changing the file invalidates them automatically.
 */
export const useUpdatedFileContents = (baseDir: string, taskId: string, file: UpdatedFile | null | undefined) =>
⋮----
// eslint-disable-next-line no-console

================
File: src/renderer/src/hooks/useUpdatedFileDiff.ts
================
import { useCallback, useEffect, useRef, useState } from 'react';
import { UpdatedFile } from '@common/types';
⋮----
import { useApi } from '@/contexts/ApiContext';
⋮----
// Files with more changed lines than this show a "load diff" placeholder instead of auto-fetching
⋮----
type CachedDiff = {
  diff: string;
  timestamp: number;
};
⋮----
type UseUpdatedFileDiffResult = {
  diff: string | null;
  loading: boolean;
  isLarge: boolean;
  load: () => void;
};
⋮----
const fileDiffKey = (file: UpdatedFile): string => `$
⋮----
/**
 * Lazily fetches the diff for an updated file from the backend on demand.
 * Small diffs are fetched automatically; files above LARGE_DIFF_THRESHOLD wait
 * for an explicit load() call (GitHub-style "large file" gate).
 */
export const useUpdatedFileDiff = (baseDir: string, taskId: string, file: UpdatedFile | null | undefined, enabled = true): UseUpdatedFileDiffResult =>
⋮----
// New file (or first mount): reset loaded state. Once a diff is loaded
// for the current key it stays loaded until the file/modal changes.
⋮----
// Already fetched for this key: keep it (no refetch when disabled/toggled)
⋮----
// Large files wait for an explicit user request
⋮----
const fetchDiff = async () =>
⋮----
// eslint-disable-next-line no-console

================
File: src/renderer/src/hooks/useUserMessageNavigation.tsx
================
import { ReactNode, useCallback, useEffect, useMemo, useState } from 'react';
import { MdKeyboardArrowDown, MdKeyboardArrowUp } from 'react-icons/md';
import { useTranslation } from 'react-i18next';
import { clsx } from 'clsx';
⋮----
import { IconButton } from '@/components/common/IconButton';
⋮----
interface UseUserMessageNavigationProps {
  containerRef: React.RefObject<HTMLDivElement | null>;
  userMessageIds: string[];
  scrollToMessageByElement?: (element: HTMLElement) => void;
  scrollToMessageById?: (id: string) => void;
  // Index-based mode (for virtualized lists where off-screen messages are not in the DOM).
  // When both are provided, navigation is computed from item indices and the visible range
  // instead of querying/measuring DOM nodes.
  userMessageIndices?: number[];
  getVisibleRange?: () => { startIndex: number; endIndex: number } | null;
  scrollToIndex?: (index: number) => void;
  alwaysVisible?: boolean;
  buttonClassName?: string;
}
⋮----
// Index-based mode (for virtualized lists where off-screen messages are not in the DOM).
// When both are provided, navigation is computed from item indices and the visible range
// instead of querying/measuring DOM nodes.
⋮----
interface NavigationButton {
  key: string;
  icon: ReactNode;
  onClick: () => void;
  tooltip: string;
  ariaLabel: string;
  disabled: boolean;
  className: string;
}
⋮----
// indices are ascending; pick the largest one strictly above the visible range
⋮----
// indices are ascending; pick the first one strictly below the visible range
⋮----
const handleScroll = () =>

================
File: src/renderer/src/hooks/useVersions.ts
================
import { VersionsInfo } from '@common/types';
import { useCallback, useEffect, useState } from 'react';
⋮----
import { useApi } from '@/contexts/ApiContext';
⋮----
export const useVersions = () =>
⋮----
// eslint-disable-next-line no-console
⋮----
// Optionally set versions to indicate an error or keep them null
⋮----
// eslint-disable-next-line react-hooks/set-state-in-effect

================
File: src/renderer/src/i18n/index.ts
================
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
// Import your translation files
import en from '@common/locales/en.json';
import zh from '@common/locales/zh.json';
import ru from '@common/locales/ru.json';
import ko from '@common/locales/ko.json';
⋮----
// eslint-disable-next-line import/no-named-as-default-member

================
File: src/renderer/src/icons/AlibabaPlanIcon.tsx
================
type Props = {
  width?: number;
  height?: number;
  className?: string;
};

================
File: src/renderer/src/icons/AnthropicCompatibleIcon.tsx
================
type Props = {
  width?: number;
  height?: number;
  className?: string;
};
⋮----
export const AnthropicCompatibleIcon = ({ width = 64, height = 64, className }: Props) => (
  <div
    aria-label="Anthropic Compatible"
    className={`flex items-center justify-center rounded-full ${className || ''}`}
    style={{
      backgroundColor: 'rgb(100, 100, 125)',
      boxShadow: 'rgba(255, 255, 255, 0.1) 0px 0px 0px 1px inset',
      color: 'rgb(255, 255, 255)',
      height: `${height}px`,
      width: `${width}px`,
    }}
  >
    <svg fill="currentColor" fillRule="evenodd" viewBox="0 0 24 24" width={width * 0.7} height={height * 0.7} xmlns="http://www.w3.org/2000/svg">
      <title>Anthropic</title>
      <path d="M13.827 3.52h3.603L24 20h-3.603l-6.57-16.48zm-7.258 0h3.767L16.906 20h-3.674l-1.343-3.461H5.017l-1.344 3.46H0L6.57 3.522zm4.132 9.959L8.453 7.687 6.205 13.48H10.7z"></path>
    </svg>
  </div>
);

================
File: src/renderer/src/icons/AnthropicIcon.tsx
================
type Props = {
  width?: number;
  height?: number;
  className?: string;
};
⋮----
export const AnthropicIcon = ({ width = 64, height = 64, className }: Props) => (
  <div
    aria-label="Anthropic"
    className={`flex items-center justify-center rounded-md ${className || ''}`}
    style={{
      backgroundColor: 'rgb(241, 240, 232)',
      color: 'rgb(20, 20, 19)',
      height: `${height}px`,
      width: `${width}px`,
    }}
  >
    <svg fill="currentColor" fillRule="evenodd" viewBox="0 0 24 24" width={width * 0.75} height={height * 0.75} xmlns="http://www.w3.org/2000/svg">
      <title>Anthropic</title>
      <path d="M13.827 3.52h3.603L24 20h-3.603l-6.57-16.48zm-7.258 0h3.767L16.906 20h-3.674l-1.343-3.461H5.017l-1.344 3.46H0L6.57 3.522zm4.132 9.959L8.453 7.687 6.205 13.48H10.7z"></path>
    </svg>
  </div>
);

================
File: src/renderer/src/icons/AzureIcon.tsx
================
type Props = {
  width?: number;
  height?: number;
  className?: string;
};

================
File: src/renderer/src/icons/BedrockIcon.tsx
================
type Props = {
  width?: number;
  height?: number;
  className?: string;
};
⋮----
export const BedrockIcon = ({ width = 64, height = 64, className }: Props) => (
  <div
    aria-label="Bedrock"
    className={`flex items-center justify-center rounded-md ${className || ''}`}
    style={{
      backgroundImage: 'linear-gradient(45deg, rgb(154, 216, 248), rgb(61, 143, 255), rgb(99, 80, 251))',
      color: 'rgb(255, 255, 255)',
      height: `${height}px`,
      width: `${width}px`,
    }}
  >
    <svg fill="currentColor" fillRule="evenodd" viewBox="0 0 24 24" width={width * 0.75} height={height * 0.75} xmlns="http://www.w3.org/2000/svg">
      <title>Bedrock</title>
      <path d="M13.05 15.513h3.08c.214 0 .389.177.389.394v1.82a1.704 1.704 0 011.296 1.661c0 .943-.755 1.708-1.685 1.708-.931 0-1.686-.765-1.686-1.708 0-.807.554-1.484 1.297-1.662v-1.425h-2.69v4.663a.395.395 0 01-.188.338l-2.69 1.641a.385.385 0 01-.405-.002l-4.926-3.086a.395.395 0 01-.185-.336V16.3L2.196 14.87A.395.395 0 012 14.555L2 14.528V9.406c0-.14.073-.27.192-.34l2.465-1.462V4.448c0-.129.062-.249.165-.322l.021-.014L9.77 1.058a.385.385 0 01.407 0l2.69 1.675a.395.395 0 01.185.336V7.6h3.856V5.683a1.704 1.704 0 01-1.296-1.662c0-.943.755-1.708 1.685-1.708.931 0 1.685.765 1.685 1.708 0 .807-.553 1.484-1.296 1.662v2.311a.391.391 0 01-.389.394h-4.245v1.806h6.624a1.69 1.69 0 011.64-1.313c.93 0 1.685.764 1.685 1.707 0 .943-.754 1.708-1.685 1.708a1.69 1.69 0 01-1.64-1.314H13.05v1.937h4.953l.915 1.18a1.66 1.66 0 01.84-.227c.931 0 1.685.764 1.685 1.707 0 .943-.754 1.708-1.685 1.708-.93 0-1.685-.765-1.685-1.708 0-.346.102-.668.276-.937l-.724-.935H13.05v1.806zM9.973 1.856L7.93 3.122V6.09h-.778V3.604L5.435 4.669v2.945l2.11 1.36L9.712 7.61V5.334h.778V7.83c0 .136-.07.263-.184.335L7.963 9.638v2.081l1.422 1.009-.446.646-1.406-.998-1.53 1.005-.423-.66 1.605-1.055v-1.99L5.038 8.29l-2.26 1.34v1.676l1.972-1.189.398.677-2.37 1.429V14.3l2.166 1.258 2.27-1.368.397.677-2.176 1.311V19.3l1.876 1.175 2.365-1.426.398.678-2.017 1.216 1.918 1.201 2.298-1.403v-5.78l-4.758 2.893-.4-.675 5.158-3.136V3.289L9.972 1.856zM16.13 18.47a.913.913 0 00-.908.92c0 .507.406.918.908.918a.913.913 0 00.907-.919.913.913 0 00-.907-.92zm3.63-3.81a.913.913 0 00-.908.92c0 .508.406.92.907.92a.913.913 0 00.908-.92.913.913 0 00-.908-.92zm1.555-4.99a.913.913 0 00-.908.92c0 .507.407.918.908.918a.913.913 0 00.907-.919.913.913 0 00-.907-.92zM17.296 3.1a.913.913 0 00-.907.92c0 .508.406.92.907.92a.913.913 0 00.908-.92.913.913 0 00-.908-.92z"></path>
    </svg>
  </div>
);

================
File: src/renderer/src/icons/CerebrasIcon.tsx
================
type Props = {
  width?: number;
  height?: number;
  className?: string;
};

================
File: src/renderer/src/icons/ClinePassIcon.tsx
================
type Props = {
  width?: number;
  height?: number;
  className?: string;
};
⋮----
export const ClinePassIcon = ({ width = 64, height = 64, className }: Props) => (
  <div
    aria-label="ClinePass"
    className={`flex items-center justify-center rounded-full ${className || ''}`}
    style={{
      background: '#353d45',
      color: 'rgb(255, 255, 255)',
      height: `${height}px`,
      width: `${width}px`,
    }}
  >
    <svg fill="currentColor" fillRule="evenodd" viewBox="0 0 24 24" width={width * 0.6} height={height * 0.6} xmlns="http://www.w3.org/2000/svg">
      <title>ClinePass</title>
      <path d="M17.035 3.991c2.75 0 4.98 2.24 4.98 5.003v1.667l1.45 2.896a1.01 1.01 0 0 1-.002.909l-1.448 2.864v1.668c0 2.762-2.23 5.002-4.98 5.002H7.074c-2.751 0-4.98-2.24-4.98-5.002V17.33l-1.48-2.855a1.01 1.01 0 0 1-.003-.927l1.482-2.887V8.994c0-2.763 2.23-5.003 4.98-5.003zM8.265 9.6a2.274 2.274 0 0 0-2.274 2.274v4.042a2.274 2.274 0 0 0 4.547 0v-4.042A2.274 2.274 0 0 0 8.265 9.6m7.326 0a2.274 2.274 0 0 0-2.274 2.274v4.042a2.274 2.274 0 1 0 4.548 0v-4.042A2.274 2.274 0 0 0 15.59 9.6z" />
      <path d="M12.054 5.558a2.779 2.779 0 1 0 0-5.558 2.779 2.779 0 0 0 0 5.558" />
    </svg>
  </div>
);

================
File: src/renderer/src/icons/DeepseekIcon.tsx
================
type Props = {
  width?: number;
  height?: number;
  className?: string;
};
⋮----
export const DeepseekIcon = ({ width = 48, height = 48, className }: Props) => (
  <div
    aria-label="DeepSeek"
    className={`flex items-center justify-center rounded-md ${className || ''}`}
    style={{
      backgroundColor: 'rgb(77, 107, 254)',
      color: 'white',
      height: `${height}px`,
      width: `${width}px`,
    }}
  >
    <svg fill="currentColor" fillRule="evenodd" viewBox="0 0 24 24" width={width * 0.75} height={height * 0.75} xmlns="http://www.w3.org/2000/svg">
      <title>DeepSeek</title>
      <path d="M23.748 4.482c-.254-.124-.364.113-.512.234-.051.039-.094.09-.137.136-.372.397-.806.657-1.373.626-.829-.046-1.537.214-2.163.848-.133-.782-.575-1.248-1.247-1.548-.352-.156-.708-.311-.955-.65-.172-.241-.219-.51-.305-.774-.055-.16-.11-.323-.293-.35-.2-.031-.278.136-.356.276-.313.572-.434 1.202-.422 1.84.027 1.436.633 2.58 1.838 3.393.137.093.172.187.129.323-.082.28-.18.552-.266.833-.055.179-.137.217-.329.14a5.526 5.526 0 01-1.736-1.18c-.857-.828-1.631-1.742-2.597-2.458a11.365 11.365 0 00-.689-.471c-.985-.957.13-1.743.388-1.836.27-.098.093-.432-.779-.428-.872.004-1.67.295-2.687.684a3.055 3.055 0 01-.465.137 9.597 9.597 0 00-2.883-.102c-1.885.21-3.39 1.102-4.497 2.623C.082 8.606-.231 10.684.152 12.85c.403 2.284 1.569 4.175 3.36 5.653 1.858 1.533 3.997 2.284 6.438 2.14 1.482-.085 3.133-.284 4.994-1.86.47.234.962.327 1.78.397.63.059 1.236-.03 1.705-.128.735-.156.684-.837.419-.961-2.155-1.004-1.682-.595-2.113-.926 1.096-1.296 2.746-2.642 3.392-7.003.05-.347.007-.565 0-.845-.004-.17.035-.237.23-.256a4.173 4.173 0 001.545-.475c1.396-.763 1.96-2.015 2.093-3.517.02-.23-.004-.467-.247-.588zM11.581 18c-2.089-1.642-3.102-2.183-3.52-2.16-.392.024-.321.471-.235.763.09.288.207.486.371.739.114.167.192.416-.113.603-.673.416-1.842-.14-1.897-.167-1.361-.802-2.5-1.86-3.301-3.307-.774-1.393-1.224-2.887-1.298-4.482-.02-.386.093-.522.477-.592a4.696 4.696 0 011.529-.039c2.132.312 3.946 1.265 5.468 2.774.868.86 1.525 1.887 2.202 2.891.72 1.066 1.494 2.082 2.48 2.914.348.292.625.514.891.677-.802.09-2.14.11-3.054-.614zm1-6.44a.306.306 0 01.415-.287.302.302 0 01.2.288.306.306 0 01-.31.307.303.303 0 01-.304-.308zm3.11 1.596c-.2.081-.399.151-.59.16a1.245 1.245 0 01-.798-.254c-.274-.23-.47-.358-.552-.758a1.73 1.73 0 01.016-.588c.07-.327-.008-.537-.239-.727-.187-.156-.426-.199-.688-.199a.559.559 0 01-.254-.078c-.11-.054-.2-.19-.114-.358.028-.054.16-.186.192-.21.356-.202.767-.136 1.146.016.352.144.618.408 1.001.782.391.451.462.576.685.914.176.265.336.537.445.848.067.195-.019.354-.25.452z"></path>
    </svg>
  </div>
);

================
File: src/renderer/src/icons/GeminiIcon.tsx
================
type Props = {
  width?: number;
  height?: number;
  className?: string;
};
⋮----
export const GeminiIcon = ({ width = 64, height = 64, className }: Props) => (
  <div
    aria-label="Gemini"
    className={`flex items-center justify-center rounded-md ${className || ''}`}
    style={{
      backgroundColor: 'rgb(255, 255, 255)',
      color: 'rgb(255, 255, 255)',
      height: `${height}px`,
      width: `${width}px`,
    }}
  >
    <svg height={width * 0.75} viewBox="0 0 24 24" width={height * 0.75} xmlns="http://www.w3.org/2000/svg">
      <title>Gemini</title>
      <path
        d="M20.616 10.835a14.147 14.147 0 01-4.45-3.001 14.111 14.111 0 01-3.678-6.452.503.503 0 00-.975 0 14.134 14.134 0 01-3.679 6.452 14.155 14.155 0 01-4.45 3.001c-.65.28-1.318.505-2.002.678a.502.502 0 000 .975c.684.172 1.35.397 2.002.677a14.147 14.147 0 014.45 3.001 14.112 14.112 0 013.679 6.453.502.502 0 00.975 0c.172-.685.397-1.351.677-2.003a14.145 14.145 0 013.001-4.45 14.113 14.113 0 016.453-3.678.503.503 0 000-.975 13.245 13.245 0 01-2.003-.678z"
        fill="#3186FF"
      ></path>
      <path
        d="M20.616 10.835a14.147 14.147 0 01-4.45-3.001 14.111 14.111 0 01-3.678-6.452.503.503 0 00-.975 0 14.134 14.134 0 01-3.679 6.452 14.155 14.155 0 01-4.45 3.001c-.65.28-1.318.505-2.002.678a.502.502 0 000 .975c.684.172 1.35.397 2.002.677a14.147 14.147 0 014.45 3.001 14.112 14.112 0 013.679 6.453.502.502 0 00.975 0c.172-.685.397-1.351.677-2.003a14.145 14.145 0 013.001-4.45 14.113 14.113 0 016.453-3.678.503.503 0 000-.975 13.245 13.245 0 01-2.003-.678z"
        fill="url(#lobe-icons-gemini-fill-0)"
      ></path>
      <path
        d="M20.616 10.835a14.147 14.147 0 01-4.45-3.001 14.111 14.111 0 01-3.678-6.452.503.503 0 00-.975 0 14.134 14.134 0 01-3.679 6.452 14.155 14.155 0 01-4.45 3.001c-.65.28-1.318.505-2.002.678a.502.502 0 000 .975c.684.172 1.35.397 2.002.677a14.147 14.147 0 014.45 3.001 14.112 14.112 0 013.679 6.453.502.502 0 00.975 0c.172-.685.397-1.351.677-2.003a14.145 14.145 0 013.001-4.45 14.113 14.113 0 016.453-3.678.503.503 0 000-.975 13.245 13.245 0 01-2.003-.678z"
        fill="url(#lobe-icons-gemini-fill-1)"
      ></path>
      <path
        d="M20.616 10.835a14.147 14.147 0 01-4.45-3.001 14.111 14.111 0 01-3.678-6.452.503.503 0 00-.975 0 14.134 14.134 0 01-3.679 6.452 14.155 14.155 0 01-4.45 3.001c-.65.28-1.318.505-2.002.678a.502.502 0 000 .975c.684.172 1.35.397 2.002.677a14.147 14.147 0 014.45 3.001 14.112 14.112 0 013.679 6.453.502.502 0 00.975 0c.172-.685.397-1.351.677-2.003a14.145 14.145 0 013.001-4.45 14.113 14.113 0 016.453-3.678.503.503 0 000-.975 13.245 13.245 0 01-2.003-.678z"
        fill="url(#lobe-icons-gemini-fill-2)"
      ></path>
      <defs>
        <linearGradient gradientUnits="userSpaceOnUse" id="lobe-icons-gemini-fill-0" x1="7" x2="11" y1="15.5" y2="12">
          <stop stopColor="#08B962"></stop>
          <stop offset="1" stopColor="#08B962" stopOpacity="0"></stop>
        </linearGradient>
        <linearGradient gradientUnits="userSpaceOnUse" id="lobe-icons-gemini-fill-1" x1="8" x2="11.5" y1="5.5" y2="11">
          <stop stopColor="#F94543"></stop>
          <stop offset="1" stopColor="#F94543" stopOpacity="0"></stop>
        </linearGradient>
        <linearGradient gradientUnits="userSpaceOnUse" id="lobe-icons-gemini-fill-2" x1="3.5" x2="17.5" y1="13.5" y2="12">
          <stop stopColor="#FABC12"></stop>
          <stop offset=".46" stopColor="#FABC12" stopOpacity="0"></stop>
        </linearGradient>
      </defs>
    </svg>
  </div>
);

================
File: src/renderer/src/icons/GpustackIcon.tsx
================
import gpustackIcon from './gpustack.png';
⋮----
type Props = {
  width?: number;
  height?: number;
  className?: string;
};
⋮----
export const GpustackIcon = ({ width = 64, height = 64, className }: Props) => (
  <div
    aria-label="GPUStack"
    className={`flex items-center justify-center rounded-md ${className || ''}`}
    style={{
      backgroundColor: 'white',
      boxShadow: 'rgba(0, 0, 0, 0.1) 0px 0px 0px 1px inset',
      color: 'rgb(255, 255, 255)',
      height: `${height}px`,
      width: `${width}px`,
    }}
  >
    <img src={gpustackIcon} width={width} height={height} className={className} alt="GPUStack" />
  </div>
);

================
File: src/renderer/src/icons/GroqIcon.tsx
================
type Props = {
  width?: number;
  height?: number;
  className?: string;
};
⋮----
export const GroqIcon = ({ width = 64, height = 64, className }: Props) => (
  <div
    aria-label="Groq"
    className={`flex items-center justify-center rounded-md ${className || ''}`}
    style={{
      backgroundColor: 'rgb(245, 80, 54)',
      color: 'rgb(255, 255, 255)',
      height: `${height}px`,
      width: `${width}px`,
    }}
  >
    <svg fill="currentColor" fillRule="evenodd" viewBox="0 0 24 24" width={width * 0.75} height={height * 0.75} xmlns="http://www.w3.org/2000/svg">
      <title>Groq</title>
      <path d="M12.036 2c-3.853-.035-7 3-7.036 6.781-.035 3.782 3.055 6.872 6.908 6.907h2.42v-2.566h-2.292c-2.407.028-4.38-1.866-4.408-4.23-.029-2.362 1.901-4.298 4.308-4.326h.1c2.407 0 4.358 1.915 4.365 4.278v6.305c0 2.342-1.944 4.25-4.323 4.279a4.375 4.375 0 01-3.033-1.252l-1.851 1.818A7 7 0 0012.029 22h.092c3.803-.056 6.858-3.083 6.879-6.816v-6.5C18.907 4.963 15.817 2 12.036 2z"></path>
    </svg>
  </div>
);

================
File: src/renderer/src/icons/index.ts
================


================
File: src/renderer/src/icons/KimiPlanIcon.tsx
================
type Props = {
  width?: number;
  height?: number;
  className?: string;
};
⋮----
export const KimiPlanIcon = ({ width = 64, height = 64, className }: Props) => (
  <div
    aria-label="Kimi Plan"
    className={`flex items-center justify-center rounded-md ${className || ''}`}
    style={{
      backgroundColor: '#000000',
      height: `${height}px`,
      width: `${width}px`,
    }}
  >
    <svg
      viewBox="0 0 512 512"
      xmlns="http://www.w3.org/2000/svg"
      fillRule="evenodd"
      clipRule="evenodd"
      strokeLinejoin="round"
      strokeMiterlimit="2"
      width={width}
      height={height}
    >
      <path
        d="M342.065 189.759c1.886-2.42 3.541-4.63 5.289-6.77.81-1.007.74-1.771-.046-2.824-7.58-9.965-8.298-21.028-3.935-32.254 3.275-8.448 10.52-12.406 19.373-13.25 5.52-.521 10.936.046 15.959 2.73 6.596 3.53 10.438 8.912 11.688 16.341.995 5.926.81 11.712-.868 17.452-2.974 10.161-10.277 15.427-20.287 16.758-8.31 1.11-16.734 1.25-25.113 1.817-.648.046-1.308 0-2.06 0z"
        fill="#027aff"
      />
      <path
        d="M321.512 144.254h-50.064l-39.637 90.384h-56.036v-89.99H131v232.868h44.787v-98.103h78.973c13.598 0 26.015-7.927 31.744-20.252v118.355h44.787v-98.103c0-23.342-18.239-42.97-41.523-44.671v-.116h-24.593a45.577 45.577 0 0026.884-24.534l29.453-65.838z"
        fill="#fff"
      />
    </svg>
  </div>
);

================
File: src/renderer/src/icons/LitellmIcon.tsx
================
type Props = {
  width?: number;
  height?: number;
  className?: string;
};
⋮----
export const LitellmIcon = ({ width = 64, height = 64, className }: Props) => (
  <div
    aria-label="LiteLLM"
    className={`flex items-center justify-center rounded-md ${className || ''}`}
    style={{
      backgroundColor: '#181638',
      boxShadow: 'rgba(0, 0, 0, 0.1) 0px 0px 0px 1px inset',
      color: 'rgb(0, 0, 0)',
      height: `${height}px`,
      width: `${width}px`,
    }}
  >
    <div style={{ fontSize: `${width * 0.6}px` }}>🚅</div>
  </div>
);

================
File: src/renderer/src/icons/LlmApiIcon.tsx
================
type Props = {
  width?: number;
  height?: number;
  className?: string;
};
⋮----
export const LlmApiIcon = ({ width = 64, height = 64, className }: Props) => (
  <div
    aria-label="LLM API"
    className={`flex items-center justify-center rounded-md ${className || ''}`}
    style={{
      backgroundColor: '#3578FF',
      height: `${height}px`,
      width: `${width}px`,
    }}
  >
    <svg viewBox="0 0 40 40" width={width} height={height} fill="none" xmlns="http://www.w3.org/2000/svg">
      <title>LLM API</title>
      <path d="M13.4093 18.8597L6.6792 15.0605L13.4093 11.2612L20.1394 15.0605L13.4093 18.8597Z" fill="white" />
      <path
        d="M13.2819 10.7731C13.407 10.7405 13.5415 10.7568 13.657 10.822L20.3875 14.6206C20.5461 14.7101 20.6438 14.879 20.6438 15.0611C20.6437 15.2431 20.546 15.4111 20.3875 15.5006L13.657 19.2992C13.5029 19.3861 13.315 19.3862 13.1609 19.2992L6.43042 15.5006C6.27201 15.4111 6.17424 15.2431 6.17407 15.0611C6.17407 14.879 6.27183 14.7102 6.43042 14.6206L13.1609 10.822L13.2819 10.7731ZM7.70661 15.06L13.4084 18.2794L19.1113 15.06L13.4084 11.8407L7.70661 15.06Z"
        fill="black"
      />
      <path d="M6.6792 15.0608V24.5112L13.4093 28.3104V23.7513V18.86L6.6792 15.0608Z" fill="black" />
      <path
        d="M12.9046 19.1552L7.18393 15.9259V24.2167L12.9046 27.446V19.1552ZM13.9144 28.3105C13.9144 28.49 13.8187 28.6572 13.6636 28.7477C13.5087 28.838 13.3172 28.8391 13.1609 28.751L6.43042 24.9513C6.272 24.8617 6.17411 24.6939 6.17407 24.5118V15.0614C6.17407 14.8818 6.2698 14.7158 6.42487 14.6253C6.57988 14.5349 6.77133 14.5337 6.92758 14.6219L13.657 18.4205C13.8156 18.5101 13.9144 18.6789 13.9144 18.8611V28.3105Z"
        fill="black"
      />
      <path d="M20.1395 15.0608V24.5112L13.4094 28.3104V23.7513V18.86L20.1395 15.0608Z" fill="white" />
      <path
        d="M19.8912 14.622C20.0475 14.5338 20.2388 14.5349 20.3939 14.6254C20.5489 14.7159 20.6447 14.8819 20.6447 15.0615V24.5119C20.6446 24.6941 20.5458 24.8618 20.3872 24.9514L13.6578 28.7511C13.5016 28.8393 13.3101 28.838 13.1551 28.7478C13 28.6572 12.9043 28.4901 12.9043 28.3105V18.8612C12.9043 18.679 13.0021 18.5102 13.1606 18.4206L19.8912 14.622ZM13.9142 27.445L19.6348 24.2167V15.9248L13.9142 19.1553V27.445Z"
        fill="black"
      />
      <path d="M26.8695 18.8597L20.1394 15.0605L26.8695 11.2612L33.5996 15.0605L26.8695 18.8597Z" fill="white" />
      <path
        d="M26.7419 10.7731C26.867 10.7405 27.0014 10.7568 27.1169 10.822L33.8475 14.6206C34.0061 14.7101 34.1038 14.879 34.1038 15.0611C34.1036 15.2431 34.006 15.4111 33.8475 15.5006L27.1169 19.2992C26.9629 19.3861 26.775 19.3862 26.6209 19.2992L19.8904 15.5006C19.732 15.4111 19.6342 15.2431 19.634 15.0611C19.634 14.879 19.7318 14.7102 19.8904 14.6206L26.6209 10.822L26.7419 10.7731ZM21.1666 15.06L26.8684 18.2794L32.5713 15.06L26.8684 11.8407L21.1666 15.06Z"
        fill="black"
      />
      <path d="M33.5997 15.0608V24.5112L26.8696 28.3104V23.7513V18.86L33.5997 15.0608Z" fill="white" />
      <path
        d="M33.3514 14.622C33.5077 14.5338 33.699 14.5349 33.8541 14.6254C34.0091 14.7159 34.1049 14.8819 34.1049 15.0615V24.5119C34.1048 24.6941 34.006 24.8618 33.8474 24.9514L27.118 28.7511C26.9618 28.8393 26.7703 28.838 26.6153 28.7478C26.4602 28.6572 26.3645 28.4901 26.3645 28.3105V18.8612C26.3645 18.679 26.4623 18.5102 26.6208 18.4206L33.3514 14.622ZM27.3744 27.445L33.095 24.2167V15.9248L27.3744 19.1553V27.445Z"
        fill="black"
      />
      <path d="M13.4093 26.4557L6.6792 22.6564L13.4093 18.8572L20.1394 22.6564L13.4093 26.4557Z" fill="white" />
      <path
        d="M13.2819 18.3693C13.407 18.3367 13.5415 18.353 13.657 18.4182L20.3875 22.2168C20.5461 22.3063 20.6438 22.4751 20.6438 22.6573C20.6437 22.8393 20.546 23.0073 20.3875 23.0968L13.657 26.8954C13.5029 26.9823 13.315 26.9824 13.1609 26.8954L6.43042 23.0968C6.27201 23.0073 6.17424 22.8393 6.17407 22.6573C6.17407 22.4752 6.27183 22.3063 6.43042 22.2168L13.1609 18.4182L13.2819 18.3693ZM7.70661 22.6562L13.4084 25.8756L19.1113 22.6562L13.4084 19.4369L7.70661 22.6562Z"
        fill="black"
      />
      <path d="M6.6792 22.657V32.1095L13.4093 35.9088V31.3497V26.4562L6.6792 22.657Z" fill="black" />
      <path
        d="M12.9046 26.7514L7.18393 23.5221V31.8151L12.9046 35.0444V26.7514ZM13.9144 35.9089C13.9144 36.0884 13.8186 36.2544 13.6636 36.345C13.5086 36.4355 13.3173 36.4376 13.1609 36.3494L6.43042 32.5497C6.272 32.4601 6.17412 32.2923 6.17407 32.1103V22.6576C6.17407 22.478 6.2698 22.312 6.42487 22.2215C6.57988 22.1311 6.77133 22.1299 6.92758 22.2181L13.657 26.0167C13.8156 26.1063 13.9144 26.2751 13.9144 26.4573V35.9089Z"
        fill="black"
      />
      <path d="M20.1395 22.657V32.1095L13.4094 35.9088V31.3497V26.4562L20.1395 22.657Z" fill="white" />
      <path
        d="M19.8912 22.2182C20.0475 22.13 20.2388 22.131 20.3939 22.2215C20.5489 22.3121 20.6447 22.4781 20.6447 22.6577V32.1103C20.6446 32.2925 20.5458 32.4603 20.3872 32.5498L13.6578 36.3495C13.5015 36.4378 13.3101 36.4355 13.1551 36.3451C13 36.2545 12.9043 36.0885 12.9043 35.909V26.4574C12.9043 26.2752 13.0021 26.1064 13.1606 26.0168L19.8912 22.2182ZM13.9142 35.0434L19.6348 31.8152V23.521L13.9142 26.7515V35.0434Z"
        fill="black"
      />
      <path d="M26.8695 26.4557L20.1394 22.6564L26.8695 18.8572L33.5996 22.6564L26.8695 26.4557Z" fill="white" />
      <path
        d="M26.7419 18.3693C26.867 18.3367 27.0014 18.353 27.1169 18.4182L33.8475 22.2168C34.0061 22.3063 34.1038 22.4751 34.1038 22.6573C34.1036 22.8393 34.006 23.0073 33.8475 23.0968L27.1169 26.8954C26.9629 26.9823 26.775 26.9824 26.6209 26.8954L19.8904 23.0968C19.732 23.0073 19.6342 22.8393 19.634 22.6573C19.634 22.4752 19.7318 22.3063 19.8904 22.2168L26.6209 18.4182L26.7419 18.3693ZM21.1666 22.6562L26.8684 25.8756L32.5713 22.6562L26.8684 19.4369L21.1666 22.6562Z"
        fill="black"
      />
      <path d="M20.1394 22.657V32.1095L26.8695 35.9088V31.3497V26.4562L20.1394 22.657Z" fill="black" />
      <path
        d="M26.3645 26.7514L20.6439 23.5221V31.8151L26.3645 35.0444V26.7514ZM27.3744 35.9089C27.3744 36.0884 27.2786 36.2544 27.1236 36.345C26.9686 36.4355 26.7772 36.4376 26.6209 36.3494L19.8904 32.5497C19.732 32.4601 19.6341 32.2923 19.634 32.1103V22.6576C19.634 22.478 19.7298 22.312 19.8848 22.2215C20.0398 22.1311 20.2313 22.1299 20.3875 22.2181L27.1169 26.0167C27.2756 26.1063 27.3744 26.2751 27.3744 26.4573V35.9089Z"
        fill="black"
      />
      <path d="M33.5997 22.657V32.1095L26.8696 35.9088V31.3497V26.4562L33.5997 22.657Z" fill="white" />
      <path
        d="M33.3514 22.2182C33.5077 22.13 33.699 22.131 33.8541 22.2215C34.0091 22.3121 34.1049 22.4781 34.1049 22.6577V32.1103C34.1048 32.2925 34.006 32.4603 33.8474 32.5498L27.118 36.3495C26.9617 36.4378 26.7703 36.4355 26.6153 36.3451C26.4602 36.2545 26.3645 36.0885 26.3645 35.909V26.4574C26.3645 26.2752 26.4623 26.1064 26.6208 26.0168L33.3514 22.2182ZM27.3744 35.0434L33.095 31.8152V23.521L27.3744 26.7515V35.0434Z"
        fill="black"
      />
      <path
        d="M19.9649 22.2719C24.9857 22.2719 29.0558 18.2018 29.0558 13.181C29.0558 8.16023 24.9857 4.09009 19.9649 4.09009C14.9442 4.09009 10.874 8.16023 10.874 13.181C10.874 18.2018 14.9442 22.2719 19.9649 22.2719Z"
        fill="white"
      />
      <path
        d="M28.5507 13.1818C28.5507 8.43993 24.7066 4.59579 19.9647 4.59579C15.2229 4.59579 11.3788 8.43993 11.3788 13.1818C11.3788 17.9236 15.2229 21.7678 19.9647 21.7678C24.7066 21.7678 28.5507 17.9236 28.5507 13.1818ZM29.5606 13.1818C29.5606 18.4815 25.2644 22.7776 19.9647 22.7776C14.665 22.7776 10.3689 18.4815 10.3689 13.1818C10.3689 7.88207 14.665 3.58594 19.9647 3.58594C25.2644 3.58594 29.5606 7.88207 29.5606 13.1818Z"
        fill="black"
      />
      <path d="M14.0549 12.8784H17.6913V16.5148H14.0549V12.8784Z" fill="black" />
      <path d="M15.1921 14.0149H16.5558V16.5149H15.1921V14.0149Z" fill="white" />
      <path d="M22.2368 12.8784H25.8732V16.5148H22.2368V12.8784Z" fill="black" />
      <path d="M23.3733 14.0149H24.7369V16.5149H23.3733V14.0149Z" fill="white" />
    </svg>
  </div>
);

================
File: src/renderer/src/icons/LmStudioIcon.tsx
================
type Props = {
  width?: number;
  height?: number;
  className?: string;
};
⋮----
export const LmStudioIcon = ({ width = 64, height = 64, className }: Props) => (
  <div
    aria-label="LM Studio"
    className={`flex items-center justify-center rounded-md ${className || ''}`}
    style={{
      backgroundImage: 'linear-gradient(135deg, rgb(108, 120, 239), rgb(79, 20, 190))',
      color: 'rgb(255, 255, 255)',
      height: `${height}px`,
      width: `${width}px`,
    }}
  >
    <svg fill="currentColor" fillRule="evenodd" viewBox="0 0 24 24" width={width * 0.75} height={height * 0.75} xmlns="http://www.w3.org/2000/svg">
      <title>LM Studio</title>
      <path
        d="M2.84 2a1.273 1.273 0 100 2.547h14.107a1.273 1.273 0 100-2.547H2.84zM7.935 5.33a1.273 1.273 0 000 2.548H22.04a1.274 1.274 0 000-2.547H7.935zM3.624 9.935c0-.704.57-1.274 1.274-1.274h14.106a1.274 1.274 0 010 2.547H4.898c-.703 0-1.274-.57-1.274-1.273zM1.273 12.188a1.273 1.273 0 100 2.547H15.38a1.274 1.274 0 000-2.547H1.273zM3.624 16.792c0-.704.57-1.274 1.274-1.274h14.106a1.273 1.273 0 110 2.547H4.898c-.703 0-1.274-.57-1.274-1.273zM13.029 18.849a1.273 1.273 0 100 2.547h9.698a1.273 1.273 0 100-2.547h-9.698z"
        fillOpacity=".3"
      ></path>
      <path d="M2.84 2a1.273 1.273 0 100 2.547h10.287a1.274 1.274 0 000-2.547H2.84zM7.935 5.33a1.273 1.273 0 000 2.548H18.22a1.274 1.274 0 000-2.547H7.935zM3.624 9.935c0-.704.57-1.274 1.274-1.274h10.286a1.273 1.273 0 010 2.547H4.898c-.703 0-1.274-.57-1.274-1.273zM1.273 12.188a1.273 1.273 0 100 2.547H11.56a1.274 1.274 0 000-2.547H1.273zM3.624 16.792c0-.704.57-1.274 1.274-1.274h10.286a1.273 1.273 0 110 2.547H4.898c-.703 0-1.274-.57-1.274-1.273zM13.029 18.849a1.273 1.273 0 100 2.547h5.78a1.273 1.273 0 100-2.547h-5.78z"></path>
    </svg>
  </div>
);

================
File: src/renderer/src/icons/MinimaxIcon.tsx
================
type Props = {
  width?: number;
  height?: number;
  className?: string;
};
⋮----
export const MinimaxIcon = ({ width = 64, height = 64, className }: Props) => (
  <div
    aria-label="Minimax"
    className={`flex items-center justify-center rounded-md ${className || ''}`}
    style={{
      background: 'linear-gradient(to right, rgb(226, 22, 126), rgb(254, 96, 60))',
      color: 'white',
      height: `${height}px`,
      width: `${width}px`,
    }}
  >
    <svg
      className="-ml-[2px]"
      fill="currentColor"
      fillRule="evenodd"
      viewBox="0 0 24 24"
      width={width * 0.75}
      height={height * 0.75}
      xmlns="http://www.w3.org/2000/svg"
    >
      <title>Minimax</title>
      <path d="M16.278 2c1.156 0 2.093.927 2.093 2.07v12.501a.74.74 0 00.744.709.74.74 0 00.743-.709V9.099a2.06 2.06 0 012.071-2.049A2.06 2.06 0 0124 9.1v6.561a.649.649 0 01-.652.645.649.649 0 01-.653-.645V9.1a.762.762 0 00-.766-.758.762.762 0 00-.766.758v7.472a2.037 2.037 0 01-2.048 2.026 2.037 2.037 0 01-2.048-2.026v-12.5a.785.785 0 00-.788-.753.785.785 0 00-.789.752l-.001 15.904A2.037 2.037 0 0113.441 22a2.037 2.037 0 01-2.048-2.026V18.04c0-.356.292-.645.652-.645.36 0 .652.289.652.645v1.934c0 .263.142.506.372.638.23.131.514.131.744 0a.734.734 0 00.372-.638V4.07c0-1.143.937-2.07 2.093-2.07zm-5.674 0c1.156 0 2.093.927 2.093 2.07v11.523a.648.648 0 01-.652.645.648.648 0 01-.652-.645V4.07a.785.785 0 00-.789-.78.785.785 0 00-.789.78v14.013a2.06 2.06 0 01-2.07 2.048 2.06 2.06 0 01-2.071-2.048V9.1a.762.762 0 00-.766-.758.762.762 0 00-.766.758v3.8a2.06 2.06 0 01-2.071 2.049A2.06 2.06 0 010 12.9v-1.378c0-.357.292-.646.652-.646.36 0 .653.29.653.646V12.9c0 .418.343.757.766.757s.766-.339.766-.757V9.099a2.06 2.06 0 012.07-2.048 2.06 2.06 0 012.071 2.048v8.984c0 .419.343.758.767.758.423 0 .766-.339.766-.758V4.07c0-1.143.937-2.07 2.093-2.07z"></path>
    </svg>
  </div>
);

================
File: src/renderer/src/icons/MistralIcon.tsx
================
type Props = {
  width?: number;
  height?: number;
  className?: string;
};
⋮----
export const MistralIcon = ({ width = 64, height = 64, className }: Props) => (
  <div
    aria-label="Mistral"
    className={`flex items-center justify-center rounded-full ${className || ''}`}
    style={{
      backgroundColor: 'rgb(250, 82, 15)',
      color: 'rgb(255, 255, 255)',
      height: `${height}px`,
      width: `${width}px`,
    }}
  >
    <svg fill="currentColor" fillRule="evenodd" viewBox="0 0 24 24" width={width * 0.75} height={height * 0.75} xmlns="http://www.w3.org/2000/svg">
      <title>Mistral</title>
      <path
        d="M3.428 3.4h3.429v3.428h3.429v3.429h-.002 3.431V6.828h3.427V3.4h3.43v13.714H24v3.429H13.714v-3.428h-3.428v-3.429h-3.43v3.428h3.43v3.429H0v-3.429h3.428V3.4zm10.286 13.715h3.428v-3.429h-3.427v3.429z"
        clipRule="evenodd"
      />
    </svg>
  </div>
);

================
File: src/renderer/src/icons/NeuralwattIcon.tsx
================
type Props = {
  width?: number;
  height?: number;
  className?: string;
};
⋮----
export const NeuralwattIcon = ({ width = 48, height = 48, className }: Props) => (
  <div
    aria-label="Neuralwatt"
    className={`flex items-center justify-center rounded-md ${className || ''}`}
    style={{
      backgroundColor: '#d55934',
      color: 'white',
      height: `${height}px`,
      width: `${width}px`,
    }}
  >
    <svg xmlns="http://www.w3.org/2000/svg" width={width * 0.6} height={height * 0.6} fill="none" viewBox="0 0 32 32">
      <path
        fill="currentColor"
        d="M29.006 32a2.963 2.963 0 1 0 0-5.926 2.963 2.963 0 0 0 0 5.926M2.963 5.926a2.963 2.963 0 1 0 0-5.926 2.963 2.963 0 0 0 0 5.926M16.018 18.952a2.961 2.961 0 1 0 0-5.922 2.961 2.961 0 0 0 0 5.922M2.962 12.116a2.667 2.667 0 1 0 0-5.333 2.667 2.667 0 0 0 0 5.333M9.5 12.116a2.667 2.667 0 1 0-.001-5.334 2.667 2.667 0 0 0 0 5.334M29.006 25.199a2.666 2.666 0 1 0 0-5.333 2.666 2.666 0 0 0 0 5.333M22.535 25.199a2.666 2.666 0 1 0 0-5.333 2.666 2.666 0 0 0 0 5.333M29.006 18.357a2.369 2.369 0 1 0 0-4.738 2.369 2.369 0 0 0 0 4.738M29.006 12.11a2.369 2.369 0 1 0 0-4.738 2.369 2.369 0 0 0 0 4.738M29.006 5.331a2.369 2.369 0 1 0 0-4.737 2.369 2.369 0 0 0 0 4.737M2.963 18.298a2.369 2.369 0 1 0 0-4.737 2.369 2.369 0 0 0 0 4.737M2.963 24.901a2.369 2.369 0 1 0 0-4.737 2.369 2.369 0 0 0 0 4.737M2.963 31.348a2.369 2.369 0 1 0 0-4.737 2.369 2.369 0 0 0 0 4.737"
      />
      <path
        fill="currentColor"
        d="M9.499 17.883a1.952 1.952 0 1 0 0-3.905 1.952 1.952 0 0 0 0 3.905M22.535 17.942a1.95 1.95 0 1 0 0-3.902 1.95 1.95 0 0 0 0 3.902M16.018 11.403a1.895 1.895 0 1 0 0-3.79 1.895 1.895 0 0 0 0 3.79M16.018 24.426a1.893 1.893 0 1 0 0-3.787 1.893 1.893 0 0 0 0 3.787M9.499 4.678a1.714 1.714 0 1 0 0-3.428 1.714 1.714 0 0 0 0 3.428M22.535 30.75a1.712 1.712 0 1 0 0-3.425 1.712 1.712 0 0 0 0 3.425"
        opacity=".5"
      />
      <path
        fill="currentColor"
        d="M22.535 10.985a1.477 1.477 0 1 0 0-2.954 1.477 1.477 0 0 0 0 2.954M9.499 23.95a1.475 1.475 0 1 0 0-2.95 1.475 1.475 0 0 0 0 2.95M16.018 30.038a1.001 1.001 0 1 0 0-2.002 1.001 1.001 0 0 0 0 2.002M16.018 4.023a.998.998 0 1 0 0-1.997.998.998 0 0 0 0 1.997"
        opacity=".3"
      />
      <path
        fill="currentColor"
        d="M9.499 29.565a.585.585 0 1 0 0-1.17.585.585 0 0 0 0 1.17M22.535 3.599a.58.58 0 1 0 0-1.161.58.58 0 0 0 0 1.16"
        opacity=".1"
      />
    </svg>
  </div>
);

================
File: src/renderer/src/icons/OllamaIcon.tsx
================
type Props = {
  width?: number;
  height?: number;
  className?: string;
};
⋮----
export const OllamaIcon = ({ width = 64, height = 64, className }: Props) => (
  <div
    aria-label="Ollama"
    className={`flex items-center justify-center rounded-md ${className || ''}`}
    style={{
      backgroundColor: 'rgb(255, 255, 255)',
      color: 'rgb(0, 0, 0)',
      height: `${height}px`,
      width: `${width}px`,
    }}
  >
    <svg fill="currentColor" fillRule="evenodd" viewBox="0 0 24 24" width={width * 0.75} height={height * 0.75} xmlns="http://www.w3.org/2000/svg">
      <title>Ollama</title>
      <path d="M7.905 1.09c.216.085.411.225.588.41.295.306.544.744.734 1.263.191.522.315 1.1.362 1.68a5.054 5.054 0 012.049-.636l.051-.004c.87-.07 1.73.087 2.48.474.101.053.2.11.297.17.05-.569.172-1.134.36-1.644.19-.52.439-.957.733-1.264a1.67 1.67 0 01.589-.41c.257-.1.53-.118.796-.042.401.114.745.368 1.016.737.248.337.434.769.561 1.287.23.934.27 2.163.115 3.645l.053.04.026.019c.757.576 1.284 1.397 1.563 2.35.435 1.487.216 3.155-.534 4.088l-.018.021.002.003c.417.762.67 1.567.724 2.4l.002.03c.064 1.065-.2 2.137-.814 3.19l-.007.01.01.024c.472 1.157.62 2.322.438 3.486l-.006.039a.651.651 0 01-.747.536.648.648 0 01-.54-.742c.167-1.033.01-2.069-.48-3.123a.643.643 0 01.04-.617l.004-.006c.604-.924.854-1.83.8-2.72-.046-.779-.325-1.544-.8-2.273a.644.644 0 01.18-.886l.009-.006c.243-.159.467-.565.58-1.12a4.229 4.229 0 00-.095-1.974c-.205-.7-.58-1.284-1.105-1.683-.595-.454-1.383-.673-2.38-.61a.653.653 0 01-.632-.371c-.314-.665-.772-1.141-1.343-1.436a3.288 3.288 0 00-1.772-.332c-1.245.099-2.343.801-2.67 1.686a.652.652 0 01-.61.425c-1.067.002-1.893.252-2.497.703-.522.39-.878.935-1.066 1.588a4.07 4.07 0 00-.068 1.886c.112.558.331 1.02.582 1.269l.008.007c.212.207.257.53.109.785-.36.622-.629 1.549-.673 2.44-.05 1.018.186 1.902.719 2.536l.016.019a.643.643 0 01.095.69c-.576 1.236-.753 2.252-.562 3.052a.652.652 0 01-1.269.298c-.243-1.018-.078-2.184.473-3.498l.014-.035-.008-.012a4.339 4.339 0 01-.598-1.309l-.005-.019a5.764 5.764 0 01-.177-1.785c.044-.91.278-1.842.622-2.59l.012-.026-.002-.002c-.293-.418-.51-.953-.63-1.545l-.005-.024a5.352 5.352 0 01.093-2.49c.262-.915.777-1.701 1.536-2.269.06-.045.123-.09.186-.132-.159-1.493-.119-2.73.112-3.67.127-.518.314-.95.562-1.287.27-.368.614-.622 1.015-.737.266-.076.54-.059.797.042zm4.116 9.09c.936 0 1.8.313 2.446.855.63.527 1.005 1.235 1.005 1.94 0 .888-.406 1.58-1.133 2.022-.62.375-1.451.557-2.403.557-1.009 0-1.871-.259-2.493-.734-.617-.47-.963-1.13-.963-1.845 0-.707.398-1.417 1.056-1.946.668-.537 1.55-.849 2.485-.849zm0 .896a3.07 3.07 0 00-1.916.65c-.461.37-.722.835-.722 1.25 0 .428.21.829.61 1.134.455.347 1.124.548 1.943.548.799 0 1.473-.147 1.932-.426.463-.28.7-.686.7-1.257 0-.423-.246-.89-.683-1.256-.484-.405-1.14-.643-1.864-.643zm.662 1.21l.004.004c.12.151.095.37-.056.49l-.292.23v.446a.375.375 0 01-.376.373.375.375 0 01-.376-.373v-.46l-.271-.218a.347.347 0 01-.052-.49.353.353 0 01.494-.051l.215.172.22-.174a.353.353 0 01.49.051zm-5.04-1.919c.478 0 .867.39.867.871a.87.87 0 01-.868.871.87.87 0 01-.867-.87.87.87 0 01.867-.872zm8.706 0c.48 0 .868.39.868.871a.87.87 0 01-.868.871.87.87 0 01-.867-.87.87.87 0 01.867-.872zM7.44 2.3l-.003.002a.659.659 0 00-.285.238l-.005.006c-.138.189-.258.467-.348.832-.17.692-.216 1.631-.124 2.782.43-.128.899-.208 1.404-.237l.01-.001.019-.034c.046-.082.095-.161.148-.239.123-.771.022-1.692-.253-2.444-.134-.364-.297-.65-.453-.813a.628.628 0 00-.107-.09L7.44 2.3zm9.174.04l-.002.001a.628.628 0 00-.107.09c-.156.163-.32.45-.453.814-.29.794-.387 1.776-.23 2.572l.058.097.008.014h.03a5.184 5.184 0 011.466.212c.086-1.124.038-2.043-.128-2.722-.09-.365-.21-.643-.349-.832l-.004-.006a.659.659 0 00-.285-.239h-.004z"></path>
    </svg>
  </div>
);

================
File: src/renderer/src/icons/OpenAiCompatibleIcon.tsx
================
type Props = {
  width?: number;
  height?: number;
  className?: string;
};

================
File: src/renderer/src/icons/OpenAiIcon.tsx
================
type Props = {
  width?: number;
  height?: number;
  className?: string;
};
⋮----
export const OpenAiIcon = ({ width = 64, height = 64, className }: Props) => (
  <div
    aria-label="OpenAI"
    className={`flex items-center justify-center rounded-md ${className || ''}`}
    style={{
      backgroundColor: 'rgb(0, 0, 0)',
      boxShadow: 'rgba(255, 255, 255, 0.1) 0px 0px 0px 1px inset',
      color: 'rgb(255, 255, 255)',
      height: `${height}px`,
      width: `${width}px`,
    }}
  >
    <svg fill="currentColor" fillRule="evenodd" viewBox="0 0 24 24" width={width * 0.75} height={height * 0.75} xmlns="http://www.w3.org/2000/svg">
      <title>OpenAI</title>
      <path d="M21.55 10.004a5.416 5.416 0 00-.478-4.501c-1.217-2.09-3.662-3.166-6.05-2.66A5.59 5.59 0 0010.831 1C8.39.995 6.224 2.546 5.473 4.838A5.553 5.553 0 001.76 7.496a5.487 5.487 0 00.691 6.5 5.416 5.416 0 00.477 4.502c1.217 2.09 3.662 3.165 6.05 2.66A5.586 5.586 0 0013.168 23c2.443.006 4.61-1.546 5.361-3.84a5.553 5.553 0 003.715-2.66 5.488 5.488 0 00-.693-6.497v.001zm-8.381 11.558a4.199 4.199 0 01-2.675-.954c.034-.018.093-.05.132-.074l4.44-2.53a.71.71 0 00.364-.623v-6.176l1.877 1.069c.02.01.033.029.036.05v5.115c-.003 2.274-1.87 4.118-4.174 4.123zM4.192 17.78a4.059 4.059 0 01-.498-2.763c.032.02.09.055.131.078l4.44 2.53c.225.13.504.13.73 0l5.42-3.088v2.138a.068.068 0 01-.027.057L9.9 19.288c-1.999 1.136-4.552.46-5.707-1.51h-.001zM3.023 8.216A4.15 4.15 0 015.198 6.41l-.002.151v5.06a.711.711 0 00.364.624l5.42 3.087-1.876 1.07a.067.067 0 01-.063.005l-4.489-2.559c-1.995-1.14-2.679-3.658-1.53-5.63h.001zm15.417 3.54l-5.42-3.088L14.896 7.6a.067.067 0 01.063-.006l4.489 2.557c1.998 1.14 2.683 3.662 1.529 5.633a4.163 4.163 0 01-2.174 1.807V12.38a.71.71 0 00-.363-.623zm1.867-2.773a6.04 6.04 0 00-.132-.078l-4.44-2.53a.731.731 0 00-.729 0l-5.42 3.088V7.325a.068.068 0 01.027-.057L14.1 4.713c2-1.137 4.555-.46 5.707 1.513.487.833.664 1.809.499 2.757h.001zm-11.741 3.81l-1.877-1.068a.065.065 0 01-.036-.051V6.559c.001-2.277 1.873-4.122 4.181-4.12.976 0 1.92.338 2.671.954-.034.018-.092.05-.131.073l-4.44 2.53a.71.71 0 00-.365.623l-.003 6.173v.002zm1.02-2.168L12 9.25l2.414 1.375v2.75L12 14.75l-2.415-1.375v-2.75z"></path>
    </svg>
  </div>
);

================
File: src/renderer/src/icons/OpenCodeGoIcon.tsx
================
type Props = {
  width?: number;
  height?: number;
  className?: string;
};
⋮----
export const OpenCodeGoIcon = ({ width = 64, height = 64, className }: Props) => (
  <div
    aria-label="OpenCode Go"
    className={`flex items-center justify-center rounded-md ${className || ''}`}
    style={{
      backgroundColor: '#131010',
      color: 'rgb(255, 255, 255)',
      height: `${height}px`,
      width: `${width}px`,
    }}
  >
    <svg width={width * 0.75} height={width * 0.75 * (30 / 54)} viewBox="0 0 54 30" fill="none" xmlns="http://www.w3.org/2000/svg">
      <path d="M24 30H0V0H24V6H6V24H18V18H12V12H24V30Z" fill="currentColor"></path>
      <path d="M12 18H18V24H6V12H12V18Z" fill="currentColor" fillOpacity="0.2"></path>
      <path d="M48 12V24H36V12H48Z" fill="currentColor" fillOpacity="0.2"></path>
      <path d="M54 30H30V0H54V30ZM36 24H48V6H36V24Z" fill="currentColor"></path>
    </svg>
  </div>
);

================
File: src/renderer/src/icons/OpenCodeIcon.tsx
================
type Props = {
  width?: number;
  height?: number;
  className?: string;
};
⋮----
export const OpenCodeIcon = ({ width = 64, height = 64, className }: Props) => (
  <div
    aria-label="OpenCode ZEN"
    className={`flex items-center justify-center rounded-md ${className || ''}`}
    style={{
      backgroundColor: '#131010',
      color: 'rgb(255, 255, 255)',
      height: `${height}px`,
      width: `${width}px`,
    }}
  >
    <svg width={width * 0.95} height={height * 0.95} viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
      <path d="M320 224V352H192V224H320Z" fill="#5A5858"></path>
      <path fillRule="evenodd" clipRule="evenodd" d="M384 416H128V96H384V416ZM320 160H192V352H320V160Z" fill="white"></path>
    </svg>
  </div>
);

================
File: src/renderer/src/icons/OpenRouterIcon.tsx
================
type Props = {
  width?: number;
  height?: number;
  className?: string;
};
⋮----
export const OpenRouterIcon = ({ width = 64, height = 64, className }: Props) => (
  <div
    aria-label="OpenRouter"
    className={`flex items-center justify-center rounded-md ${className || ''}`}
    style={{
      backgroundColor: 'rgb(101, 102, 241)',
      color: 'rgb(255, 255, 255)',
      height: `${height}px`,
      width: `${width}px`,
    }}
  >
    <svg fill="currentColor" fillRule="evenodd" viewBox="0 0 24 24" width={width * 0.75} height={height * 0.75} xmlns="http://www.w3.org/2000/svg">
      <title>OpenRouter</title>
      <path d="M16.804 1.957l7.22 4.105v.087L16.73 10.21l.017-2.117-.821-.03c-1.059-.028-1.611.002-2.268.11-1.064.175-2.038.577-3.147 1.352L8.345 11.03c-.284.195-.495.336-.68.455l-.515.322-.397.234.385.23.53.338c.476.314 1.17.796 2.701 1.866 1.11.775 2.083 1.177 3.147 1.352l.3.045c.694.091 1.375.094 2.825.033l.022-2.159 7.22 4.105v.087L16.589 22l.014-1.862-.635.022c-1.386.042-2.137.002-3.138-.162-1.694-.28-3.26-.926-4.881-2.059l-2.158-1.5a21.997 21.997 0 00-.755-.498l-.467-.28a55.927 55.927 0 00-.76-.43C2.908 14.73.563 14.116 0 14.116V9.888l.14.004c.564-.007 2.91-.622 3.809-1.124l1.016-.58.438-.274c.428-.28 1.072-.726 2.686-1.853 1.621-1.133 3.186-1.78 4.881-2.059 1.152-.19 1.974-.213 3.814-.138l.02-1.907z"></path>
    </svg>
  </div>
);

================
File: src/renderer/src/icons/RequestyIcon.tsx
================
import requestyIcon from './requesty.png';
⋮----
type Props = {
  width?: number;
  height?: number;
  className?: string;
};

================
File: src/renderer/src/icons/SyntheticIcon.tsx
================
type Props = {
  width?: number;
  height?: number;
  className?: string;
};
⋮----
export const SyntheticIcon = ({ width = 64, height = 64, className }: Props) => (
  <div
    aria-label="Synthetic"
    className={`flex items-center justify-center rounded-md relative ${className || ''}`}
    style={{
      backgroundColor: 'white',
      height: `${height}px`,
      width: `${width}px`,
    }}
  >
    <svg
      width={width * 0.8}
      height={height * 0.8}
      viewBox="0 0 54 54"
      fill="none"
      xmlns="http://www.w3.org/2000/svg"
      className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2"
    >
      <path
        d="M27.8517 26.3314C27.8439 26.1451 27.8371 25.9303 27.8337 25.692C27.8203 24.7571 27.8545 23.4581 28.0457 22.0052C28.4322 19.0688 29.4394 15.6521 31.8222 13.2693C34.205 10.8867 37.6212 9.87992 40.5575 9.49344C42.0107 9.30219 43.3101 9.26734 44.245 9.28076C44.4829 9.28417 44.6968 9.29231 44.883 9.30009C44.8908 9.48595 44.8975 9.69933 44.901 9.93676C44.9144 10.8716 44.8795 12.171 44.6883 13.6242C44.3018 16.5605 43.2958 19.9773 40.9131 22.3602C38.5303 24.743 35.1129 25.7495 32.1765 26.136C30.7234 26.3272 29.4246 26.3614 28.4897 26.348C28.2518 26.3446 28.0378 26.3392 27.8517 26.3314Z"
        stroke="black"
        strokeWidth="2.5"
      ></path>
      <path
        d="M27.8916 27.1592C28.0179 27.0219 28.165 26.8653 28.3311 26.6943C28.9826 26.0238 29.9253 25.1295 31.0879 24.2373C33.4375 22.4342 36.5658 20.7305 39.9355 20.7305C43.3051 20.7306 46.4327 22.4343 48.7822 24.2373C49.945 25.1296 50.8885 26.0238 51.54 26.6943C51.7059 26.865 51.8514 27.022 51.9775 27.1592C51.8516 27.2961 51.7055 27.4518 51.54 27.6221C50.8885 28.2926 49.945 29.1868 48.7822 30.0791C46.4327 31.8821 43.3052 33.5868 39.9355 33.5869C36.5657 33.5869 33.4375 31.8822 31.0879 30.0791C29.9252 29.1868 28.9826 28.2926 28.3311 27.6221C28.1652 27.4514 28.0177 27.2963 27.8916 27.1592Z"
        stroke="black"
        strokeWidth="2.5"
      ></path>
      <path
        d="M27.4459 27.6915C27.6323 27.6837 27.8471 27.677 28.0854 27.6736C29.0202 27.6602 30.3192 27.6943 31.7721 27.8856C34.7085 28.272 38.1253 29.2793 40.5081 31.6621C42.8906 34.0448 43.8974 37.4611 44.2839 40.3973C44.4752 41.8505 44.51 43.1499 44.4966 44.0848C44.4932 44.3228 44.485 44.5367 44.4773 44.7229C44.2914 44.7306 44.078 44.7374 43.8406 44.7408C42.9057 44.7542 41.6063 44.7194 40.1531 44.5281C37.2168 44.1417 33.8 43.1356 31.4172 40.753C29.0343 38.3701 28.0278 34.9528 27.6413 32.0163C27.4501 30.5633 27.4159 29.2644 27.4294 28.3296C27.4328 28.0916 27.4381 27.8777 27.4459 27.6915Z"
        stroke="black"
        strokeWidth="2.5"
      ></path>
      <path
        d="M26.916 28.1289C27.0533 28.2552 27.2099 28.4023 27.3809 28.5684C28.0514 29.2199 28.9457 30.1626 29.8379 31.3252C31.641 33.6748 33.3447 36.8031 33.3447 40.1728C33.3446 43.5424 31.6409 46.67 29.8379 49.0195C28.9456 50.1823 28.0514 51.1258 27.3809 51.7773C27.2102 51.9432 27.0532 52.0887 26.916 52.2148C26.7791 52.0889 26.6234 51.9428 26.4531 51.7773C25.7826 51.1258 24.8884 50.1823 23.9961 49.0195C22.1931 46.67 20.4884 43.5426 20.4883 40.1728C20.4883 36.803 22.193 33.6748 23.9961 31.3252C24.8884 30.1625 25.7826 29.2199 26.4531 28.5684C26.6238 28.4025 26.7789 28.255 26.916 28.1289Z"
        stroke="black"
        strokeWidth="2.5"
      ></path>
      <path
        d="M26.2147 27.691C26.2225 27.8774 26.2293 28.0922 26.2327 28.3305C26.2461 29.2653 26.2119 30.5643 26.0207 32.0172C25.6342 34.9536 24.627 38.3704 22.2442 40.7532C19.8614 43.1357 16.4452 44.1425 13.5089 44.529C12.0557 44.7203 10.7563 44.7551 9.82145 44.7417C9.58349 44.7383 9.36957 44.7301 9.1834 44.7224C9.17564 44.5365 9.16886 44.3231 9.16545 44.0857C9.15203 43.1508 9.18689 41.8514 9.37813 40.3982C9.76458 37.4619 10.7706 34.0451 13.1533 31.6623C15.5361 29.2795 18.9535 28.2729 21.8899 27.8865C23.343 27.6952 24.6419 27.6611 25.5767 27.6745C25.8146 27.6779 26.0286 27.6833 26.2147 27.691Z"
        stroke="black"
        strokeWidth="2.5"
      ></path>
      <path
        d="M25.7021 27.1602C25.5759 27.2974 25.4288 27.4541 25.2627 27.625C24.6111 28.2955 23.6684 29.1899 22.5059 30.082C20.1562 31.8851 17.028 33.5889 13.6582 33.5889C10.2886 33.5887 7.16105 31.885 4.81153 30.082C3.64873 29.1897 2.70529 28.2955 2.05372 27.625C1.88786 27.4543 1.74236 27.2973 1.61622 27.1602C1.74215 27.0232 1.88824 26.8676 2.05372 26.6973C2.70527 26.0267 3.64874 25.1326 4.81153 24.2402C7.16108 22.4372 10.2885 20.7326 13.6582 20.7324C17.0281 20.7324 20.1562 22.4371 22.5059 24.2402C23.6686 25.1325 24.6112 26.0268 25.2627 26.6973C25.4285 26.8679 25.576 27.023 25.7021 27.1602Z"
        stroke="black"
        strokeWidth="2.5"
      ></path>
      <path
        d="M25.7338 25.9813C25.5474 25.9891 25.3326 25.9959 25.0943 25.9993C24.1595 26.0127 22.8605 25.9785 21.4076 25.7873C18.4712 25.4008 15.0544 24.3936 12.6716 22.0108C10.2891 19.628 9.28226 16.2118 8.89579 13.2755C8.70453 11.8223 8.66969 10.5229 8.6831 9.58806C8.68652 9.35009 8.69466 9.13618 8.70244 8.95C8.8883 8.94224 9.10168 8.93546 9.33911 8.93205C10.274 8.91863 11.5734 8.95349 13.0266 9.14473C15.9629 9.53118 19.3797 10.5372 21.7625 12.9199C24.1454 15.3027 25.1519 18.7201 25.5383 21.6565C25.7296 23.1096 25.7638 24.4085 25.7503 25.3433C25.7469 25.5812 25.7415 25.7952 25.7338 25.9813Z"
        stroke="black"
        strokeWidth="2.5"
      ></path>
      <path
        d="M26.9121 25.7031C26.7748 25.5769 26.6182 25.4298 26.4473 25.2637C25.7768 24.6121 24.8824 23.6694 23.9902 22.5068C22.1872 20.1572 20.4834 17.029 20.4834 13.6592C20.4835 10.2896 22.1873 7.16203 23.9902 4.8125C24.8826 3.6497 25.7767 2.70627 26.4473 2.05469C26.618 1.88883 26.775 1.74333 26.9121 1.61719C27.049 1.74312 27.2047 1.88921 27.375 2.05469C28.0455 2.70624 28.9397 3.64971 29.832 4.8125C31.6351 7.16206 33.3397 10.2895 33.3398 13.6592C33.3398 17.029 31.6351 20.1572 29.832 22.5068C28.9398 23.6695 28.0455 24.6121 27.375 25.2637C27.2043 25.4295 27.0492 25.577 26.9121 25.7031Z"
        stroke="black"
        strokeWidth="2.5"
      ></path>
    </svg>
    <svg
      width={width * 0.25}
      height={height * 0.25}
      viewBox="0 0 19 19"
      fill="none"
      xmlns="http://www.w3.org/2000/svg"
      className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2"
    >
      <path
        d="M8.56067 1.3799L10.4618 1.34671C10.5492 1.34522 10.6286 1.38935 10.6747 1.45923L10.7093 1.53774L11.045 2.92076C11.1559 3.37707 11.4798 3.75232 11.9152 3.92819C12.3419 4.10041 12.8251 4.0605 13.2183 3.82145L14.3036 3.16178C14.3771 3.11703 14.4662 3.1144 14.5407 3.14983L14.6099 3.19745L15.9965 4.57385C16.0577 4.63461 16.0825 4.72126 16.0659 4.80217L16.035 4.88085L15.4788 5.80573C15.2386 6.20505 15.1867 6.68631 15.3299 7.12299L15.4034 7.30728C15.5966 7.71593 15.9524 8.02086 16.3793 8.15073L16.5666 8.19532L17.4238 8.34639C17.5414 8.36729 17.628 8.46852 17.6302 8.58794L17.6642 10.533C17.6662 10.6479 17.5891 10.7499 17.478 10.7794L16.3785 11.0721C15.9508 11.1859 15.5956 11.4767 15.399 11.8666L15.3249 12.0398C15.1648 12.4858 15.2187 12.9804 15.4714 13.3812L16.0028 14.2246C16.0497 14.2991 16.0532 14.3908 16.0168 14.4666L15.9672 14.5358L14.5394 15.9448C14.4825 16.0008 14.4038 16.025 14.3277 16.0139L14.2531 15.9908L13.2136 15.4747C12.8594 15.2989 12.4528 15.2697 12.0801 15.388L11.9219 15.4474C11.5639 15.6067 11.2838 15.8977 11.1361 16.2562L11.0812 16.4134L10.7617 17.5217C10.7312 17.6271 10.6352 17.7006 10.5255 17.7026L8.58735 17.7364C8.46855 17.7385 8.36456 17.6565 8.33878 17.5405L8.08316 16.3817L8.03422 16.2078C7.91462 15.8694 7.67485 15.5859 7.36105 15.4118L7.19756 15.3345L7.04329 15.2815C6.73077 15.1941 6.39776 15.2128 6.0968 15.3342L5.94955 15.4032L4.77293 16.0371C4.6979 16.0774 4.60969 16.0756 4.53755 16.0373L4.47222 15.9877L3.13752 14.5596C3.0776 14.4954 3.05736 14.4064 3.07873 14.3252L3.11353 14.2484L3.7373 13.3331C3.98342 12.9717 4.04895 12.5208 3.923 12.109L3.85746 11.9353L3.78169 11.7911C3.61486 11.5129 3.35814 11.2998 3.05423 11.1865L2.89807 11.1374L1.49728 10.7907C1.38749 10.7635 1.30969 10.6659 1.30755 10.5528L1.27503 8.6898C1.27334 8.58474 1.33722 8.49154 1.43207 8.45363L1.47385 8.44118L2.69615 8.18641C3.14512 8.09283 3.52318 7.80066 3.72943 7.39873L3.80738 7.2196C3.94662 6.83155 3.92084 6.40585 3.73992 6.04091L3.6523 5.8891L3.04806 4.9581C2.98255 4.85725 2.99751 4.72403 3.08453 4.64101L4.58516 3.20932C4.67146 3.12699 4.80459 3.11666 4.90217 3.18523L5.71797 3.75897C6.12951 4.04818 6.65895 4.11193 7.1274 3.92874C7.53749 3.76829 7.85309 3.43752 7.99657 3.02769L8.04812 2.84707L8.32092 1.57747C8.34533 1.46387 8.44449 1.38193 8.56067 1.3799Z"
        fill="white"
        stroke="black"
        strokeWidth="1.5"
      ></path>
      <circle cx="9.46694" cy="9.5416" r="2.53895" transform="rotate(-1 9.46694 9.5416)" fill="white" stroke="black" strokeWidth="1.5"></circle>
    </svg>
  </div>
);

================
File: src/renderer/src/icons/VertexAiIcon.tsx
================
type Props = {
  width?: number;
  height?: number;
  className?: string;
};
⋮----
export const VertexAiIcon = ({ width = 64, height = 64, className }: Props) => (
  <div
    aria-label="VertexAI"
    className={`flex items-center justify-center rounded-md ${className || ''}`}
    style={{
      backgroundColor: 'rgb(66, 133, 244)',
      color: 'rgb(255, 255, 255)',
      height: `${height}px`,
      width: `${width}px`,
    }}
  >
    <svg fill="currentColor" fillRule="evenodd" viewBox="0 0 24 24" width={width * 0.75} height={height * 0.75} xmlns="http://www.w3.org/2000/svg">
      <title>VertexAI</title>
      <path d="M11.995 20.216a1.892 1.892 0 100 3.785 1.892 1.892 0 000-3.785zm0 2.806a.927.927 0 11.927-.914.914.914 0 01-.927.914z"></path>
      <path
        clipRule="evenodd"
        d="M21.687 14.144c.237.038.452.16.605.344a.978.978 0 01-.18 1.3l-8.24 6.082a1.892 1.892 0 00-1.147-1.508l8.28-6.08a.991.991 0 01.682-.138z"
      ></path>
      <path clipRule="evenodd" d="M10.122 21.842l-8.217-6.066a.952.952 0 01-.206-1.287.978.978 0 011.287-.206l8.28 6.08a1.893 1.893 0 00-1.144 1.479z"></path>
      <path d="M4.273 4.475a.978.978 0 01-.965-.965V1.09a.978.978 0 111.943 0v2.42a.978.978 0 01-.978.965zM4.247 13.034a.978.978 0 100-1.956.978.978 0 000 1.956zM4.247 10.19a.978.978 0 100-1.956.978.978 0 000 1.956zM4.247 7.332a.978.978 0 100-1.956.978.978 0 000 1.956z"></path>
      <path d="M19.718 7.307a.978.978 0 01-.965-.979v-2.42a.965.965 0 011.93 0v2.42a.964.964 0 01-.965.979zM19.743 13.047a.978.978 0 100-1.956.978.978 0 000 1.956zM19.743 10.151a.978.978 0 100-1.956.978.978 0 000 1.956zM19.743 2.068a.978.978 0 100-1.956.978.978 0 000 1.956z"></path>
      <path d="M11.995 15.917a.978.978 0 01-.965-.965v-2.459a.978.978 0 011.943 0v2.433a.976.976 0 01-.978.991zM11.995 18.762a.978.978 0 100-1.956.978.978 0 000 1.956zM11.995 10.64a.978.978 0 100-1.956.978.978 0 000 1.956zM11.995 7.783a.978.978 0 100-1.956.978.978 0 000 1.956z"></path>
      <path d="M15.856 10.177a.978.978 0 01-.965-.965v-2.42a.977.977 0 011.702-.763.979.979 0 01.241.763v2.42a.978.978 0 01-.978.965zM15.869 4.913a.978.978 0 100-1.956.978.978 0 000 1.956zM15.869 15.853a.978.978 0 100-1.956.978.978 0 000 1.956zM15.869 12.996a.978.978 0 100-1.956.978.978 0 000 1.956z"></path>
      <path d="M8.121 15.853a.978.978 0 100-1.956.978.978 0 000 1.956zM8.121 7.783a.978.978 0 100-1.956.978.978 0 000 1.956zM8.121 4.913a.978.978 0 100-1.957.978.978 0 000 1.957zM8.134 12.996a.978.978 0 01-.978-.94V9.611a.965.965 0 011.93 0v2.445a.966.966 0 01-.952.94z"></path>
    </svg>
  </div>
);

================
File: src/renderer/src/icons/ZaiPlanIcon.tsx
================
type Props = {
  width?: number;
  height?: number;
  className?: string;
};

================
File: src/renderer/src/pages/Home.tsx
================
import { ProjectData } from '@common/types';
import { AnimatePresence, motion } from 'framer-motion';
import { Activity, Suspense, lazy, startTransition, useCallback, useEffect, useOptimistic, useRef, useState, useTransition } from 'react';
import { MdBarChart, MdSettings, MdUpload } from 'react-icons/md';
import { PiNotebookFill } from 'react-icons/pi';
import { useTranslation } from 'react-i18next';
import { useHotkeys } from 'react-hotkeys-hook';
import { useSearchParams } from 'react-router-dom';
import { compareBaseDirs } from '@common/utils';
⋮----
import { useConfiguredHotkeys } from '@/hooks/useConfiguredHotkeys';
import { useOS } from '@/hooks/useOS';
import { LogsPage } from '@/components/logs/LogsPage';
import { IconButton } from '@/components/common/IconButton';
import { LoadingOverlay } from '@/components/common/LoadingOverlay';
import { NoProjectsOpen } from '@/components/project/NoProjectsOpen';
import { OpenProjectDialog } from '@/components/project/OpenProjectDialog';
import { ProjectTabs } from '@/components/project/ProjectTabs';
import { ProjectView } from '@/components/project/ProjectView';
import { useVersions } from '@/hooks/useVersions';
import { ExtensionComponentWrapper } from '@/components/extensions/ExtensionComponentWrapper';
import { FloatingExtensionPanels } from '@/components/extensions/FloatingExtensionPanels';
import { HtmlInfoDialog } from '@/components/common/HtmlInfoDialog';
import { ProjectSettingsProvider } from '@/contexts/ProjectSettingsContext';
import { TelemetryInfoDialog } from '@/components/TelemetryInfoDialog';
import { showInfoNotification } from '@/utils/notifications';
import { useApi } from '@/contexts/ApiContext';
import { URL_PARAMS, encodeBaseDir, decodeBaseDir, ROUTES } from '@/utils/routes';
import { useBooleanState } from '@/hooks/useBooleanState';
import { PaletteItemType, useCommandPaletteStore } from '@/stores/commandPaletteStore';
import { closeSettings, openSettingsPage, useSettingsNavigationStore } from '@/stores/settingsNavigationStore';
import { registerAction, unregisterAction } from '@/stores/actionsStore';
⋮----
// The URL parameter is the source of truth for the active project, falling back to the
// backend-tracked selection until the first navigation writes the parameter
⋮----
const loadProjects = async () =>
⋮----
// eslint-disable-next-line no-console
⋮----
const handleShowView = (viewId: string) =>
⋮----
const checkReleaseNotes = async () =>
⋮----
// The URL parameter allows each window to have its own active project;
// the backend call is a write-behind mirror
⋮----
// Only change selection if we're closing the currently active project
⋮----
// Pick adjacent from remaining array (not original!) to avoid re-selecting the closed project
⋮----
// If closing non-active project: don't touch searchParams at all
⋮----
// Close current project tab
⋮----
// Handle URL parameters for direct project/task navigation
⋮----
// Propagate task deep links to the active project view
⋮----
// Project is already open and activeProject is derived from the URL, nothing to do
⋮----
// Project is not open yet (deep link): add it
⋮----
// eslint-disable-next-line no-console
⋮----
// Open new project dialog
⋮----
// Open usage dashboard
⋮----
// Open model library
⋮----
// Open settings
⋮----
// Close overlays on Escape
⋮----
// Switch to specific project tabs (Alt/Cmd + 1-9)
⋮----
// Ctrl+Tab cycling (forward)
⋮----
// Ctrl+Shift+Tab cycling (backward)
⋮----
// Reset Ctrl+Tab state on Control key up
⋮----
const handleKeyUp = (e: KeyboardEvent) =>
⋮----
const handleAddProject = async (baseDir: string) =>
⋮----
// Activate using the baseDir as stored by the backend so the strict comparisons stay consistent
⋮----
return ''; // Should not happen if showUpdateIcon is true
⋮----
tooltip=
⋮----
<OpenProjectDialog onClose=

================
File: src/renderer/src/pages/Logs.tsx
================
import { useTranslation } from 'react-i18next';
⋮----
import { LogsPage } from '@/components/logs/LogsPage';
⋮----
<LogsPage onClose=

================
File: src/renderer/src/pages/Onboarding.tsx
================
import { useCallback, useEffect, useState } from 'react';
import { clsx } from 'clsx';
import { HiArrowRight, HiArrowLeft } from 'react-icons/hi2';
import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { isEqual } from 'lodash';
import { DEFAULT_AGENT_PROFILE, DEFAULT_PROVIDER_MODELS } from '@common/agent';
import { SettingsData } from '@common/types';
⋮----
import { useSaveSettings, useSettingsStore } from '@/stores/settingsStore';
import { useAgents } from '@/contexts/AgentsContext';
import { useModelProviders } from '@/contexts/ModelProviderContext';
import { AiderSettings } from '@/components/settings/AiderSettings';
import { LanguageSelector } from '@/components/settings/LanguageSelector';
import { OnboardingProviderSetup } from '@/components/onboarding/OnboardingProviderSetup';
import { OnboardingPathChoice, OnboardingPath } from '@/components/onboarding/OnboardingPathChoice';
import { AgentSettings } from '@/components/settings/agent/AgentSettings';
import { Button } from '@/components/common/Button';
import { OnboardingStepper } from '@/components/onboarding/OnboardingStepper';
import { ROUTES } from '@/utils/routes';
import { showErrorNotification, showInfoNotification } from '@/utils/notifications';
⋮----
// Point the untouched default agent profile to the first provider configured during onboarding,
// so the agent doesn't keep a hardcoded provider the user never set up.
⋮----
const handleNext = async () =>
⋮----
// eslint-disable-next-line no-console
⋮----
const handleBack = () =>
⋮----
// eslint-disable-next-line no-console
⋮----
const handleFinish = async () =>
⋮----
// Save agent profile changes
⋮----
// Find profiles that were added, updated, or deleted
⋮----
// Handle deleted profiles
⋮----
// Handle added and updated profiles
⋮----
// New profile
⋮----
// Updated profile - check if it actually changed
⋮----
// Update profile order if needed
⋮----
// eslint-disable-next-line no-console
⋮----
// eslint-disable-next-line no-console
⋮----
const handleSelectPath = (path: OnboardingPath) =>
⋮----
{/* Language Selector in top-right corner */}
⋮----
<div className=
{/* Stepper */}
⋮----
{/* Step Content */}

⋮----
{/* Navigation Buttons */}

================
File: src/renderer/src/pages/ReadonlyApp.tsx
================
import { ReadonlyBootstrap, SettingsData, THEMES } from '@common/types';
import { useCallback, useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { Navigate, Route, Routes, useSearchParams } from 'react-router-dom';
import { IconContext } from 'react-icons';
⋮----
import { ReadonlyProjectView } from '@/components/readonly/ReadonlyProjectView';
import { ReadonlyHeader } from '@/components/readonly/ReadonlyHeader';
import { ExtensionComponentWrapper } from '@/components/extensions/ExtensionComponentWrapper';
import { ReadonlyApiProvider } from '@/contexts/ReadonlyApiContext';
import { TooltipProvider } from '@/components/ui/Tooltip';
import { ReadonlyModelProvider } from '@/contexts/ModelProviderContext';
import { ExtensionsProvider } from '@/contexts/ExtensionsContext';
import { useSettingsStore } from '@/stores/settingsStore';
import { decodeBaseDir, encodeBaseDir, ROUTES, URL_PARAMS } from '@/utils/routes';
import i18n from '@/i18n';
⋮----
type Props = {
  bootstrap: ReadonlyBootstrap;
};
⋮----
export const ReadonlyApp = (

================
File: src/renderer/src/pages/Settings.tsx
================
import { AgentProfile, Font, McpServersData, ProjectData, ProviderProfile, SettingsData, Theme } from '@common/types';
import { Dispatch, ReactNode, SetStateAction, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { clsx } from 'clsx';
import { AnimatePresence, motion } from 'framer-motion';
import {
  FaBrain,
  FaChevronDown,
  FaChevronRight,
  FaCog,
  FaInfoCircle,
  FaKeyboard,
  FaMicrophone,
  FaPlug,
  FaPuzzlePiece,
  FaRobot,
  FaServer,
} from 'react-icons/fa';
import { MdTerminal } from 'react-icons/md';
import { LuClipboardList } from 'react-icons/lu';
import { RiMenuUnfold4Line } from 'react-icons/ri';
import { HiXMark } from 'react-icons/hi2';
⋮----
import { useResponsive } from '@/hooks/useResponsive';
import { useBooleanState } from '@/hooks/useBooleanState';
import { getPathBasename } from '@/utils/path-utils';
import { useApi } from '@/contexts/ApiContext';
import { AiderSettings } from '@/components/settings/AiderSettings';
import { GeneralSettings } from '@/components/settings/GeneralSettings';
import { AgentSettings } from '@/components/settings/agent/AgentSettings';
import { McpSettings } from '@/components/settings/mcp/McpSettings';
import { AboutSettings } from '@/components/settings/AboutSettings';
import { NetworkSettings } from '@/components/settings/NetworkSettings';
import { MemorySettings } from '@/components/settings/MemorySettings';
import { VoiceSettings } from '@/components/settings/VoiceSettings';
import { HotkeysSettings } from '@/components/settings/HotkeysSettings';
import { TaskSettings } from '@/components/settings/TaskSettings';
import { ExtensionsSettings } from '@/components/settings/ExtensionsSettings';
⋮----
type PageId = 'general' | 'aider' | 'agents' | 'mcpServers' | 'tasks' | 'memory' | 'voice' | 'hotkeys' | 'network' | 'extensions' | 'about';
⋮----
interface SidebarItem {
  id: string;
  label: string;
  icon?: ReactNode;
  children?: { id: string; label: string }[];
  pageId: PageId;
}
⋮----
type Props = {
  settings: SettingsData;
  updateSettings: (settings: SettingsData) => void;
  onLanguageChange: (language: string) => void;
  onZoomChange: (zoomLevel: number) => void;
  onThemeChange: (theme: Theme) => void;
  onFontChange: (fontName: Font) => void;
  onFontSizeChange: (fontSize: number) => void;
  initialPageId?: string;
  initialOptions?: Record<string, unknown>;
  agentProfiles?: AgentProfile[];
  setAgentProfiles?: (profiles: AgentProfile[]) => void;
  mcpServers: McpServersData;
  setMcpServers: Dispatch<SetStateAction<McpServersData>>;
  openProjects?: ProjectData[];
  providers?: ProviderProfile[];
  setProviders?: (providers: ProviderProfile[]) => void;
  onShowLogs?: () => void;
};
⋮----
export const Settings = ({
  settings,
  updateSettings,
  onLanguageChange,
  onZoomChange,
  onThemeChange,
  onFontChange,
  onFontSizeChange,
  initialPageId,
  initialOptions,
  agentProfiles,
  setAgentProfiles,
  mcpServers,
  setMcpServers,
  openProjects,
  providers,
  setProviders,
  onShowLogs,
}: Props) =>
⋮----
const scrollToSection = (sectionId: string) =>
⋮----
const handleItemClick = (item: SidebarItem) =>
⋮----
const handleOpenMcpServers = (context?: string) =>
⋮----
const handleChildClick = (pageId: PageId, sectionId: string) =>
⋮----
// Handle agent context selection
⋮----
// Extract project baseDir from sectionId (format: agent-{baseDir})
⋮----
// Extract project baseDir from sectionId (format: mcpServers-{baseDir})
⋮----
// Extract project baseDir from sectionId (format: extension-{baseDir})
⋮----
const renderContent = () =>
⋮----
className=
⋮----
e.stopPropagation();
⋮----
{!item.children?.length && <span className="w-6" />} {/* Spacer for items without children */}
⋮----
{/* Children */}
⋮----
{/* Sidebar */}
⋮----
{/* Mobile navigation drawer */}
⋮----
{/* Content */}

================
File: src/renderer/src/pages/UpdatedFilesDiff.tsx
================
import { useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useSearchParams } from 'react-router-dom';
import { UpdatedFile } from '@common/types';
⋮----
import { groupFilesByCommit } from '@/components/Workspace/group-files';
import { UpdatedFilesDiffModal } from '@/components/Workspace/UpdatedFilesDiffModal';
import { useApi } from '@/contexts/ApiContext';
import { URL_PARAMS, decodeBaseDir } from '@/utils/routes';
⋮----
const loadFiles = async () =>
⋮----
// eslint-disable-next-line no-console

================
File: src/renderer/src/stores/actionsStore.ts
================
export const registerAction = (id: string, handler: () => void) =>
⋮----
export const unregisterAction = (id: string) =>
⋮----
/**
 * Invoke a registered UI action by id. Single entry point for all consumers:
 * command palette, extension system (future), toolbar buttons, hotkeys.
 * @returns true if the action was found and invoked, false otherwise
 */
export const invokeAction = (id: string): boolean =>
⋮----
// eslint-disable-next-line no-console

================
File: src/renderer/src/stores/commandPaletteStore.ts
================
import { devtools } from 'zustand/middleware';
import { createWithEqualityFn } from 'zustand/traditional';
⋮----
export enum PaletteItemType {
  Action = 'action',
  File = 'file',
  Task = 'task',
  Project = 'project',
}
⋮----
export type PaletteItem = {
  id: string;
  label: string;
  description?: string;
  state?: string;
  archived?: boolean;
  type: PaletteItemType;
  shortcut?: string;
  action: () => void;
};
⋮----
type CommandPaletteStore = {
  isOpen: boolean;
  items: Map<string, PaletteItem>;
  recentlyUsed: string[];
  itemIdsByScope: Map<string, Set<string>>;
  itemsByScope: Map<string, Map<string, PaletteItem>>;

  openPalette: () => void;
  closePalette: () => void;
  togglePalette: () => void;
  replaceItems: (scope: string, items: PaletteItem[]) => void;
  clearItems: (scope: string) => void;
  addRecentlyUsed: (itemId: string) => void;
};
⋮----
const loadRecentlyUsed = (): string[] =>
⋮----
const saveRecentlyUsed = (ids: string[]) =>
⋮----
// Ignore storage errors.
⋮----
const hasSameMetadata = (first: PaletteItem, second: PaletteItem)

================
File: src/renderer/src/stores/commitStore.ts
================
import { createWithEqualityFn } from 'zustand/traditional';
import { shallow } from 'zustand/vanilla/shallow';
import { devtools } from 'zustand/middleware';
⋮----
interface CommitStore {
  committingMap: Map<string, boolean>;

  setCommitting: (baseDir: string, taskId: string, isCommitting: boolean) => void;
}
⋮----
const getCommitKey = (baseDir: string, taskId: string) => `$
⋮----
export const useIsCommitting = (baseDir: string, taskId: string)

================
File: src/renderer/src/stores/extensionToolsStore.ts
================
import { useShallow } from 'zustand/react/shallow';
import { createWithEqualityFn } from 'zustand/traditional';
import { devtools } from 'zustand/middleware';
import { shallow } from 'zustand/vanilla/shallow';
import { ExtensionToolInfo } from '@common/types';
import { ApplicationAPI } from '@common/api';
⋮----
const getToolsCacheKey = (projectDir?: string): string
⋮----
interface ExtensionToolsState {
  toolsInfoMap: Map<string, ExtensionToolInfo[]>;
  loadingTools: Set<string>;
  initialized: boolean;
}
⋮----
interface ExtensionToolsActions {
  loadToolsInfo: (api: ApplicationAPI, projectDir?: string, forceRefresh?: boolean) => Promise<ExtensionToolInfo[]>;
  refreshAllToolsInfo: (api: ApplicationAPI) => Promise<ExtensionToolInfo[]>;
  getToolsInfo: (projectDir?: string) => ExtensionToolInfo[] | undefined;
  isLoadingTools: (projectDir?: string) => boolean;
}
⋮----
type ExtensionToolsStore = ExtensionToolsState & ExtensionToolsActions;
⋮----
// Return cached if available and not forcing refresh
⋮----
// If a request is already in flight, return cached data immediately or wait for it
⋮----
const checkLoaded = () =>
⋮----
// eslint-disable-next-line no-console
⋮----
export const useExtensionToolsInfo = (projectDir?: string): ExtensionToolInfo[]
⋮----
export const useExtensionToolsLoading = (projectDir?: string): boolean

================
File: src/renderer/src/stores/extensionUIStore.ts
================
import { useShallow } from 'zustand/react/shallow';
import { createWithEqualityFn } from 'zustand/traditional';
import { shallow } from 'zustand/vanilla/shallow';
import { devtools } from 'zustand/middleware';
import { ExtensionUIComponent, ExtensionUIRefreshData } from '@common/types';
import { ExtensionDisplayAPI } from '@common/api';
⋮----
const getComponentsCacheKey = (placement?: string, projectDir?: string, taskId?: string): string
⋮----
export const getDataCacheKey = (extensionId: string, componentId: string, projectDir?: string, taskId?: string): string
⋮----
interface ExtensionUIState {
  componentsMap: Map<string, ExtensionUIComponent[]>;
  dataMap: Map<string, unknown>;
  dataLoadedMap: Map<string, boolean>;
  loadingComponents: Set<string>;
  loadingData: Set<string>;
  initialized: boolean;
}
⋮----
interface ExtensionUIActions {
  loadComponents: (api: ExtensionDisplayAPI, placement?: string, projectDir?: string, taskId?: string) => Promise<ExtensionUIComponent[]>;
  loadComponentData: (
    api: ExtensionDisplayAPI,
    extensionId: string,
    componentId: string,
    projectDir?: string,
    taskId?: string,
    forceRefresh?: boolean,
  ) => Promise<unknown>;
  handleRefreshEvent: (api: ExtensionDisplayAPI, data: ExtensionUIRefreshData, projectDir?: string, taskId?: string) => void;
  invalidateComponents: (placement?: string, projectDir?: string, taskId?: string) => void;
  invalidateData: (extensionId?: string, componentId?: string, projectDir?: string, taskId?: string) => void;
  getComponents: (placement?: string, projectDir?: string, taskId?: string) => ExtensionUIComponent[] | undefined;
  getComponentData: (extensionId: string, componentId: string, projectDir?: string, taskId?: string) => unknown;
  isDataLoaded: (extensionId: string, componentId: string, projectDir?: string, taskId?: string) => boolean;
  isLoadingComponents: (placement?: string, projectDir?: string, taskId?: string) => boolean;
  cleanupTaskCache: (projectDir: string, taskId: string) => void;
  cleanupProjectCache: (projectDir: string) => void;
}
⋮----
type ExtensionUIStore = ExtensionUIState & ExtensionUIActions;
⋮----
// Return cached if available
⋮----
// Prevent duplicate requests
⋮----
// Wait for existing request - poll until loaded
⋮----
const checkLoaded = () =>
⋮----
// Mark as loading
⋮----
// eslint-disable-next-line no-console
⋮----
// Return cached if available and not forcing refresh
⋮----
// Prevent duplicate requests (only block if no cached data to show)
⋮----
// If we have cached data, return it immediately (stale-while-revalidate)
⋮----
// Otherwise wait for the pending request
⋮----
// Mark as loading
⋮----
// eslint-disable-next-line no-console
⋮----
// Return cached data on error if available
⋮----
// Only handle component reloads - data refresh is handled by forceRefresh in loadComponentData
// We don't invalidate data cache to avoid flicker (stale-while-revalidate pattern)
⋮----
// Find all matching component cache keys to invalidate
// Note: We invalidate keys even if they have empty arrays, because an extension
// might now return components after settings change (e.g., toggling a feature on)
⋮----
// Only filter by extensionId if the cache has components to check against
// Empty caches should still be invalidated since the extension may now return components
⋮----
// Clear all
⋮----
// Clear all
⋮----
// If partial key, find all matching
⋮----
// Module-level action functions (no hook subscription required)
export const loadExtensionUIComponents = (api: ExtensionDisplayAPI, placement?: string, projectDir?: string, taskId?: string)
⋮----
export const loadExtensionComponentData = (
  api: ExtensionDisplayAPI,
  extensionId: string,
  componentId: string,
  projectDir?: string,
  taskId?: string,
  forceRefresh?: boolean,
)
⋮----
export const handleExtensionUIRefreshEvent = (api: ExtensionDisplayAPI, data: ExtensionUIRefreshData, projectDir?: string, taskId?: string)
⋮----
export const isExtensionUIDataLoaded = (extensionId: string, componentId: string, projectDir?: string, taskId?: string)
⋮----
export const getExtensionComponentData = (extensionId: string, componentId: string, projectDir?: string, taskId?: string)
⋮----
// Selector hooks for optimized re-renders
export const useExtensionComponents = (placement?: string, projectDir?: string, taskId?: string): ExtensionUIComponent[] | undefined =>
⋮----
export const useExtensionComponentData = (extensionId: string, componentId: string, projectDir?: string, taskId?: string): unknown =>
⋮----
export const useIsLoadingComponents = (placement?: string, projectDir?: string, taskId?: string): boolean =>
⋮----
export const cleanupTaskCache = (projectDir: string, taskId: string)
⋮----
export const cleanupProjectCache = (projectDir: string)

================
File: src/renderer/src/stores/fileEditorStore.ts
================
import { devtools } from 'zustand/middleware';
import { createWithEqualityFn } from 'zustand/traditional';
⋮----
import type { OpenEditorFile } from '@/components/Workspace/FileEditorTabs';
⋮----
type ProjectEditorState = {
  openFiles: OpenEditorFile[];
  activeFilePath: string | null;
  isEditorOpen: boolean;
};
⋮----
type FileEditorStore = {
  projectsMap: Map<string, ProjectEditorState>;

  openFile: (projectDir: string, path: string, taskId: string) => void;
  openEditor: (projectDir: string) => void;
  closeTab: (projectDir: string, path: string) => void;
  closeTabs: (projectDir: string, paths: string[]) => void;
  setActiveFile: (projectDir: string, path: string) => void;
  closeEditor: (projectDir: string) => void;
  cleanupProject: (projectDir: string) => void;
};
⋮----
type PersistedState = Record<string, Pick<ProjectEditorState, 'openFiles' | 'activeFilePath'>>;
⋮----
const loadPersistedState = (): PersistedState =>
⋮----
const savePersistedState = (state: PersistedState) =>
⋮----
// Ignore storage errors.
⋮----
const normalizeState = (state: ProjectEditorState): ProjectEditorState =>
⋮----
const getProjectState = (projectsMap: Map<string, ProjectEditorState>, projectDir: string): ProjectEditorState
⋮----
const withProjectState = (projectsMap: Map<string, ProjectEditorState>, projectDir: string, projectState: ProjectEditorState) =>
⋮----
export const useProjectEditorState = (projectDir: string): ProjectEditorState
⋮----
export const useFileEditorActions = ()

================
File: src/renderer/src/stores/overlayStore.ts
================
import { devtools } from 'zustand/middleware';
import { createWithEqualityFn } from 'zustand/traditional';
⋮----
type OverlayStore = {
  openOverlays: Set<string>;
  focusRequest: number;

  openOverlay: (id: string) => void;
  closeOverlay: (id: string) => void;
};

================
File: src/renderer/src/stores/projectStore.ts
================
import { useShallow } from 'zustand/react/shallow';
import { TaskData, DefaultTaskState } from '@common/types';
import { createWithEqualityFn } from 'zustand/traditional';
import { shallow } from 'zustand/vanilla/shallow';
import { devtools } from 'zustand/middleware';
⋮----
interface ProjectStore {
  projectTasksMap: Map<string, TaskData[]>;

  setProjectTasks: (projectBaseDir: string, tasks: TaskData[]) => void;
  updateProjectTask: (projectBaseDir: string, taskData: TaskData) => void;
  addProjectTask: (projectBaseDir: string, taskData: TaskData) => void;
  removeProjectTask: (projectBaseDir: string, taskId: string) => void;
  clearProjectTasks: (projectBaseDir: string) => void;
}
⋮----
// Module-level action functions (no hook subscription required)
export const setProjectTasks = (projectBaseDir: string, tasks: TaskData[])
⋮----
export const updateProjectTask = (projectBaseDir: string, taskData: TaskData)
⋮----
export const addProjectTask = (projectBaseDir: string, taskData: TaskData)
⋮----
export const removeProjectTask = (projectBaseDir: string, taskId: string)
⋮----
export const clearProjectTasks = (projectBaseDir: string)
⋮----
export const useProjectTasks = (projectBaseDir: string)
⋮----
export const useProjectProcessingState = (projectBaseDir: string)
⋮----
export const useProjectTaskWorktreePath = (projectBaseDir: string, taskId: string)

================
File: src/renderer/src/stores/settingsNavigationStore.ts
================
import { devtools } from 'zustand/middleware';
import { create } from 'zustand';
⋮----
export type SettingsPageInfo = {
  pageId: string;
  options?: Record<string, unknown>;
};
⋮----
type SettingsNavigationStore = {
  settingsPage: SettingsPageInfo | null;
  openSettingsPage: (pageId: string, options?: Record<string, unknown>) => void;
  closeSettings: () => void;
};
⋮----
export const openSettingsPage = (pageId: string, options?: Record<string, unknown>) =>
⋮----
export const closeSettings = () =>

================
File: src/renderer/src/stores/settingsStore.ts
================
import { useCallback } from 'react';
import { createWithEqualityFn } from 'zustand/traditional';
import { shallow } from 'zustand/vanilla/shallow';
import { devtools } from 'zustand/middleware';
import { Font, SettingsData, Theme } from '@common/types';
⋮----
import { useApi } from '@/contexts/ApiContext';
⋮----
interface SettingsState {
  settings: SettingsData | null;
  theme: Theme | null;
  font: Font | null;
  fontSize: number | null;
  readonlyExtensionUi: boolean | null;
}
⋮----
interface SettingsActions {
  setSettingsState: (settings: SettingsData) => void;
  setThemeValue: (theme: Theme) => void;
  setFontValue: (font: Font) => void;
  setFontSizeValue: (fontSize: number) => void;
}
⋮----
type SettingsStore = SettingsState & SettingsActions;
⋮----
export const useSaveSettings = () =>
⋮----
// eslint-disable-next-line no-console
⋮----
export const setTheme = (theme: Theme)
⋮----
export const setFont = (font: Font)
⋮----
export const setFontSize = (fontSize: number)

================
File: src/renderer/src/stores/taskFilesStore.ts
================
import { useShallow } from 'zustand/react/shallow';
import { createWithEqualityFn } from 'zustand/traditional';
import { shallow } from 'zustand/vanilla/shallow';
import { devtools } from 'zustand/middleware';
⋮----
interface TaskFilesStore {
  allFilesMap: Map<string, string[]>;
  autocompletionWordsMap: Map<string, string[]>;
  usageMap: Map<string, Set<string>>;
  taskDirByTaskIdMap: Map<string, string>;

  setAllFiles: (taskId: string, taskDir: string, allFiles: string[]) => void;
  setAutocompletionWords: (taskId: string, taskDir: string, autocompletionWords: string[]) => void;
  releaseTaskFiles: (taskId: string) => void;
}
⋮----
const updateTaskDirTracking = (
  state: TaskFilesStore,
  taskId: string,
  newTaskDir: string,
): Pick<TaskFilesStore, 'allFilesMap' | 'autocompletionWordsMap' | 'usageMap' | 'taskDirByTaskIdMap'> =>
⋮----
export const setTaskAllFiles = (taskId: string, taskDir: string, allFiles: string[])
⋮----
export const setTaskAutocompletionWords = (taskId: string, taskDir: string, autocompletionWords: string[])
⋮----
export const releaseTaskFiles = (taskId: string)
⋮----
export const useTaskAllFiles = (taskDir: string | undefined)
⋮----
export const useTaskAutocompletionWords = (taskDir: string | undefined)

================
File: src/renderer/src/stores/taskSidebarStore.ts
================
import { devtools } from 'zustand/middleware';
import { createWithEqualityFn } from 'zustand/traditional';
⋮----
type TaskSidebarStore = {
  showArchived: boolean;
  setShowArchived: (show: boolean) => void;
};

================
File: src/renderer/src/stores/taskStore.ts
================
import { useShallow } from 'zustand/react/shallow';
import { ContextFile, ModelsData, QueuedPromptData, QuestionData, TodoItem, TokensInfoData, Message, isLogMessage, isLoadingMessage } from '@common/types';
import { createWithEqualityFn } from 'zustand/traditional';
import { shallow } from 'zustand/vanilla/shallow';
import { devtools } from 'zustand/middleware';
⋮----
export interface TaskState {
  loading: boolean;
  loaded: boolean;
  tokensInfo: TokensInfoData | null;
  question: QuestionData | null;
  todoItems: TodoItem[];
  contextFiles: ContextFile[];
  aiderModelsData: ModelsData | null;
  lastActiveAt: Date | null;
  queuedPrompts: QueuedPromptData[];
  canUndoContextChange: boolean;
}
⋮----
interface TaskStore {
  taskStateMap: Map<string, TaskState>;
  taskMessagesMap: Map<string, Message[]>;

  updateTaskState: (taskId: string, updates: Partial<TaskState>) => void;
  setMessages: (taskId: string, updateMessages: (prev: Message[]) => Message[]) => void;
  setTodoItems: (taskId: string, updateTodoItems: (prev: TodoItem[]) => TodoItem[]) => void;
  setTokensInfo: (taskId: string, tokensInfo: TokensInfoData | null) => void;
  setQuestion: (taskId: string, question: QuestionData | null) => void;
  setAiderModelsData: (taskId: string, modelsData: ModelsData | null) => void;
  setQueuedPrompts: (taskId: string, queuedPrompts: QueuedPromptData[]) => void;
  clearSession: (taskId: string) => void;
  unloadTask: (taskId: string) => void;
  unloadTasks: (taskIds: string[]) => void;
}
⋮----
// we are already in a pending state, no need to update
⋮----
// Module-level action functions (no hook subscription required)
export const updateTaskState = (taskId: string, updates: Partial<TaskState>)
⋮----
export const setMessages = (taskId: string, updateMessages: (prev: Message[])
⋮----
export const removeMessageActionId = (taskId: string, messageId: string, actionId: string)
⋮----
export const setTodoItems = (taskId: string, updateTodoItems: (prev: TodoItem[])
⋮----
export const setTokensInfo = (taskId: string, tokensInfo: TokensInfoData | null)
⋮----
export const setQuestion = (taskId: string, question: QuestionData | null)
⋮----
export const setAiderModelsData = (taskId: string, modelsData: ModelsData | null)
⋮----
export const setQueuedPrompts = (taskId: string, queuedPrompts: QueuedPromptData[])
⋮----
export const clearSession = (taskId: string)
⋮----
export const unloadTask = (taskId: string)
⋮----
export const unloadTasks = (taskIds: string[])
⋮----
export const touchTaskActivity = (taskId: string) =>
⋮----
export const useOptimizedTaskState = (taskId: string)
⋮----
export const useTaskTokensInfo = (taskId: string)
⋮----
export const useTaskFileTokensInfo = (taskId: string)
⋮----
export const useTaskQuestion = (taskId: string)
⋮----
export const useTaskTodoItems = (taskId: string)
⋮----
export const useTaskQueuedPrompts = (taskId: string)
⋮----
export const useTaskMessages = (taskId: string)
⋮----
export const useTaskLoaded = (taskId: string)

================
File: src/renderer/src/stores/terminalStore.ts
================
import { useShallow } from 'zustand/react/shallow';
import { createWithEqualityFn } from 'zustand/traditional';
import { shallow } from 'zustand/vanilla/shallow';
import { devtools } from 'zustand/middleware';
import { v4 as uuidv4 } from 'uuid';
⋮----
export type TerminalTab = {
  id: string;
  ptyId: string | null;
};
⋮----
export const getSessionKey = (baseDir: string, taskId: string): string => `$
⋮----
interface TerminalState {
  tabsMap: Map<string, TerminalTab[]>;
  activeTabMap: Map<string, string | null>;
  visibleMap: Map<string, boolean>;
}
⋮----
interface TerminalActions {
  addTab: (sessionKey: string) => string;
  removeTab: (sessionKey: string, tabId: string) => void;
  setActiveTab: (sessionKey: string, tabId: string) => void;
  setVisible: (sessionKey: string, visible: boolean) => void;
  setTabPtyId: (sessionKey: string, tabId: string, ptyId: string | null) => void;
  handlePtyExit: (ptyId: string) => void;
}
⋮----
type TerminalStore = TerminalState & TerminalActions;
⋮----
// Module-level action functions (no hook subscription required)
export const addTerminalTab = (sessionKey: string)
⋮----
export const removeTerminalTab = (sessionKey: string, tabId: string)
⋮----
export const setActiveTerminalTab = (sessionKey: string, tabId: string)
⋮----
export const setTerminalVisible = (sessionKey: string, visible: boolean)
⋮----
export const toggleTerminalVisible = (sessionKey: string)
⋮----
export const setTabPtyId = (sessionKey: string, tabId: string, ptyId: string | null)
⋮----
export const handlePtyExit = (ptyId: string)
⋮----
// Selector hooks for optimized re-renders
export const useTerminalTabs = (sessionKey: string): TerminalTab[]
⋮----
export const useActiveTerminalTabId = (sessionKey: string): string | null
⋮----
export const useTerminalVisible = (sessionKey: string): boolean

================
File: src/renderer/src/themes/theme-aurora.scss
================
.theme-aurora {

  --color-bg-primary: #0B1426;
  --color-bg-primary-light: #132038;
  --color-bg-primary-light-strong: #13203880;
  --color-bg-secondary: #1A2C4A;
  --color-bg-secondary-light: #22385C;
  --color-bg-secondary-light-strongest: #22385Cf2;
  --color-bg-tertiary: #2A446E;
  --color-bg-tertiary-emphasis: #D4A57440;
  --color-bg-tertiary-strong: #D4A57460;
  --color-bg-fourth: #345080;
  --color-bg-fourth-muted: #7BC4C433;
  --color-bg-fourth-emphasis: #7BC4C44C;
  --color-bg-fifth: #3E5C92;
  --color-bg-selection: #D4A57450;
  --color-bg-code-block: #08101C;

  --color-bg-diff-viewer-old-primary: #4A2A2A;
  --color-bg-diff-viewer-old-secondary: #3A1F1F;
  --color-bg-diff-viewer-new-primary: #2A4A3A;
  --color-bg-diff-viewer-new-secondary: #1F3A2A;

  --color-text-primary: #F8FBFF;
  --color-text-secondary: #E8F4FF;
  --color-text-tertiary: #D8EDFF;
  --color-text-muted-light: #C8E6FF;
  --color-text-muted: #7BC4C4;
  --color-text-muted-dark: #3E5C92;
  --color-text-dark: #2A446E;

  --color-border-dark: #1A2C4A;
  --color-border-dark-light: #22385C;
  --color-border-dark-light-strong: #22385C80;
  --color-border-default-dark: #2A446E;
  --color-border-default: #345080;
  --color-border-accent: #D4A574;
  --color-border-light: #7BC4C4;

  --color-accent-primary: #D4A574;
  --color-accent-secondary: #7BC4C4;
  --color-accent-light: #E8F4FF;

  --color-success: #7BC4C4;
  --color-success-light: #9DD4D4;
  --color-success-subtle: #7BC4C41a;
  --color-success-muted: #7BC4C433;
  --color-success-emphasis: #7BC4C44c;

  --color-warning: #D4A574;
  --color-warning-subtle: #D4A57419;
  --color-warning-emphasis: #D4A5744c;
  --color-warning-light: #E4B584;
  --color-warning-text: #0B1426;

  --color-error: #E07B7B;
  --color-error-dark: #C06565;
  --color-error-subtle: #E07B7B1a;
  --color-error-muted: #E07B7B26;
  --color-error-emphasis: #E07B7B4c;
  --color-error-strong: #E07B7B80;
  --color-error-light: #E89B9B;
  --color-error-lighter: #F0BBBB;
  --color-error-lightest: #F8DBDB;

  --color-info: #5A8BC4;
  --color-info-light: #7AA5D4;
  --color-info-lighter: #9ABFE4;
  --color-info-lightest: #BAD9F4;
  --color-info-subtle: #5A8BC41a;
  --color-info-light-muted: #5A8BC433;
  --color-info-light-emphasis: #5A8BC44c;

  // Button-specific colors
  --color-button-primary: #D4A574;
  --color-button-primary-light: #E4B584;
  --color-button-primary-subtle: #D4A57419;
  --color-button-primary-emphasis: #D4A5744c;
  --color-button-primary-text: #0B1426;

  --color-button-secondary: #5A8BC4;
  --color-button-secondary-light: #7AA5D4;
  --color-button-secondary-subtle: #5A8BC41a;
  --color-button-secondary-emphasis: #5A8BC44c;
  --color-button-secondary-text: #F8FBFF;

  --color-button-danger: #E07B7B;
  --color-button-danger-light: #E89B9B;
  --color-button-danger-subtle: #E07B7B1a;
  --color-button-danger-emphasis: #E07B7B4c;
  --color-button-danger-text: #F8FBFF;

  --color-input-bg: #132038;
  --color-input-border: #D4A574;
  --color-input-text: #F8FBFF;

  --color-agent-auto-approve: #7BC4C4;
  --color-agent-aider-tools: #D4A574;
  --color-agent-power-tools: #B47BC4;
  --color-agent-todo-tools: #5A8BC4;
  --color-agent-tasks-tools: #7BC4C4;
  --color-agent-memory-tools: #D4A5B4;
  --color-agent-skills-tools: #76cc99;
  --color-agent-subagents-tools: #55aa7f;
  --color-agent-context-files: #E4B584;
  --color-agent-repo-map: #9DD4D4;
  --color-agent-ai-request: #E07B7B;
  --color-agent-sub-agent: #7B9DE0;
}

================
File: src/renderer/src/themes/theme-botanical-garden-dark.scss
================
.theme-botanical-garden-dark {

  --color-bg-primary: #121513;
  --color-bg-primary-light: #171c19;
  --color-bg-primary-light-strong: #171c1980;
  --color-bg-secondary: #1c241f;
  --color-bg-secondary-light: #232e27;
  --color-bg-secondary-light-strongest: #232e27f2;
  --color-bg-tertiary: #2a3a30;
  --color-bg-tertiary-emphasis: #2a3a304c;
  --color-bg-tertiary-strong: #2a3a307f;
  --color-bg-fourth: #314738;
  --color-bg-fourth-muted: #31473833;
  --color-bg-fourth-emphasis: #3147384c;
  --color-bg-fifth: #3d5a46;
  --color-bg-selection: #3b4b40;
  --color-bg-code-block: #141616;

  --color-bg-diff-viewer-old-primary: #3a1d16;
  --color-bg-diff-viewer-old-secondary: #2a1510;
  --color-bg-diff-viewer-new-primary: #0c2617;
  --color-bg-diff-viewer-new-secondary: #081c11;

  --color-text-primary: #f5f3ed;
  --color-text-secondary: #c9c6bf;
  --color-text-tertiary: #b5b2ab;
  --color-text-muted-light: #9a9791;
  --color-text-muted: #6a726c;
  --color-text-muted-dark: #3f4d44;
  --color-text-dark: #2a3a30;

  --color-border-dark: #1c241f;
  --color-border-dark-light: #232e27;
  --color-border-dark-light-strong: #232e2780;
  --color-border-default-dark: #2a3a30;
  --color-border-default: #314738;
  --color-border-accent: #4a7c59;
  --color-border-light: #c9c6bf;

  --color-accent-primary: #4a7c59;
  --color-accent-secondary: #5d8a6a;
  --color-accent-light: #83ad8e;

  --color-success: #4a7c59;
  --color-success-light: #83ad8e;
  --color-success-subtle: #4a7c591a;
  --color-success-muted: #4a7c5933;
  --color-success-emphasis: #4a7c594c;

  --color-warning: #f9a620;
  --color-warning-subtle: #f9a62019;
  --color-warning-emphasis: #f9a6204c;
  --color-warning-light: #fbbf4b;
  --color-warning-text: #1a1a1a;

  --color-error: #b7472a;
  --color-error-dark: #7a2a18;
  --color-error-subtle: #b7472a1a;
  --color-error-muted: #b7472a26;
  --color-error-emphasis: #b7472a4c;
  --color-error-strong: #b7472a80;
  --color-error-light: #d26b52;
  --color-error-lighter: #e39a87;
  --color-error-lightest: #f0c3b7;

  --color-info: #3b82f6;
  --color-info-light: #60a5fa;
  --color-info-lighter: #93c5fd;
  --color-info-lightest: #bfdbfe;
  --color-info-subtle: #3b82f61a;
  --color-info-light-muted: #3b82f633;
  --color-info-light-emphasis: #3b82f64c;

  // Button-specific colors
  --color-button-primary: #f9a620;
  --color-button-primary-light: #fbbf4b;
  --color-button-primary-subtle: #f9a62019;
  --color-button-primary-emphasis: #f9a6204c;
  --color-button-primary-text: #1a1a1a;

  --color-button-secondary: #4a7c59;
  --color-button-secondary-light: #83ad8e;
  --color-button-secondary-subtle: #4a7c591a;
  --color-button-secondary-emphasis: #4a7c594c;
  --color-button-secondary-text: #f5f3ed;

  --color-button-danger: #b7472a;
  --color-button-danger-light: #d26b52;
  --color-button-danger-subtle: #b7472a1a;
  --color-button-danger-emphasis: #b7472a4c;
  --color-button-danger-text: #f5f3ed;

  --color-input-bg: #171c19;
  --color-input-border: #314738;
  --color-input-text: #f5f3ed;

  --color-agent-auto-approve: #83ad8e;
  --color-agent-aider-tools: #f9a620;
  --color-agent-power-tools: #c084fc;
  --color-agent-todo-tools: #38bdf8;
  --color-agent-tasks-tools: #10b981;
  --color-agent-memory-tools: #f472b6;
  --color-agent-skills-tools: #c7d392;
  --color-agent-subagents-tools: #55aa7f;
  --color-agent-context-files: #f9a620;
  --color-agent-repo-map: #93c5fd;
  --color-agent-ai-request: #b7472a;
  --color-agent-sub-agent: #4a7c59;
}

================
File: src/renderer/src/themes/theme-botanical-garden.scss
================
.theme-botanical-garden {

  --color-bg-primary: #f5f3ed;
  --color-bg-primary-light: #faf8f2;
  --color-bg-primary-light-strong: #faf8f280;
  --color-bg-secondary: #f0ede5;
  --color-bg-secondary-light: #ebe7dd;
  --color-bg-secondary-light-strongest: #ebe7ddf2;
  --color-bg-tertiary: #e8e4da;
  --color-bg-tertiary-emphasis: #e8e4da4c;
  --color-bg-tertiary-strong: #e8e4da7f;
  --color-bg-fourth: #e3dfd4;
  --color-bg-fourth-muted: #e3dfd433;
  --color-bg-fourth-emphasis: #e3dfd44c;
  --color-bg-fifth: #ded9ce;
  --color-bg-selection: #4a7c5933;
  --color-bg-code-block: #2d3a2f;

  --color-bg-diff-viewer-old-primary: #f4e8e6;
  --color-bg-diff-viewer-old-secondary: #f0e0dd;
  --color-bg-diff-viewer-new-primary: #e8f4e8;
  --color-bg-diff-viewer-new-secondary: #ddf0dd;

  --color-text-primary: #2d3a2f;
  --color-text-secondary: #3d4a3f;
  --color-text-tertiary: #4d5a4f;
  --color-text-muted-light: #6d7a6f;
  --color-text-muted: #8d9a8f;
  --color-text-muted-dark: #adbaaf;
  --color-text-dark: #4a7c59;

  --color-border-dark: #d4d0c5;
  --color-border-dark-light: #dad6cb;
  --color-border-dark-light-strong: #dad6cb80;
  --color-border-default-dark: #e0dccf;
  --color-border-default: #e6e2d5;
  --color-border-accent: #4a7c59;
  --color-border-light: #ece8db;

  --color-accent-primary: #4a7c59;
  --color-accent-secondary: #f9a620;
  --color-accent-light: #6d8c79;

  --color-success: #4a7c59;
  --color-success-light: #5d8a6a;
  --color-success-subtle: #4a7c591a;
  --color-success-muted: #4a7c5933;
  --color-success-emphasis: #4a7c594c;

  --color-warning: #f9a620;
  --color-warning-subtle: #f9a62019;
  --color-warning-emphasis: #f9a6204c;
  --color-warning-light: #fab440;
  --color-warning-text: #2d3a2f;

  --color-error: #b7472a;
  --color-error-dark: #953722;
  --color-error-subtle: #b7472a1a;
  --color-error-muted: #b7472a26;
  --color-error-emphasis: #b7472a4c;
  --color-error-strong: #b7472a80;
  --color-error-light: #c75f42;
  --color-error-lighter: #d7775a;
  --color-error-lightest: #e78f72;

  --color-info: #4a7a9a;
  --color-info-light: #5d8fb0;
  --color-info-lighter: #7aa3c2;
  --color-info-lightest: #97b8d4;
  --color-info-subtle: #4a7a9a1a;
  --color-info-light-muted: #4a7a9a33;
  --color-info-light-emphasis: #4a7a9a4c;

  // Button-specific colors
  --color-button-primary: #4a7c59;
  --color-button-primary-light: #5d8a6a;
  --color-button-primary-subtle: #4a7c5919;
  --color-button-primary-emphasis: #4a7c594c;
  --color-button-primary-text: #f5f3ed;

  --color-button-secondary: #f9a620;
  --color-button-secondary-light: #fab440;
  --color-button-secondary-subtle: #f9a6201a;
  --color-button-secondary-emphasis: #f9a6204c;
  --color-button-secondary-text: #2d3a2f;

  --color-button-danger: #b7472a;
  --color-button-danger-light: #c75f42;
  --color-button-danger-subtle: #b7472a1a;
  --color-button-danger-emphasis: #b7472a4c;
  --color-button-danger-text: #f5f3ed;

  --color-input-bg: #faf8f2;
  --color-input-border: #d4d0c5;
  --color-input-text: #2d3a2f;

  --color-agent-auto-approve: #4a7c59;
  --color-agent-aider-tools: #f9a620;
  --color-agent-power-tools: #8a6a9a;
  --color-agent-todo-tools: #4a7a9a;
  --color-agent-tasks-tools: #4a9a6a;
  --color-agent-memory-tools: #8a6a4a;
  --color-agent-skills-tools: #7dad71;
  --color-agent-subagents-tools: #55aa7f;
  --color-agent-context-files: #9a8a4a;
  --color-agent-repo-map: #6a8a9a;
  --color-agent-ai-request: #b7472a;
  --color-agent-sub-agent: #4a6a9a;
}

================
File: src/renderer/src/themes/theme-bw.scss
================
.theme-bw {

  --color-bg-primary: #ffffff;
  --color-bg-primary-light: #f8f8f8;
  --color-bg-primary-light-strong: #f8f8f880;
  --color-bg-secondary: #e0e0e0;
  --color-bg-secondary-light: #d0d0d0;
  --color-bg-secondary-light-strongest: #d0d0d0f2;
  --color-bg-tertiary: #b0b0b0;
  --color-bg-tertiary-emphasis: #b0b0b04c;
  --color-bg-tertiary-strong: #b0b0b07f;
  --color-bg-fourth: #808080;
  --color-bg-fourth-muted: #80808033;
  --color-bg-fourth-emphasis: #8080804c;
  --color-bg-fifth: #404040;
  --color-bg-selection: #b0b0b0;
  --color-bg-code-block: #f0f0f0;

  --color-bg-diff-viewer-old-primary: #e0e0e0;
  --color-bg-diff-viewer-old-secondary: #d0d0d0;
  --color-bg-diff-viewer-new-primary: #f0f0f0;
  --color-bg-diff-viewer-new-secondary: #e8e8e8;

  --color-text-primary: #000000;
  --color-text-secondary: #202020;
  --color-text-tertiary: #404040;
  --color-text-muted-light: #606060;
  --color-text-muted: #808080;
  --color-text-muted-dark: #a0a0a0;
  --color-text-dark: #000000;

  --color-border-dark: #e0e0e0;
  --color-border-dark-light: #d0d0d0;
  --color-border-dark-light-strong: #d0d0d080;
  --color-border-default-dark: #b0b0b0;
  --color-border-default: #808080;
  --color-border-accent: #404040;
  --color-border-light: #000000;

  --color-accent-primary: #404040;
  --color-accent-secondary: #808080;
  --color-accent-light: #000000;

  --color-success: #808080;
  --color-success-light: #a0a0a0;
  --color-success-subtle: #8080801a;
  --color-success-muted: #80808033;
  --color-success-emphasis: #8080804c;

  --color-warning: #606060;
  --color-warning-subtle: #60606019;
  --color-warning-emphasis: #6060604c;
  --color-warning-light: #707070;
  --color-warning-text: #ffffff;

  --color-error: #000000;
  --color-error-dark: #000000;
  --color-error-subtle: #0000001a;
  --color-error-muted: #00000026;
  --color-error-emphasis: #0000004c;
  --color-error-strong: #00000080;
  --color-error-light: #202020;
  --color-error-lighter: #404040;
  --color-error-lightest: #606060;

  --color-info: #404040;
  --color-info-light: #606060;
  --color-info-lighter: #808080;
  --color-info-lightest: #a0a0a0;
  --color-info-subtle: #4040401a;
  --color-info-light-muted: #40404033;
  --color-info-light-emphasis: #4040404c;

  // Button-specific colors
  --color-button-primary: #606060;
  --color-button-primary-light: #707070;
  --color-button-primary-subtle: #60606019;
  --color-button-primary-emphasis: #6060604c;
  --color-button-primary-text: #ffffff;

  --color-button-secondary: #404040;
  --color-button-secondary-light: #606060;
  --color-button-secondary-subtle: #4040401a;
  --color-button-secondary-emphasis: #4040404c;
  --color-button-secondary-text: #000000;

  --color-button-danger: #000000;
  --color-button-danger-light: #202020;
  --color-button-danger-subtle: #0000001a;
  --color-button-danger-emphasis: #0000004c;
  --color-button-danger-text: #000000;

  --color-input-bg: #f8f8f8;
  --color-input-border: #808080;
  --color-input-text: #000000;

  --color-agent-auto-approve: #a0a0a0;
  --color-agent-aider-tools: #808080;
  --color-agent-power-tools: #606060;
  --color-agent-todo-tools: #404040;
  --color-agent-tasks-tools: #707070;
  --color-agent-memory-tools: #505050;
  --color-agent-skills-tools: #404040;
  --color-agent-subagents-tools: #55aa7f;
  --color-agent-context-files: #202020;
  --color-agent-repo-map: #000000;
  --color-agent-ai-request: #505050;
  --color-agent-sub-agent: #303030;
}

================
File: src/renderer/src/themes/theme-cappuccino.scss
================
.theme-cappuccino {

  --color-bg-primary: #ede4d3;
  --color-bg-primary-light: #f0e7d6;
  --color-bg-primary-light-strong: #f0e7d680;
  --color-bg-secondary: #e6d7c3;
  --color-bg-secondary-light: #e9dac6;
  --color-bg-secondary-light-strongest: #e9dac6f2;
  --color-bg-tertiary: #dcc9b0;
  --color-bg-tertiary-emphasis: #dcc9b04c;
  --color-bg-tertiary-strong: #dcc9b07f;
  --color-bg-fourth: #d2bb9d;
  --color-bg-fourth-muted: #d2bb9d33;
  --color-bg-fourth-emphasis: #d2bb9d4c;
  --color-bg-fifth: #c4a882;
  --color-bg-selection: #dcc9b0;
  --color-bg-code-block: #f2e9dc;

  --color-bg-diff-viewer-old-primary: #fee2e2;
  --color-bg-diff-viewer-old-secondary: #fecaca;
  --color-bg-diff-viewer-new-primary: #dcfce7;
  --color-bg-diff-viewer-new-secondary: #bbf7d0;

  --color-text-primary: #2d1f16;
  --color-text-secondary: #4a3426;
  --color-text-tertiary: #5d4a36;
  --color-text-muted-light: #8d7a64;
  --color-text-muted: #9d8a74;
  --color-text-muted-dark: #caa590;
  --color-text-dark: #2d1f16;

  --color-border-dark: #dcc9b0;
  --color-border-dark-light: #d2bb9d;
  --color-border-dark-light-strong: #c4a88280;
  --color-border-default-dark: #c4a882;
  --color-border-default: #937865;
  --color-border-accent: #5d4a36;
  --color-border-light: #4a3426;

  --color-accent-primary: #8b4513;
  --color-accent-secondary: #c4a882;
  --color-accent-light: #4a3426;

  --color-success: #22c55e;
  --color-success-light: #4ade80;
  --color-success-subtle: #22c55e1a;
  --color-success-muted: #22c55e33;
  --color-success-emphasis: #22c55e4c;

  --color-warning: #d97706;
  --color-warning-subtle: #d9770619;
  --color-warning-emphasis: #d977064c;
  --color-warning-light: #f59e0b;
  --color-warning-text: #ede4d3;

  --color-error: #ef4444;
  --color-error-dark: #991b1b;
  --color-error-subtle: #ef44441a;
  --color-error-muted: #ef444426;
  --color-error-emphasis: #ef44444c;
  --color-error-strong: #ef444480;
  --color-error-light: #f87171;
  --color-error-lighter: #fca5a5;
  --color-error-lightest: #fecaca;

  --color-info: #2563eb;
  --color-info-light: #3b82f6;
  --color-info-lighter: #60a5fa;
  --color-info-lightest: #93c5fd;
  --color-info-subtle: #2563eb1a;
  --color-info-light-muted: #2563eb33;
  --color-info-light-emphasis: #2563eb4c;

  // Button-specific colors
  --color-button-primary: #8b4513;
  --color-button-primary-light: #9f664f;
  --color-button-primary-subtle: #d9770619;
  --color-button-primary-emphasis: #d977064c;
  --color-button-primary-text: #ede4d3;

  --color-button-secondary: #2563eb;
  --color-button-secondary-light: #3b82f6;
  --color-button-secondary-subtle: #2563eb1a;
  --color-button-secondary-emphasis: #2563eb4c;
  --color-button-secondary-text: #ede4d3;

  --color-button-danger: #ef4444;
  --color-button-danger-light: #f87171;
  --color-button-danger-subtle: #ef44441a;
  --color-button-danger-emphasis: #ef44444c;
  --color-button-danger-text: #ede4d3;

  --color-input-bg: #f2e9dc;
  --color-input-border: #c4a882;
  --color-input-text: #2d1f16;

  --color-agent-auto-approve: #4ade80;
  --color-agent-aider-tools: #fb923c;
  --color-agent-power-tools: #c084fc;
  --color-agent-todo-tools: #38bdf8;
  --color-agent-tasks-tools: #6ee7b7;
  --color-agent-memory-tools: #f9a8d4;
  --color-agent-skills-tools: #79acac;
  --color-agent-subagents-tools: #55aa7f;
  --color-agent-context-files: #facc15;
  --color-agent-repo-map: #60a5fa;
  --color-agent-ai-request: #d97706;
  --color-agent-sub-agent: #7c3aed;
}

================
File: src/renderer/src/themes/theme-charcoal.scss
================
.theme-charcoal {

  --color-bg-primary: #1a1a1a;
  --color-bg-primary-light: #242424;
  --color-bg-primary-light-strong: #24242480;
  --color-bg-secondary: #2d2d2d;
  --color-bg-secondary-light: #363636;
  --color-bg-secondary-light-strongest: #363636f2;
  --color-bg-tertiary: #404040;
  --color-bg-tertiary-emphasis: #4040404c;
  --color-bg-tertiary-strong: #4040407f;
  --color-bg-fourth: #4a4a4a;
  --color-bg-fourth-muted: #4a4a4a33;
  --color-bg-fourth-emphasis: #4a4a4a4c;
  --color-bg-fifth: #545454;
  --color-bg-selection: #404040;
  --color-bg-code-block: #141414;

  --color-bg-diff-viewer-old-primary: #3f1d25;
  --color-bg-diff-viewer-old-secondary: #2c141a;
  --color-bg-diff-viewer-new-primary: #022c22;
  --color-bg-diff-viewer-new-secondary: #011f19;

  --color-text-primary: #ffffff;
  --color-text-secondary: #e8e8e8;
  --color-text-tertiary: #cccccc;
  --color-text-muted-light: #a8a8a8;
  --color-text-muted: #808080;
  --color-text-muted-dark: #666666;
  --color-text-dark: #404040;

  --color-border-dark: #2d2d2d;
  --color-border-dark-light: #363636;
  --color-border-dark-light-strong: #36363680;
  --color-border-default-dark: #404040;
  --color-border-default: #4a4a4a;
  --color-border-accent: #545454;
  --color-border-light: #808080;

  --color-accent-primary: #d4af37;
  --color-accent-secondary: #c0c0c0;
  --color-accent-light: #e8e8e8;

  --color-success: #4ade80;
  --color-success-light: #86efac;
  --color-success-subtle: #4ade801a;
  --color-success-muted: #4ade8033;
  --color-success-emphasis: #4ade804c;

  --color-warning: #fbbf24;
  --color-warning-subtle: #fbbf2419;
  --color-warning-emphasis: #fbbf244c;
  --color-warning-light: #fcd34d;
  --color-warning-text: #1a1a1a;

  --color-error: #f87171;
  --color-error-dark: #dc2626;
  --color-error-subtle: #f871711a;
  --color-error-muted: #f8717126;
  --color-error-emphasis: #f871714c;
  --color-error-strong: #f8717180;
  --color-error-light: #fca5a5;
  --color-error-lighter: #fecaca;
  --color-error-lightest: #fee2e2;

  --color-info: #60a5fa;
  --color-info-light: #93c5fd;
  --color-info-lighter: #bfdbfe;
  --color-info-lightest: #dbeafe;
  --color-info-subtle: #60a5fa1a;
  --color-info-light-muted: #60a5fa33;
  --color-info-light-emphasis: #60a5fa4c;

  // Button-specific colors
  --color-button-primary: #fbbf24;
  --color-button-primary-light: #fcd34d;
  --color-button-primary-subtle: #fbbf2419;
  --color-button-primary-emphasis: #fbbf244c;
  --color-button-primary-text: #1a1a1a;

  --color-button-secondary: #60a5fa;
  --color-button-secondary-light: #93c5fd;
  --color-button-secondary-subtle: #60a5fa1a;
  --color-button-secondary-emphasis: #60a5fa4c;
  --color-button-secondary-text: #ffffff;

  --color-button-danger: #f87171;
  --color-button-danger-light: #fca5a5;
  --color-button-danger-subtle: #f871711a;
  --color-button-danger-emphasis: #f871714c;
  --color-button-danger-text: #ffffff;

  --color-input-bg: #242424;
  --color-input-border: #4a4a4a;
  --color-input-text: #ffffff;

  --color-agent-auto-approve: #4ade80;
  --color-agent-aider-tools: #fb923c;
  --color-agent-power-tools: #c084fc;
  --color-agent-todo-tools: #60a5fa;
  --color-agent-tasks-tools: #34d399;
  --color-agent-memory-tools: #f472b6;
  --color-agent-skills-tools: #447da6;
  --color-agent-subagents-tools: #55aa7f;
  --color-agent-context-files: #facc15;
  --color-agent-repo-map: #93c5fd;
  --color-agent-ai-request: #ef4444;
  --color-agent-sub-agent: #3b82f6;
}

================
File: src/renderer/src/themes/theme-crimson.scss
================
.theme-crimson {

  --color-bg-primary: #120E0F;
  --color-bg-primary-light: #1A1516;
  --color-bg-primary-light-strong: #1A151680;
  --color-bg-secondary: #231C1E;
  --color-bg-secondary-light: #2C2326;
  --color-bg-secondary-light-strongest: #2C2326F2;
  --color-bg-tertiary: #3A2E31;
  --color-bg-tertiary-emphasis: #C4505A40;
  --color-bg-tertiary-strong: #C4505A60;
  --color-bg-fourth: #463A3D;
  --color-bg-fourth-muted: #C4505A33;
  --color-bg-fourth-emphasis: #C4505A4C;
  --color-bg-fifth: #55474A;
  --color-bg-selection: #C4505A50;
  --color-bg-code-block: #0E0A0B;

  --color-bg-diff-viewer-old-primary: #3D1F21;
  --color-bg-diff-viewer-old-secondary: #2B1517;
  --color-bg-diff-viewer-new-primary: #1F3D25;
  --color-bg-diff-viewer-new-secondary: #152B1A;

  --color-text-primary: #F2E9EA;
  --color-text-secondary: #B0A1A4;
  --color-text-tertiary: #8F8084;
  --color-text-muted-light: #726468;
  --color-text-muted: #55474A;
  --color-text-muted-dark: #463A3D;
  --color-text-dark: #2C2326;

  --color-border-dark: #231C1E;
  --color-border-dark-light: #2C2326;
  --color-border-dark-light-strong: #2C232680;
  --color-border-default-dark: #3A2E31;
  --color-border-default: #463A3D;
  --color-border-accent: #C4505A;
  --color-border-light: #726468;

  --color-accent-primary: #C4505A;
  --color-accent-secondary: #B0A1A4;
  --color-accent-light: #F2E9EA;

  --color-success: #5DB86C;
  --color-success-light: #7ECF8A;
  --color-success-subtle: #5DB86C1a;
  --color-success-muted: #5DB86C33;
  --color-success-emphasis: #5DB86C4c;

  --color-warning: #D49A54;
  --color-warning-subtle: #D49A5419;
  --color-warning-emphasis: #D49A544c;
  --color-warning-light: #DEAE6E;
  --color-warning-text: #120E0F;

  --color-error: #D0575B;
  --color-error-dark: #9E3A3E;
  --color-error-subtle: #D0575B1a;
  --color-error-muted: #D0575B26;
  --color-error-emphasis: #D0575B4c;
  --color-error-strong: #D0575B80;
  --color-error-light: #DE7A7E;
  --color-error-lighter: #EA9DA0;
  --color-error-lightest: #F5C0C2;

  --color-info: #5A8FAA;
  --color-info-light: #74A5BD;
  --color-info-lighter: #8EBBD0;
  --color-info-lightest: #A8D1E3;
  --color-info-subtle: #5A8FAA1a;
  --color-info-light-muted: #5A8FAA33;
  --color-info-light-emphasis: #5A8FAA4c;

  // Button-specific colors
  --color-button-primary: #C4505A;
  --color-button-primary-light: #D47279;
  --color-button-primary-subtle: #C4505A19;
  --color-button-primary-emphasis: #C4505A4c;
  --color-button-primary-text: #120E0F;

  --color-button-secondary: #5A8FAA;
  --color-button-secondary-light: #74A5BD;
  --color-button-secondary-subtle: #5A8FAA1a;
  --color-button-secondary-emphasis: #5A8FAA4c;
  --color-button-secondary-text: #F2E9EA;

  --color-button-danger: #D0575B;
  --color-button-danger-light: #DE7A7E;
  --color-button-danger-subtle: #D0575B1a;
  --color-button-danger-emphasis: #D0575B4c;
  --color-button-danger-text: #F2E9EA;

  --color-input-bg: #1A1516;
  --color-input-border: #463A3D;
  --color-input-text: #F2E9EA;

  --color-agent-auto-approve: #5DB86C;
  --color-agent-aider-tools: #C4505A;
  --color-agent-power-tools: #B08DC9;
  --color-agent-todo-tools: #5A8FAA;
  --color-agent-tasks-tools: #5DB86C;
  --color-agent-memory-tools: #C97BA0;
  --color-agent-skills-tools: #8FB85D;
  --color-agent-subagents-tools: #5AAA85;
  --color-agent-context-files: #D4C054;
  --color-agent-repo-map: #74A5BD;
  --color-agent-ai-request: #AA6B5A;
  --color-agent-sub-agent: #6B8FAA;
}

================
File: src/renderer/src/themes/theme-dark.scss
================
.theme-dark {

  --color-bg-primary: #141417;
  --color-bg-primary-light: #191a22;
  --color-bg-primary-light-strong: #191a2280;
  --color-bg-secondary: #222431;
  --color-bg-secondary-light: #2a2c3f;
  --color-bg-secondary-light-strongest: #2a2c3ff2;
  --color-bg-tertiary: #333652;
  --color-bg-tertiary-emphasis: #3336524C;
  --color-bg-tertiary-strong: #3336527F;
  --color-bg-fourth: #3d4166;
  --color-bg-fourth-muted: #3d416633;
  --color-bg-fourth-emphasis: #3d41664c;
  --color-bg-fifth: #585c75;
  --color-bg-selection: #5d5f6e;
  --color-bg-code-block: #171718;

  --color-bg-diff-viewer-old-primary: #3f1d25;
  --color-bg-diff-viewer-old-secondary: #2c141a;
  --color-bg-diff-viewer-new-primary: #022c22;
  --color-bg-diff-viewer-new-secondary: #011f19;

  --color-text-primary: #f1f3f5;
  --color-text-secondary: #999ba3;
  --color-text-tertiary: #8c8e95;
  --color-text-muted-light: #7b7d86;
  --color-text-muted: #585c75;
  --color-text-muted-dark: #3d4166;
  --color-text-dark: #333652;

  --color-border-dark: #222431;
  --color-border-dark-light: #2a2c3f;
  --color-border-dark-light-strong: #2a2c3f80;
  --color-border-default-dark: #333652;
  --color-border-default: #3d4166;
  --color-border-accent: #585c75;
  --color-border-light: #999ba3;

  --color-accent-primary: #585c75;
  --color-accent-secondary: #7b7d86;
  --color-accent-light: #999ba3;

  --color-success: #22c55e;
  --color-success-light: #4ade80;
  --color-success-subtle: #22c55e1a;
  --color-success-muted: #22c55e33;
  --color-success-emphasis: #22c55e4c;

  --color-warning: #d97706;
  --color-warning-subtle: #d9770619;
  --color-warning-emphasis: #d977064c;
  --color-warning-light: #f59e0b;
  --color-warning-text: #1a1a1a;

  --color-error: #ef4444;
  --color-error-dark: #991b1b;
  --color-error-subtle: #ef44441a;
  --color-error-muted: #ef444426;
  --color-error-emphasis: #ef44444c;
  --color-error-strong: #ef444480;
  --color-error-light: #f87171;
  --color-error-lighter: #fca5a5;
  --color-error-lightest: #fecaca;

  --color-info: #2563eb;
  --color-info-light: #3b82f6;
  --color-info-lighter: #60a5fa;
  --color-info-lightest: #93c5fd;
  --color-info-subtle: #2563eb1a;
  --color-info-light-muted: #2563eb33;
  --color-info-light-emphasis: #2563eb4c;

  // Button-specific colors
  --color-button-primary: #d97706;
  --color-button-primary-light: #f59e0b;
  --color-button-primary-subtle: #d9770619;
  --color-button-primary-emphasis: #d977064c;
  --color-button-primary-text: #1a1a1a;

  --color-button-secondary: #2563eb;
  --color-button-secondary-light: #3b82f6;
  --color-button-secondary-subtle: #2563eb1a;
  --color-button-secondary-emphasis: #2563eb4c;
  --color-button-secondary-text: #f1f3f5;

  --color-button-danger: #ef4444;
  --color-button-danger-light: #f87171;
  --color-button-danger-subtle: #ef44441a;
  --color-button-danger-emphasis: #ef44444c;
  --color-button-danger-text: #f1f3f5;

  --color-input-bg: #191a22;
  --color-input-border: #3d4166;
  --color-input-text: #f1f3f5;

  --color-agent-auto-approve: #4ade80;
  --color-agent-aider-tools: #fb923c;
  --color-agent-power-tools: #c084fc;
  --color-agent-todo-tools: #38bdf8;
  --color-agent-tasks-tools: #10b981;
  --color-agent-memory-tools: #f472b6;
  --color-agent-skills-tools: #c7d392;
  --color-agent-subagents-tools: #55aa7f;
  --color-agent-context-files: #facc15;
  --color-agent-repo-map: #60a5fa;
  --color-agent-ai-request: #884239;
  --color-agent-sub-agent: #3368a8;
}

================
File: src/renderer/src/themes/theme-forest.scss
================
.theme-forest {
  
  --color-bg-primary: #0f2419; 
  --color-bg-primary-light: #1a4d2e; 
  --color-bg-primary-light-strong: #1a4d2e80;
  --color-bg-secondary: #2d5a3d; 
  --color-bg-secondary-light: #3d6b4d; 
  --color-bg-secondary-light-strongest: #3d6b4df2;
  --color-bg-tertiary: #4a7c59; 
  --color-bg-tertiary-emphasis: #4a7c594c;
  --color-bg-tertiary-strong: #4a7c597f;
  --color-bg-fourth: #5d8a6a; 
  --color-bg-fourth-muted: #5d8a6a33;
  --color-bg-fourth-emphasis: #5d8a6a4c;
  --color-bg-fifth: #7fb069; 
  --color-bg-selection: #4a7c59;
  --color-bg-code-block: #0a1f14; 

  --color-bg-diff-viewer-old-primary: #4d2a1a; 
  --color-bg-diff-viewer-old-secondary: #3d1f0f; 
  --color-bg-diff-viewer-new-primary: #1a4d2a; 
  --color-bg-diff-viewer-new-secondary: #0f3d1f; 

  --color-text-primary: #f0f7f0; 
  --color-text-secondary: #d4e6d4; 
  --color-text-tertiary: #b8d4b8; 
  --color-text-muted-light: #9cc29c; 
  --color-text-muted: #7fb069; 
  --color-text-muted-dark: #5d8a6a; 
  --color-text-dark: #4a7c59; 

  --color-border-dark: #2d5a3d;
  --color-border-dark-light: #3d6b4d;
  --color-border-dark-light-strong: #3d6b4d80;
  --color-border-default-dark: #4a7c59;
  --color-border-default: #5d8a6a;
  --color-border-accent: #7fb069;
  --color-border-light: #9cc29c;

  --color-accent-primary: #7fb069; 
  --color-accent-secondary: #9cc29c; 
  --color-accent-light: #b8d4b8; 

  --color-success: #4a7c59; 
  --color-success-light: #5d8a6a;
  --color-success-subtle: #4a7c591a;
  --color-success-muted: #4a7c5933;
  --color-success-emphasis: #4a7c594c;

  --color-warning: #8b9a46; 
  --color-warning-subtle: #8b9a4619;
  --color-warning-emphasis: #8b9a464c;
  --color-warning-light: #a6b85c;
  --color-warning-text: #f0f7f0;

  --color-error: #c85a5a; 
  --color-error-dark: #a04040;
  --color-error-subtle: #c85a5a1a;
  --color-error-muted: #c85a5a26;
  --color-error-emphasis: #c85a5a4c;
  --color-error-strong: #c85a5a80;
  --color-error-light: #d47474;
  --color-error-lighter: #e09999;
  --color-error-lightest: #ecbfbf;

  --color-info: #4a7a9a;
  --color-info-light: #5d8fb0;
  --color-info-lighter: #7aa3c2;
  --color-info-lightest: #97b8d4;
  --color-info-subtle: #4a7a9a1a;
  --color-info-light-muted: #4a7a9a33;
  --color-info-light-emphasis: #4a7a9a4c;

  // Button-specific colors
  --color-button-primary: #7fb069;
  --color-button-primary-light: #8fc279;
  --color-button-primary-subtle: #7fb06919;
  --color-button-primary-emphasis: #7fb0694c;
  --color-button-primary-text: #0f2419;

  --color-button-secondary: #4a7a9a;
  --color-button-secondary-light: #5d8fb0;
  --color-button-secondary-subtle: #4a7a9a1a;
  --color-button-secondary-emphasis: #4a7a9a4c;
  --color-button-secondary-text: #f0f7f0;

  --color-button-danger: #c85a5a;
  --color-button-danger-light: #d47474;
  --color-button-danger-subtle: #c85a5a1a;
  --color-button-danger-emphasis: #c85a5a4c;
  --color-button-danger-text: #f0f7f0;

  --color-input-bg: #1a4d2e;
  --color-input-border: #5d8a6a;
  --color-input-text: #f0f7f0;

  --color-agent-auto-approve: #5d8a6a;
  --color-agent-aider-tools: #b8854a;
  --color-agent-power-tools: #8a6a9a;
  --color-agent-todo-tools: #4a7a9a;
  --color-agent-tasks-tools: #6a9a7a;
  --color-agent-memory-tools: #9a7a8a;
  --color-agent-skills-tools: #8b5a6a;
  --color-agent-subagents-tools: #55aa7f;
  --color-agent-context-files: #9a8a4a;
  --color-agent-repo-map: #6a8a9a;
  --color-agent-ai-request: #9a4a4a;
  --color-agent-sub-agent: #4a6a9a;
}

================
File: src/renderer/src/themes/theme-fresh.scss
================
.theme-fresh {
  // Background colors
  --color-bg-primary: #f8fafc;
  --color-bg-primary-light: #f1f5f9;
  --color-bg-primary-light-strong: #f1f5f980;
  --color-bg-secondary: #e2e8f0;
  --color-bg-secondary-light: #cbd5e1;
  --color-bg-secondary-light-strongest: #cbd5e1f2;
  --color-bg-tertiary: #94a3b8;
  --color-bg-tertiary-emphasis: #94a3b84c;
  --color-bg-tertiary-strong: #94a3b87f;
  --color-bg-fourth: #64748b;
  --color-bg-fourth-muted: #64748b33;
  --color-bg-fourth-emphasis: #64748b4c;
  --color-bg-fifth: #475569;
  --color-bg-selection: #94a3b8;
  --color-bg-code-block: #f8fafc;

  // Diff viewer colors
  --color-bg-diff-viewer-old-primary: #fee2e2;
  --color-bg-diff-viewer-old-secondary: #fecaca;
  --color-bg-diff-viewer-new-primary: #dcfce7;
  --color-bg-diff-viewer-new-secondary: #bbf7d0;

  // Text colors
  --color-text-primary: #0f172a;
  --color-text-secondary: #334155;
  --color-text-tertiary: #475569;
  --color-text-muted-light: #64748b;
  --color-text-muted: #94a3b8;
  --color-text-muted-dark: #cbd5e1;
  --color-text-dark: #e2e8f0;

  // Border colors
  --color-border-dark: #e2e8f0;
  --color-border-dark-light: #cbd5e1;
  --color-border-dark-light-strong: #cbd5e180;
  --color-border-default-dark: #94a3b8;
  --color-border-default: #64748b;
  --color-border-accent: #10b981;
  --color-border-light: #0f172a;

  // Accent colors
  --color-accent-primary: #10b981;
  --color-accent-secondary: #06b6d4;
  --color-accent-light: #0f172a;

  // Status colors
  --color-success: #10b981;
  --color-success-light: #34d399;
  --color-success-subtle: #10b9811a;
  --color-success-muted: #10b98133;
  --color-success-emphasis: #10b9814c;

  --color-warning: #f59e0b;
  --color-warning-subtle: #f59e0b19;
  --color-warning-emphasis: #f59e0b4c;
  --color-warning-light: #fbbf24;
  --color-warning-text: #0f172a;

  --color-error: #ef4444;
  --color-error-dark: #dc2626;
  --color-error-subtle: #ef44441a;
  --color-error-muted: #ef444426;
  --color-error-emphasis: #ef44444c;
  --color-error-strong: #ef444480;
  --color-error-light: #f87171;
  --color-error-lighter: #fca5a5;
  --color-error-lightest: #fecaca;

  --color-info: #3b82f6;
  --color-info-light: #60a5fa;
  --color-info-lighter: #93c5fd;
  --color-info-lightest: #dbeafe;
  --color-info-subtle: #3b82f61a;
  --color-info-light-muted: #3b82f633;
  --color-info-light-emphasis: #3b82f64c;

  // Button colors
  --color-button-primary: #10b981;
  --color-button-primary-light: #34d399;
  --color-button-primary-subtle: #10b98119;
  --color-button-primary-emphasis: #10b9814c;
  --color-button-primary-text: #ffffff;

  --color-button-secondary: #3b82f6;
  --color-button-secondary-light: #60a5fa;
  --color-button-secondary-subtle: #3b82f61a;
  --color-button-secondary-emphasis: #3b82f64c;
  --color-button-secondary-text: #ffffff;

  --color-button-danger: #ef4444;
  --color-button-danger-light: #f87171;
  --color-button-danger-subtle: #ef44441a;
  --color-button-danger-emphasis: #ef44444c;
  --color-button-danger-text: #ffffff;

  // Input colors
  --color-input-bg: #f8fafc;
  --color-input-border: #cbd5e1;
  --color-input-text: #0f172a;

  // Agent tool colors
  --color-agent-auto-approve: #10b981;
  --color-agent-aider-tools: #f59e0b;
  --color-agent-power-tools: #8b5cf6;
  --color-agent-todo-tools: #3b82f6;
  --color-agent-tasks-tools: #14b8a6;
  --color-agent-memory-tools: #f472b6;
  --color-agent-skills-tools: #a78bfa;
  --color-agent-subagents-tools: #55aa7f;
  --color-agent-context-files: #06b6d4;
  --color-agent-repo-map: #10b981;
  --color-agent-ai-request: #ef4444;
  --color-agent-sub-agent: #6366f1;
}

================
File: src/renderer/src/themes/theme-lavender.scss
================
.theme-lavender {
  
  --color-bg-primary: #2d1b4e; 
  --color-bg-primary-light: #3d2a5f; 
  --color-bg-primary-light-strong: #3d2a5f80;
  --color-bg-secondary: #4c3970; 
  --color-bg-secondary-light: #5b4881; 
  --color-bg-secondary-light-strongest: #5b4881f2;
  --color-bg-tertiary: #6a5792; 
  --color-bg-tertiary-emphasis: #6a57924c;
  --color-bg-tertiary-strong: #6a57927f;
  --color-bg-fourth: #7966a3; 
  --color-bg-fourth-muted: #7966a333;
  --color-bg-fourth-emphasis: #7966a34c;
  --color-bg-fifth: #8875b4; 
  --color-bg-selection: #6a5792;
  --color-bg-code-block: #1f0d3a; 

  --color-bg-diff-viewer-old-primary: #5d2a3d; 
  --color-bg-diff-viewer-old-secondary: #4a1f2e; 
  --color-bg-diff-viewer-new-primary: #2a4d3d; 
  --color-bg-diff-viewer-new-secondary: #1f3a2e; 

  --color-text-primary: #faf7ff; 
  --color-text-secondary: #e6d9ff; 
  --color-text-tertiary: #d1bbff; 
  --color-text-muted-light: #bc9dff; 
  --color-text-muted: #8875b4; 
  --color-text-muted-dark: #6a5792; 
  --color-text-dark: #4c3970; 

  --color-border-dark: #4c3970;
  --color-border-dark-light: #5b4881;
  --color-border-dark-light-strong: #5b488180;
  --color-border-default-dark: #6a5792;
  --color-border-default: #7966a3;
  --color-border-accent: #8875b4;
  --color-border-light: #bc9dff;

  --color-accent-primary: #8875b4; 
  --color-accent-secondary: #bc9dff; 
  --color-accent-light: #d1bbff; 

  --color-success: #6b8e5a; 
  --color-success-light: #7fa068;
  --color-success-subtle: #6b8e5a1a;
  --color-success-muted: #6b8e5a33;
  --color-success-emphasis: #6b8e5a4c;

  --color-warning: #b8864a; 
  --color-warning-subtle: #b8864a19;
  --color-warning-emphasis: #b8864a4c;
  --color-warning-light: #c9975c;
  --color-warning-text: #faf7ff;

  --color-error: #c85a7a; 
  --color-error-dark: #a04060;
  --color-error-subtle: #c85a7a1a;
  --color-error-muted: #c85a7a26;
  --color-error-emphasis: #c85a7a4c;
  --color-error-strong: #c85a7a80;
  --color-error-light: #d47490;
  --color-error-lighter: #e099b0;
  --color-error-lightest: #ecbfcf;

  --color-info: #5a7ac8;
  --color-info-light: #6c8cd4;
  --color-info-lighter: #8fa5e0;
  --color-info-lightest: #b2bdec;
  --color-info-subtle: #5a7ac81a;
  --color-info-light-muted: #5a7ac833;
  --color-info-light-emphasis: #5a7ac84c;

  // Button-specific colors
  --color-button-primary: #8875b4;
  --color-button-primary-light: #9a87c4;
  --color-button-primary-subtle: #8875b419;
  --color-button-primary-emphasis: #8875b44c;
  --color-button-primary-text: #faf7ff;

  --color-button-secondary: #5a7ac8;
  --color-button-secondary-light: #6c8cd4;
  --color-button-secondary-subtle: #5a7ac81a;
  --color-button-secondary-emphasis: #5a7ac84c;
  --color-button-secondary-text: #faf7ff;

  --color-button-danger: #c85a7a;
  --color-button-danger-light: #d47490;
  --color-button-danger-subtle: #c85a7a1a;
  --color-button-danger-emphasis: #c85a7a4c;
  --color-button-danger-text: #faf7ff;

  --color-input-bg: #3d2a5f;
  --color-input-border: #7966a3;
  --color-input-text: #faf7ff;

  --color-agent-auto-approve: #7fa068;
  --color-agent-aider-tools: #b8864a;
  --color-agent-power-tools: #8875b4;
  --color-agent-todo-tools: #5a7ac8;
  --color-agent-tasks-tools: #7a9a6a;
  --color-agent-memory-tools: #b48a9a;
  --color-agent-skills-tools: #c084fc;
  --color-agent-subagents-tools: #55aa7f;
  --color-agent-context-files: #a68b4a;
  --color-agent-repo-map: #6c8cd4;
  --color-agent-ai-request: #c85a7a;
  --color-agent-sub-agent: #5a7ac8;
}

================
File: src/renderer/src/themes/theme-light.scss
================
.theme-light {

  --color-bg-primary: #ffffff;
  --color-bg-primary-light: #f8f9fa;
  --color-bg-primary-light-strong: #f8f9fa80;
  --color-bg-secondary: #e9ecef;
  --color-bg-secondary-light: #dee2e6;
  --color-bg-secondary-light-strongest: #dee2e6f2;
  --color-bg-tertiary: #ced4da;
  --color-bg-tertiary-emphasis: #ced4da4c;
  --color-bg-tertiary-strong: #ced4da7f;
  --color-bg-fourth: #adb5bd;
  --color-bg-fourth-muted: #adb5bd33;
  --color-bg-fourth-emphasis: #adb5bd4c;
  --color-bg-fifth: #6c757d;
  --color-bg-selection: #ced4da;
  --color-bg-code-block: #f8f9fa;

  --color-bg-diff-viewer-old-primary: #fee2e2;
  --color-bg-diff-viewer-old-secondary: #fecaca;
  --color-bg-diff-viewer-new-primary: #dcfce7;
  --color-bg-diff-viewer-new-secondary: #bbf7d0;

  --color-text-primary: #212529;
  --color-text-secondary: #495057;
  --color-text-tertiary: #6c757d;
  --color-text-muted-light: #adb5bd;
  --color-text-muted: #6c757d;
  --color-text-muted-dark: #495057;
  --color-text-dark: #343a40;

  --color-border-dark: #e9ecef;
  --color-border-dark-light: #dee2e6;
  --color-border-dark-light-strong: #dee2e680;
  --color-border-default-dark: #ced4da;
  --color-border-default: #adb5bd;
  --color-border-accent: #6c757d;
  --color-border-light: #495057;

  --color-accent-primary: #6c757d;
  --color-accent-secondary: #adb5bd;
  --color-accent-light: #495057;

  --color-success: #22c55e;
  --color-success-light: #4ade80;
  --color-success-subtle: #22c55e1a;
  --color-success-muted: #22c55e33;
  --color-success-emphasis: #22c55e4c;

  --color-warning: #0ea5e9;
  --color-warning-subtle: #0ea5e919;
  --color-warning-emphasis: #0ea5e94c;
  --color-warning-light: #38bdf8;
  --color-warning-text: #ffffff;

  --color-error: #ef4444;
  --color-error-dark: #991b1b;
  --color-error-subtle: #ef44441a;
  --color-error-muted: #ef444426;
  --color-error-emphasis: #ef44444c;
  --color-error-strong: #ef444480;
  --color-error-light: #f87171;
  --color-error-lighter: #fca5a5;
  --color-error-lightest: #fecaca;

  --color-info: #2563eb;
  --color-info-light: #3b82f6;
  --color-info-lighter: #60a5fa;
  --color-info-lightest: #93c5fd;
  --color-info-subtle: #2563eb1a;
  --color-info-light-muted: #2563eb33;
  --color-info-light-emphasis: #2563eb4c;

  // Button-specific colors
  --color-button-primary: #0ea5e9;
  --color-button-primary-light: #38bdf8;
  --color-button-primary-subtle: #0ea5e919;
  --color-button-primary-emphasis: #0ea5e94c;
  --color-button-primary-text: #ffffff;

  --color-button-secondary: #2563eb;
  --color-button-secondary-light: #3b82f6;
  --color-button-secondary-subtle: #2563eb1a;
  --color-button-secondary-emphasis: #2563eb4c;
  --color-button-secondary-text: #ffffff;

  --color-button-danger: #ef4444;
  --color-button-danger-light: #f87171;
  --color-button-danger-subtle: #ef44441a;
  --color-button-danger-emphasis: #ef44444c;
  --color-button-danger-text: #ffffff;

  --color-input-bg: #f8f9fa;
  --color-input-border: #adb5bd;
  --color-input-text: #212529;

  --color-agent-auto-approve: #4ade80;
  --color-agent-aider-tools: #fb923c;
  --color-agent-power-tools: #c084fc;
  --color-agent-todo-tools: #38bdf8;
  --color-agent-tasks-tools: #059669;
  --color-agent-memory-tools: #ec4899;
  --color-agent-skills-tools: #9d42f3;
  --color-agent-subagents-tools: #55aa7f;
  --color-agent-context-files: #facc15;
  --color-agent-repo-map: #60a5fa;
  --color-agent-ai-request: #dc2626;
  --color-agent-sub-agent: #2563eb;
}

================
File: src/renderer/src/themes/theme-midnight.scss
================
.theme-midnight {

  --color-bg-primary: #000000;
  --color-bg-primary-light: #080808;
  --color-bg-primary-light-strong: #08080880;
  --color-bg-secondary: #101010;
  --color-bg-secondary-light: #181818;
  --color-bg-secondary-light-strongest: #181818f2;
  --color-bg-tertiary: #202020;
  --color-bg-tertiary-emphasis: #2020204c;
  --color-bg-tertiary-strong: #2020207f;
  --color-bg-fourth: #282828;
  --color-bg-fourth-muted: #28282833;
  --color-bg-fourth-emphasis: #2828284c;
  --color-bg-fifth: #303030;
  --color-bg-selection: #303030;
  --color-bg-code-block: #000000;

  --color-bg-diff-viewer-old-primary: #200000;
  --color-bg-diff-viewer-old-secondary: #100000;
  --color-bg-diff-viewer-new-primary: #002000;
  --color-bg-diff-viewer-new-secondary: #001000;

  --color-text-primary: #ffffff;
  --color-text-secondary: #e0e0e0;
  --color-text-tertiary: #c0c0c0;
  --color-text-muted-light: #a0a0a0;
  --color-text-muted: #808080;
  --color-text-muted-dark: #606060;
  --color-text-dark: #404040;

  --color-border-dark: #101010;
  --color-border-dark-light: #181818;
  --color-border-dark-light-strong: #18181880;
  --color-border-default-dark: #202020;
  --color-border-default: #282828;
  --color-border-accent: #303030;
  --color-border-light: #404040;

  --color-accent-primary: #303030;
  --color-accent-secondary: #282828;
  --color-accent-light: #404040;

  --color-success: #00ff00;
  --color-success-light: #40ff40;
  --color-success-subtle: #00ff001a;
  --color-success-muted: #00ff0033;
  --color-success-emphasis: #00ff004c;

  --color-warning: #ffff00;
  --color-warning-subtle: #ffff0019;
  --color-warning-emphasis: #ffff004c;
  --color-warning-light: #ffff40;
  --color-warning-text: #707020;

  --color-error: #ff0000;
  --color-error-dark: #800000;
  --color-error-subtle: #ff00001a;
  --color-error-muted: #ff000026;
  --color-error-emphasis: #ff00004c;
  --color-error-strong: #ff000080;
  --color-error-light: #ff4040;
  --color-error-lighter: #ff8080;
  --color-error-lightest: #ffc0c0;

  --color-info: #0000ff;
  --color-info-light: #4040ff;
  --color-info-lighter: #8080ff;
  --color-info-lightest: #c0c0ff;
  --color-info-subtle: #0000ff1a;
  --color-info-light-muted: #0000ff33;
  --color-info-light-emphasis: #0000ff4c;

  // Button-specific colors
  --color-button-primary: #ffff00;
  --color-button-primary-light: #ffff40;
  --color-button-primary-subtle: #ffff0019;
  --color-button-primary-emphasis: #ffff004c;
  --color-button-primary-text: #000000;

  --color-button-secondary: #0000ff;
  --color-button-secondary-light: #4040ff;
  --color-button-secondary-subtle: #0000ff1a;
  --color-button-secondary-emphasis: #0000ff4c;
  --color-button-secondary-text: #ffffff;

  --color-button-danger: #ff0000;
  --color-button-danger-light: #ff4040;
  --color-button-danger-subtle: #ff00001a;
  --color-button-danger-emphasis: #ff00004c;
  --color-button-danger-text: #ffffff;

  --color-input-bg: #080808;
  --color-input-border: #282828;
  --color-input-text: #ffffff;

  --color-agent-auto-approve: #00ff00;
  --color-agent-aider-tools: #ff8000;
  --color-agent-power-tools: #ff00ff;
  --color-agent-todo-tools: #00ffff;
  --color-agent-tasks-tools: #00ff88;
  --color-agent-memory-tools: #ff00aa;
  --color-agent-skills-tools: #ae7fa3;
  --color-agent-subagents-tools: #55aa7f;
  --color-agent-context-files: #ffff00;
  --color-agent-repo-map: #0080ff;
  --color-agent-ai-request: #ff0000;
  --color-agent-sub-agent: #0000ff;
}

================
File: src/renderer/src/themes/theme-neon.scss
================
.theme-neon {

  --color-bg-primary: #0d1117;
  --color-bg-primary-light: #161b22;
  --color-bg-primary-light-strong: #161b2280;
  --color-bg-secondary: #21262d;
  --color-bg-secondary-light: #30363d;
  --color-bg-secondary-light-strongest: #30363df2;
  --color-bg-tertiary: #484f58;
  --color-bg-tertiary-emphasis: #ff006680;
  --color-bg-tertiary-strong: #ff006640;
  --color-bg-fourth: #6e7681;
  --color-bg-fourth-muted: #00ffff33;
  --color-bg-fourth-emphasis: #00ffff4c;
  --color-bg-fifth: #8b949e;
  --color-bg-selection: #ff006640;
  --color-bg-code-block: #0a0e14;

  --color-bg-diff-viewer-old-primary: #67060c;
  --color-bg-diff-viewer-old-secondary: #490202;
  --color-bg-diff-viewer-new-primary: #033a16;
  --color-bg-diff-viewer-new-secondary: #0f2419;

  --color-text-primary: #ffffff;
  --color-text-secondary: #00ffff;
  --color-text-tertiary: #ff6600;
  --color-text-muted-light: #ff0099;
  --color-text-muted: #484f58;
  --color-text-muted-dark: #30363d;
  --color-text-dark: #21262d;

  --color-border-dark: #21262d;
  --color-border-dark-light: #30363d;
  --color-border-dark-light-strong: #30363d80;
  --color-border-default-dark: #484f58;
  --color-border-default: #00ffff;
  --color-border-accent: #ff0066;
  --color-border-light: #ffff00;

  --color-accent-primary: #00ffff;
  --color-accent-secondary: #ff0066;
  --color-accent-light: #ffff00;

  --color-success: #00ff88;
  --color-success-light: #44ff99;
  --color-success-subtle: #00ff881a;
  --color-success-muted: #00ff8833;
  --color-success-emphasis: #00ff884c;

  --color-warning: #ffaa00;
  --color-warning-subtle: #ffaa0019;
  --color-warning-emphasis: #ffaa004c;
  --color-warning-light: #ffcc33;
  --color-warning-text: #0d1117;

  --color-error: #ff3366;
  --color-error-dark: #cc0044;
  --color-error-subtle: #ff33661a;
  --color-error-muted: #ff336626;
  --color-error-emphasis: #ff33664c;
  --color-error-strong: #ff336680;
  --color-error-light: #ff6699;
  --color-error-lighter: #ff99bb;
  --color-error-lightest: #ffccdd;

  --color-info: #3366ff;
  --color-info-light: #6699ff;
  --color-info-lighter: #99ccff;
  --color-info-lightest: #cce6ff;
  --color-info-subtle: #3366ff1a;
  --color-info-light-muted: #3366ff33;
  --color-info-light-emphasis: #3366ff4c;

  // Button-specific colors
  --color-button-primary: #00ffff;
  --color-button-primary-light: #33ffff;
  --color-button-primary-subtle: #00ffff19;
  --color-button-primary-emphasis: #00ffff4c;
  --color-button-primary-text: #0d1117;

  --color-button-secondary: #3366ff;
  --color-button-secondary-light: #6699ff;
  --color-button-secondary-subtle: #3366ff1a;
  --color-button-secondary-emphasis: #3366ff4c;
  --color-button-secondary-text: #ffffff;

  --color-button-danger: #ff3366;
  --color-button-danger-light: #ff6699;
  --color-button-danger-subtle: #ff33661a;
  --color-button-danger-emphasis: #ff33664c;
  --color-button-danger-text: #ffffff;

  --color-input-bg: #161b22;
  --color-input-border: #00ffff;
  --color-input-text: #ffffff;

  --color-agent-auto-approve: #00ff88;
  --color-agent-aider-tools: #ff9900;
  --color-agent-power-tools: #cc66ff;
  --color-agent-todo-tools: #3366ff;
  --color-agent-tasks-tools: #00ffcc;
  --color-agent-memory-tools: #ff66cc;
  --color-agent-skills-tools: #8ac3e1;
  --color-agent-subagents-tools: #55aa7f;
  --color-agent-context-files: #ffcc00;
  --color-agent-repo-map: #00ccff;
  --color-agent-ai-request: #ff0066;
  --color-agent-sub-agent: #0099ff;
}

================
File: src/renderer/src/themes/theme-neopunk.scss
================
.theme-neopunk {

  --color-bg-primary: #0a0a0f;
  --color-bg-primary-light: #14141e;
  --color-bg-primary-light-strong: #14141e80;
  --color-bg-secondary: #1a1a2e;
  --color-bg-secondary-light: #16213e;
  --color-bg-secondary-light-strongest: #16213ef2;
  --color-bg-tertiary: #0f3460;
  --color-bg-tertiary-emphasis: #ff00ff4c;
  --color-bg-tertiary-strong: #ff00ff7f;
  --color-bg-fourth: #16213e;
  --color-bg-fourth-muted: #00ffff33;
  --color-bg-fourth-emphasis: #00ffff4c;
  --color-bg-fifth: #e94560;
  --color-bg-selection: #ff00ff40;
  --color-bg-code-block: #050508;

  --color-bg-diff-viewer-old-primary: #4a0e0e;
  --color-bg-diff-viewer-old-secondary: #2a0808;
  --color-bg-diff-viewer-new-primary: #0a4a2a;
  --color-bg-diff-viewer-new-secondary: #062a18;

  --color-text-primary: #ffffff;
  --color-text-secondary: #00ffff;
  --color-text-tertiary: #ff00ff;
  --color-text-muted-light: #00ff88;
  --color-text-muted: #0f3460;
  --color-text-muted-dark: #16213e;
  --color-text-dark: #1a1a2e;

  --color-border-dark: #1a1a2e;
  --color-border-dark-light: #16213e;
  --color-border-dark-light-strong: #16213e80;
  --color-border-default-dark: #0f3460;
  --color-border-default: #00ffff;
  --color-border-accent: #ff00ff;
  --color-border-light: #00ff88;

  --color-accent-primary: #00ffff;
  --color-accent-secondary: #ff00ff;
  --color-accent-light: #00ff88;

  --color-success: #00ff88;
  --color-success-light: #44ff99;
  --color-success-subtle: #00ff881a;
  --color-success-muted: #00ff8833;
  --color-success-emphasis: #00ff884c;

  --color-warning: #ffaa00;
  --color-warning-subtle: #ffaa0019;
  --color-warning-emphasis: #ffaa004c;
  --color-warning-light: #ffcc33;
  --color-warning-text: #0a0a0f;

  --color-error: #ff3366;
  --color-error-dark: #cc0044;
  --color-error-subtle: #ff33661a;
  --color-error-muted: #ff336626;
  --color-error-emphasis: #ff33664c;
  --color-error-strong: #ff336680;
  --color-error-light: #ff6699;
  --color-error-lighter: #ff99bb;
  --color-error-lightest: #ffccdd;

  --color-info: #3366ff;
  --color-info-light: #6699ff;
  --color-info-lighter: #99ccff;
  --color-info-lightest: #cce6ff;
  --color-info-subtle: #3366ff1a;
  --color-info-light-muted: #3366ff33;
  --color-info-light-emphasis: #3366ff4c;

  // Button-specific colors
  --color-button-primary: #ffaa00;
  --color-button-primary-light: #ffcc33;
  --color-button-primary-subtle: #ffaa0019;
  --color-button-primary-emphasis: #ffaa004c;
  --color-button-primary-text: #0a0a0f;

  --color-button-secondary: #3366ff;
  --color-button-secondary-light: #6699ff;
  --color-button-secondary-subtle: #3366ff1a;
  --color-button-secondary-emphasis: #3366ff4c;
  --color-button-secondary-text: #ffffff;

  --color-button-danger: #ff3366;
  --color-button-danger-light: #ff6699;
  --color-button-danger-subtle: #ff33661a;
  --color-button-danger-emphasis: #ff33664c;
  --color-button-danger-text: #ffffff;

  --color-input-bg: #14141e;
  --color-input-border: #00ffff;
  --color-input-text: #ffffff;

  --color-agent-auto-approve: #00ff88;
  --color-agent-aider-tools: #ff6600;
  --color-agent-power-tools: #ff00ff;
  --color-agent-todo-tools: #00ccff;
  --color-agent-tasks-tools: #00ffaa;
  --color-agent-memory-tools: #ff00aa;
  --color-agent-skills-tools: #2bbc20;
  --color-agent-subagents-tools: #55aa7f;
  --color-agent-context-files: #ffff00;
  --color-agent-repo-map: #ff0099;
  --color-agent-ai-request: #ff3366;
  --color-agent-sub-agent: #3366ff;
}

================
File: src/renderer/src/themes/theme-obsidian.scss
================
.theme-obsidian {

  --color-bg-primary: #0F0E0C;
  --color-bg-primary-light: #171614;
  --color-bg-primary-light-strong: #17161480;
  --color-bg-secondary: #1F1D1A;
  --color-bg-secondary-light: #282522;
  --color-bg-secondary-light-strongest: #282522F2;
  --color-bg-tertiary: #33302C;
  --color-bg-tertiary-emphasis: #D4A05440;
  --color-bg-tertiary-strong: #D4A05460;
  --color-bg-fourth: #3D3A36;
  --color-bg-fourth-muted: #D4A05433;
  --color-bg-fourth-emphasis: #D4A0544C;
  --color-bg-fifth: #4D4A46;
  --color-bg-selection: #D4A05450;
  --color-bg-code-block: #0B0A09;

  --color-bg-diff-viewer-old-primary: #3D1F1F;
  --color-bg-diff-viewer-old-secondary: #2B1515;
  --color-bg-diff-viewer-new-primary: #1F3D25;
  --color-bg-diff-viewer-new-secondary: #152B1A;

  --color-text-primary: #F0EBE3;
  --color-text-secondary: #A89F95;
  --color-text-tertiary: #8A8078;
  --color-text-muted-light: #6E655D;
  --color-text-muted: #524A43;
  --color-text-muted-dark: #3D3A36;
  --color-text-dark: #282522;

  --color-border-dark: #1F1D1A;
  --color-border-dark-light: #282522;
  --color-border-dark-light-strong: #28252280;
  --color-border-default-dark: #33302C;
  --color-border-default: #3D3A36;
  --color-border-accent: #D4A054;
  --color-border-light: #6E655D;

  --color-accent-primary: #D4A054;
  --color-accent-secondary: #A89F95;
  --color-accent-light: #F0EBE3;

  --color-success: #5DB86C;
  --color-success-light: #7ECF8A;
  --color-success-subtle: #5DB86C1a;
  --color-success-muted: #5DB86C33;
  --color-success-emphasis: #5DB86C4c;

  --color-warning: #D4A054;
  --color-warning-subtle: #D4A05419;
  --color-warning-emphasis: #D4A0544c;
  --color-warning-light: #E0B76E;
  --color-warning-text: #0F0E0C;

  --color-error: #CC5F5F;
  --color-error-dark: #9A3D3D;
  --color-error-subtle: #CC5F5F1a;
  --color-error-muted: #CC5F5F26;
  --color-error-emphasis: #CC5F5F4c;
  --color-error-strong: #CC5F5F80;
  --color-error-light: #D98080;
  --color-error-lighter: #E6A0A0;
  --color-error-lightest: #F2C0C0;

  --color-info: #5A8FAA;
  --color-info-light: #74A5BD;
  --color-info-lighter: #8EBBD0;
  --color-info-lightest: #A8D1E3;
  --color-info-subtle: #5A8FAA1a;
  --color-info-light-muted: #5A8FAA33;
  --color-info-light-emphasis: #5A8FAA4c;

  // Button-specific colors
  --color-button-primary: #D4A054;
  --color-button-primary-light: #E0B76E;
  --color-button-primary-subtle: #D4A05419;
  --color-button-primary-emphasis: #D4A0544c;
  --color-button-primary-text: #0F0E0C;

  --color-button-secondary: #5A8FAA;
  --color-button-secondary-light: #74A5BD;
  --color-button-secondary-subtle: #5A8FAA1a;
  --color-button-secondary-emphasis: #5A8FAA4c;
  --color-button-secondary-text: #F0EBE3;

  --color-button-danger: #CC5F5F;
  --color-button-danger-light: #D98080;
  --color-button-danger-subtle: #CC5F5F1a;
  --color-button-danger-emphasis: #CC5F5F4c;
  --color-button-danger-text: #F0EBE3;

  --color-input-bg: #171614;
  --color-input-border: #3D3A36;
  --color-input-text: #F0EBE3;

  --color-agent-auto-approve: #5DB86C;
  --color-agent-aider-tools: #D4A054;
  --color-agent-power-tools: #B08DC9;
  --color-agent-todo-tools: #5A8FAA;
  --color-agent-tasks-tools: #5DB86C;
  --color-agent-memory-tools: #C97BA0;
  --color-agent-skills-tools: #8FB85D;
  --color-agent-subagents-tools: #5AAA85;
  --color-agent-context-files: #D4C054;
  --color-agent-repo-map: #74A5BD;
  --color-agent-ai-request: #AA6B5A;
  --color-agent-sub-agent: #6B8FAA;
}

================
File: src/renderer/src/themes/theme-ocean.scss
================
.theme-ocean {

  --color-bg-primary: #0c4a6e;
  --color-bg-primary-light: #075985;
  --color-bg-primary-light-strong: #07598580;
  --color-bg-secondary: #0369a1;
  --color-bg-secondary-light: #0284c7;
  --color-bg-secondary-light-strongest: #0284c7f2;
  --color-bg-tertiary: #0ea5e9;
  --color-bg-tertiary-emphasis: #0ea5e94c;
  --color-bg-tertiary-strong: #0ea5e97f;
  --color-bg-fourth: #38bdf8;
  --color-bg-fourth-muted: #38bdf833;
  --color-bg-fourth-emphasis: #38bdf84c;
  --color-bg-fifth: #7dd3fc;
  --color-bg-selection: #0ea5e9;
  --color-bg-code-block: #082f49;

  --color-bg-diff-viewer-old-primary: #7c2d12;
  --color-bg-diff-viewer-old-secondary: #571c0c;
  --color-bg-diff-viewer-new-primary: #134e4a;
  --color-bg-diff-viewer-new-secondary: #0f3027;

  --color-text-primary: #f0f9ff;
  --color-text-secondary: #bae6fd;
  --color-text-tertiary: #7dd3fc;
  --color-text-muted-light: #38bdf8;
  --color-text-muted: #0ea5e9;
  --color-text-muted-dark: #0284c7;
  --color-text-dark: #0369a1;

  --color-border-dark: #0369a1;
  --color-border-dark-light: #0284c7;
  --color-border-dark-light-strong: #0284c780;
  --color-border-default-dark: #0ea5e9;
  --color-border-default: #38bdf8;
  --color-border-accent: #7dd3fc;
  --color-border-light: #bae6fd;

  --color-accent-primary: #7dd3fc;
  --color-accent-secondary: #38bdf8;
  --color-accent-light: #bae6fd;

  --color-success: #06b6d4;
  --color-success-light: #22d3ee;
  --color-success-subtle: #06b6d41a;
  --color-success-muted: #06b6d433;
  --color-success-emphasis: #06b6d44c;

  --color-warning: #f59e0b;
  --color-warning-subtle: #f59e0b19;
  --color-warning-emphasis: #f59e0b4c;
  --color-warning-light: #fbbf24;
  --color-warning-text: #0c4a6e;

  --color-error: #dc2626;
  --color-error-dark: #991b1b;
  --color-error-subtle: #dc26261a;
  --color-error-muted: #dc262626;
  --color-error-emphasis: #dc26264c;
  --color-error-strong: #dc262680;
  --color-error-light: #ef4444;
  --color-error-lighter: #f87171;
  --color-error-lightest: #fecaca;

  --color-info: #0ea5e9;
  --color-info-light: #38bdf8;
  --color-info-lighter: #7dd3fc;
  --color-info-lightest: #bae6fd;
  --color-info-subtle: #0ea5e91a;
  --color-info-light-muted: #0ea5e933;
  --color-info-light-emphasis: #0ea5e94c;

  // Button-specific colors
  --color-button-primary: #7dd3fc;
  --color-button-primary-light: #bae6fd;
  --color-button-primary-subtle: #7dd3fc19;
  --color-button-primary-emphasis: #7dd3fc4c;
  --color-button-primary-text: #0c4a6e;

  --color-button-secondary: #0ea5e9;
  --color-button-secondary-light: #38bdf8;
  --color-button-secondary-subtle: #0ea5e91a;
  --color-button-secondary-emphasis: #0ea5e94c;
  --color-button-secondary-text: #f0f9ff;

  --color-button-danger: #dc2626;
  --color-button-danger-light: #ef4444;
  --color-button-danger-subtle: #dc26261a;
  --color-button-danger-emphasis: #dc26264c;
  --color-button-danger-text: #f0f9ff;

  --color-input-bg: #075985;
  --color-input-border: #38bdf8;
  --color-input-text: #f0f9ff;

  --color-agent-auto-approve: #22d3ee;
  --color-agent-aider-tools: #f59e0b;
  --color-agent-power-tools: #8b5cf6;
  --color-agent-todo-tools: #38bdf8;
  --color-agent-tasks-tools: #14b8a6;
  --color-agent-memory-tools: #a78bfa;
  --color-agent-skills-tools: #3d92cd;
  --color-agent-subagents-tools: #55aa7f;
  --color-agent-context-files: #06b6d4;
  --color-agent-repo-map: #60a5fa;
  --color-agent-ai-request: #dc2626;
  --color-agent-sub-agent: #1d4ed8;
}

================
File: src/renderer/src/themes/theme-serenity.scss
================
.theme-serenity {

  --color-bg-primary: #e8e6e3;
  --color-bg-primary-light: #e2e0dc;
  --color-bg-primary-light-strong: #e2e0dc80;
  --color-bg-secondary: #d9d6d1;
  --color-bg-secondary-light: #cfc9c2;
  --color-bg-secondary-light-strongest: #cfc9c2f2;
  --color-bg-tertiary: #c4bdb4;
  --color-bg-tertiary-emphasis: #c4bdb44c;
  --color-bg-tertiary-strong: #c4bdb47f;
  --color-bg-fourth: #b5aca0;
  --color-bg-fourth-muted: #b5aca033;
  --color-bg-fourth-emphasis: #b5aca04c;
  --color-bg-fifth: #9d9186;
  --color-bg-selection: #c4bdb4;
  --color-bg-code-block: #f8f3ee;

  --color-bg-diff-viewer-old-primary: #fee2e2;
  --color-bg-diff-viewer-old-secondary: #fecaca;
  --color-bg-diff-viewer-new-primary: #dcfce7;
  --color-bg-diff-viewer-new-secondary: #bbf7d0;

  --color-text-primary: #3a3530;
  --color-text-secondary: #4a453f;
  --color-text-tertiary: #5a544d;
  --color-text-muted-light: #8a7f73;
  --color-text-muted: #6a5f53;
  --color-text-muted-dark: #4a453f;
  --color-text-dark: #2a251f;

  --color-border-dark: #d9d6d1;
  --color-border-dark-light: #cfc9c2;
  --color-border-dark-light-strong: #cfc9c280;
  --color-border-default-dark: #c4bdb4;
  --color-border-default: #b5aca0;
  --color-border-accent: #9d9186;
  --color-border-light: #5a544d;

  --color-accent-primary: #7a6b5d;
  --color-accent-secondary: #9d8b7a;
  --color-accent-light: #b5a394;

  --color-success: #6b8e5a;
  --color-success-light: #8ba876;
  --color-success-subtle: #6b8e5a1a;
  --color-success-muted: #6b8e5a33;
  --color-success-emphasis: #6b8e5a4c;

  --color-warning: #b8956a;
  --color-warning-subtle: #b8956a19;
  --color-warning-emphasis: #b8956a4c;
  --color-warning-light: #d4b887;
  --color-warning-text: #ffffff;

  --color-error: #a66b5b;
  --color-error-dark: #8b5a4a;
  --color-error-subtle: #a66b5b1a;
  --color-error-muted: #a66b5b26;
  --color-error-emphasis: #a66b5b4c;
  --color-error-strong: #a66b5b80;
  --color-error-light: #c18a7a;
  --color-error-lighter: #d4a394;
  --color-error-lightest: #e8c4b8;

  --color-info: #7a8ba6;
  --color-info-light: #9aa8bd;
  --color-info-lighter: #b5c2d1;
  --color-info-lightest: #d1dae6;
  --color-info-subtle: #7a8ba61a;
  --color-info-light-muted: #7a8ba633;
  --color-info-light-emphasis: #7a8ba64c;

  // Button-specific colors
  --color-button-primary: #7a6b5d;
  --color-button-primary-light: #9d8b7a;
  --color-button-primary-subtle: #7a6b5d1a;
  --color-button-primary-emphasis: #7a6b5d4c;
  --color-button-primary-text: #f5f2ed;

  --color-button-secondary: #5a544d;
  --color-button-secondary-light: #6a5f53;
  --color-button-secondary-subtle: #5a544d1a;
  --color-button-secondary-emphasis: #5a544d4c;
  --color-button-secondary-text: #f5f2ed;

  --color-button-danger: #a66b5b;
  --color-button-danger-light: #c18a7a;
  --color-button-danger-subtle: #a66b5b1a;
  --color-button-danger-emphasis: #a66b5b4c;
  --color-button-danger-text: #f5f2ed;

  --color-input-bg: #ddd9d4;
  --color-input-border: #b5aca0;
  --color-input-text: #3a3530;

  --color-agent-auto-approve: #8ba876;
  --color-agent-aider-tools: #d4b887;
  --color-agent-power-tools: #b5a394;
  --color-agent-todo-tools: #9aa8bd;
  --color-agent-tasks-tools: #8a7f73;
  --color-agent-memory-tools: #9d8b7a;
  --color-agent-skills-tools: #a68a7a;
  --color-agent-subagents-tools: #55aa7f;
  --color-agent-context-files: #c9b896;
  --color-agent-repo-map: #b5c2d1;
  --color-agent-ai-request: #a66b5b;
  --color-agent-sub-agent: #7a8ba6;
}

================
File: src/renderer/src/themes/themes.scss
================
@use 'theme-dark.scss';
@use 'theme-light.scss';
@use 'theme-bw.scss';
@use 'theme-midnight.scss';
@use 'theme-neon.scss';
@use 'theme-ocean.scss';
@use 'theme-forest.scss';
@use 'theme-charcoal.scss';
@use 'theme-lavender.scss';
@use 'theme-neopunk.scss';
@use 'theme-aurora.scss';
@use 'theme-serenity.scss';
@use 'theme-cappuccino.scss';
@use 'theme-fresh.scss';
@use 'theme-botanical-garden.scss';
@use 'theme-botanical-garden-dark.scss';
@use 'theme-obsidian.scss';
@use 'theme-crimson.scss';

================
File: src/renderer/src/utils/agents.ts
================
import { AgentProfile, TaskData } from '@common/types';
import { useMemo } from 'react';
⋮----
import { useAgents } from '@/contexts/AgentsContext';
import { useProjectSettings } from '@/contexts/ProjectSettingsContext';
⋮----
/**
 * Resolves the active agent profile for a task following the correct hierarchy:
 * 1. Task-level agent profile ID
 * 2. Project-level agent profile ID (fallback)
 * 3. Creates temporary profile if task has provider/model
 */
export const resolveAgentProfile = (
  task: TaskData | undefined | null,
  projectAgentProfileId: string | undefined,
  profiles: AgentProfile[],
): AgentProfile | null =>
⋮----
// Check task-level agent profile first
⋮----
// If no task-level profile, fall back to project-level
⋮----
// Create a profile with task-level overrides
⋮----
/**
 * Hook to resolve the active agent profile for a task.
 * Uses useProjectSettings and useAgents internally to get the necessary data.
 */
export const useActiveAgentProfile = (task: TaskData | undefined | null, projectDir?: string): AgentProfile | null =>

================
File: src/renderer/src/utils/browser-notifications.ts
================
export const requestNotificationPermission = async (): Promise<boolean> =>
⋮----
// eslint-disable-next-line no-console
⋮----
export const showNotification = async (title: string, body: string): Promise<void> =>

================
File: src/renderer/src/utils/date-utils.ts
================
import { TaskData } from '@common/types';
⋮----
export const isToday = (dateString: string): boolean =>
⋮----
export const isYesterday = (dateString: string): boolean =>
⋮----
export const formatDateShort = (dateString: string): string =>
⋮----
export const getTaskDateGroup = (task: TaskData): string =>
⋮----
export const groupTasksByDate = (tasks: TaskData[]): Map<string, TaskData[]> =>

================
File: src/renderer/src/utils/extension-icons.ts
================
import { useEffect, useRef, useState } from 'react';
⋮----
export type ReactIconsMap = Record<string, Record<string, unknown>>;
⋮----
export const loadReactIcons = (): Promise<ReactIconsMap> =>
⋮----
export const useReactIcons = (): ReactIconsMap | null =>

================
File: src/renderer/src/utils/extension-library-loader.ts
================
import { ExtensionDisplayAPI } from '@common/api';
⋮----
const convertAsAliases = (exports: string): string =>
⋮----
type Replacer = (...args: string[]) => string;
⋮----
const rewriteImports = (source: string): string =>
⋮----
export const initExtensionLibraryLoader = async (): Promise<void> =>
⋮----
export const loadExtensionLibrary = async (api: ExtensionDisplayAPI, librarySpec: string): Promise<Record<string, unknown>> =>
⋮----
const module = await import(/* @vite-ignore */ url);
⋮----
export const loadAllLibraries = async (api: ExtensionDisplayAPI, libraries: Record<string, string>): Promise<Record<string, Record<string, unknown>>> =>
⋮----
// eslint-disable-next-line no-console

================
File: src/renderer/src/utils/highlighter.tsx
================
import { common, createLowlight } from 'lowlight';
import { refractor } from 'refractor';
import { ReactNode, Fragment } from 'react';
import { toJsxRuntime } from 'hast-util-to-jsx-runtime';
import { jsx, jsxs } from 'react/jsx-runtime';
import { Nodes, Root, Element, Text, Comment, Doctype } from 'hast';
import { map } from 'unist-util-map';
⋮----
// Create lowlight instance with common languages
⋮----
/**
 * Maps standard highlighting classes to Prism-compatible token classes.
 * These mappings ensure that Highlight.js (lowlight) classes are translated
 * to the classes expected by the Prism-based theme.css.
 */
const mapToPrismClass = (className: string): string =>
⋮----
/**
 * Converts HAST nodes to React elements using hast-util-to-jsx-runtime
 * This is more performant and robust than manual conversion.
 */
export const renderHast = (nodes: Nodes | Nodes[], usePrismClasses = false): ReactNode =>
⋮----
// If the class is already 'token', don't duplicate it
⋮----
// eslint-disable-next-line @typescript-eslint/no-explicit-any
⋮----
// eslint-disable-next-line @typescript-eslint/no-explicit-any
⋮----
/**
 * Highlights code using lowlight (best for auto-detection)
 */
export const highlightWithLowlight = (code: string, language?: string): ReactNode =>
⋮----
// Fallback to auto-detection
⋮----
// eslint-disable-next-line no-console
⋮----
/**
 * Highlights code using refractor (best for diffs/Prism compatibility)
 */
export const highlightWithRefractor = (code: string, language: string): ReactNode =>
⋮----
// eslint-disable-next-line no-console

================
File: src/renderer/src/utils/hotkeys.ts
================
import { HotkeyConfig } from '@common/types';
⋮----
export const getHotkeys = (config?: HotkeyConfig) =>

================
File: src/renderer/src/utils/indexedDB.ts
================
interface DBEntry<T> {
  id: string;
  data: T;
  createdAt: number;
  updatedAt: number;
}
⋮----
interface IndexedDBConfig {
  dbName: string;
  version: number;
  stores: Record<string, string>;
}
⋮----
class IndexedDBManager
⋮----
constructor(private config: IndexedDBConfig)
⋮----
private async init(): Promise<void>
⋮----
// Create object stores
⋮----
// Create indexes for common queries
⋮----
private async getStore(storeName: string, mode: IDBTransactionMode = 'readonly'): Promise<IDBObjectStore>
⋮----
async get<T>(storeName: string, id: string): Promise<T | null>
⋮----
// eslint-disable-next-line no-console
⋮----
async set<T>(storeName: string, id: string, data: T): Promise<void>
⋮----
// eslint-disable-next-line no-console
⋮----
async remove(storeName: string, id: string): Promise<void>
⋮----
// eslint-disable-next-line no-console
⋮----
async clearExpired(storeName: string, ttlMs: number): Promise<void>
⋮----
// eslint-disable-next-line no-console
⋮----
async getAll<T>(storeName: string): Promise<Array<
⋮----
// eslint-disable-next-line no-console
⋮----
// Create a singleton instance for UI preferences

================
File: src/renderer/src/utils/notifications.ts
================
import { toast } from 'react-toastify';
⋮----
import type { ToastOptions } from 'react-toastify';
⋮----
const getOptions = (): ToastOptions => (
⋮----
export const showSuccessNotification = (message: string) =>
⋮----
export const showErrorNotification = (message: string, autoClose: number | false = 3000) =>
⋮----
export const showInfoNotification = (message: string) =>
⋮----
export const showWarningNotification = (message: string) =>

================
File: src/renderer/src/utils/path-utils.ts
================
export const getPathBasename = (inputPath: string): string =>

================
File: src/renderer/src/utils/readonly-events.ts
================
import { v4 as uuidv4 } from 'uuid';
import {
  LoadingMessage,
  LogData,
  LogMessage,
  Message,
  ResponseChunkData,
  ResponseCompletedData,
  TaskCreatedData,
  TaskData,
  TaskStateData,
  ToolData,
  UserMessageData,
} from '@common/types';
⋮----
import type { ReadonlyEvent } from '@/api/readonly-browser-api';
⋮----
const upsertMessage = (state: TaskStateData, id: string, message: TaskStateData['messages'][number]): TaskStateData =>
⋮----
const upsertTask = (tasks: TaskData[], task: TaskData): TaskData[] =>
⋮----
export const isReadonlyTaskListEvent = (event: ReadonlyEvent): boolean
⋮----
export const applyReadonlyTaskListEvent = (tasks: TaskData[], event: ReadonlyEvent): TaskData[] =>
⋮----
export const applyReadonlyEvent = (state: TaskStateData, event: ReadonlyEvent, taskId: string): TaskStateData =>
⋮----
const isLogEvent = (event: ReadonlyEvent): boolean
⋮----
const isForTask = (event: ReadonlyEvent, taskId: string): boolean =>
⋮----
export const applyReadonlyLogEvent = (messages: Message[], event: ReadonlyEvent, taskId: string, defaultLoadingText: string): Message[] =>

================
File: src/renderer/src/utils/refractor-languages.ts
================
// Auto-generated map of lazy-loadable refractor language grammars
// Each entry maps a language name or alias to a dynamic import
import type { Syntax } from 'refractor';
⋮----
type LangLoader = () => Promise<{ default: Syntax }>;
⋮----
export const getLangLoader = (lang: string): LangLoader | undefined

================
File: src/renderer/src/utils/routes.ts
================
// URL parameter utilities for direct project/task navigation
⋮----
/**
 * Encodes a baseDir for use in URL parameters
 * @param baseDir - The project base directory path
 * @returns URL-encoded baseDir
 */
export const encodeBaseDir = (baseDir: string): string =>
⋮----
/**
 * Decodes a baseDir from URL parameters
 * @param encodedBaseDir - The URL-encoded baseDir
 * @returns Decoded baseDir path
 */
export const decodeBaseDir = (encodedBaseDir: string): string =>

================
File: src/renderer/src/utils/string-utils.ts
================
import type { TFunction } from 'i18next';
⋮----
export const formatHumanReadable = (t: TFunction, value: number): string =>

================
File: src/renderer/src/utils/task-messages.ts
================
import { v4 as uuidv4 } from 'uuid';
import { TODO_TOOL_GROUP_NAME } from '@common/tools';
import {
  Message,
  ReflectedMessage,
  ResponseCompletedData,
  ResponseMessage,
  TaskStateData,
  ToolData,
  ToolMessage,
  UserMessage,
  UserMessageData,
} from '@common/types';
⋮----
const convertUserMessage = (data: UserMessageData): UserMessage => (
⋮----
const convertResponse = (data: ResponseCompletedData): Message[] =>
⋮----
const convertTool = (data: ToolData): ToolMessage => (
⋮----
export const convertTaskStateMessages = (messages: TaskStateData['messages']): Message[] =>

================
File: src/renderer/src/utils/task-utils.ts
================
import { DEFAULT_TASK_STATES, TaskData } from '@common/types';
⋮----
import { getTaskDateGroup } from './date-utils';
⋮----
export type VirtualTaskItem = { type: 'header'; id: string; title: string } | { type: 'task'; id: string; task: TaskData; level: number };
⋮----
export const getTaskDir = (task: TaskData): string
⋮----
export const countAllSubtasks = (taskId: string, allTasks: TaskData[]): number
⋮----
const getMostRecentUpdatedAt = (task: TaskData, allTasks: TaskData[]): string | undefined =>
⋮----
export const getSortedVisibleTasks = (tasks: TaskData[], selectedStates: Set<string>, showArchived: boolean = false, searchQuery: string = ''): TaskData[] =>
⋮----
const isAnyAncestorArchived = (task: TaskData): boolean =>
⋮----
const sortFn = (a: TaskData, b: TaskData) =>
⋮----
// Pinned tasks come first
⋮----
// Then sort by most recent updatedAt (including subtasks, descending)
⋮----
const addTaskWithChildren = (task: TaskData) =>
⋮----
export const flattenTasksForVirtualization = (
  tasks: TaskData[],
  expandedIds: Set<string>,
  selectedStates: Set<string>,
  showArchived: boolean = false,
  searchQuery: string = '',
): VirtualTaskItem[] =>
⋮----
const addTaskWithSubtasks = (task: TaskData, level: number) =>
⋮----
// Add pinned tasks first (without header)

================
File: src/renderer/src/voice/gemini.ts
================
import { GeminiVoiceModel } from '@common/agent';
import { GoogleGenAI, LiveServerMessage, Modality, Session } from '@google/genai';
⋮----
import { VoiceProvider, VoiceSession, VoiceSessionConfig, VoiceSessionState } from './types';
⋮----
export class GeminiVoiceProvider implements VoiceProvider
⋮----
// Silence detection refs
⋮----
// Session management refs
⋮----
async startSession(config: VoiceSessionConfig): Promise<VoiceSession>
⋮----
// Setup audio processing once and keep it alive
⋮----
private async createGeminiSession(config: VoiceSessionConfig): Promise<VoiceSession>
⋮----
// The transcription-only live model streams text transcriptions instead of audio.
⋮----
// eslint-disable-next-line no-console
⋮----
// eslint-disable-next-line no-console
⋮----
// Initialize silence detection
⋮----
async stopSession(): Promise<void>
⋮----
// Clear silence detection timeout
⋮----
// Close Gemini session
⋮----
// Cleanup audio processing
⋮----
// Clear audio queue
⋮----
private async closeGeminiSession(): Promise<void>
⋮----
// eslint-disable-next-line no-console
⋮----
private async setupAudioProcessing(stream: MediaStream): Promise<void>
⋮----
// Calculate RMS to detect audio level
⋮----
// Check if audio level is above threshold (adjust as needed)
const audioThreshold = 0.01; // Threshold for detecting audio input
⋮----
// Reset the silence timer when audio is detected
⋮----
// Clear existing timeout
⋮----
// Check for silence
⋮----
// Send directly to active session
⋮----
// Queue audio when session is inactive but audio processing is active
⋮----
private handleMessage(message: LiveServerMessage): void
⋮----
private async handleSessionClose(): Promise<void>
⋮----
private convertFloat32ToInt16(float32Array: Float32Array): Int16Array
⋮----
private arrayBufferToBase64(buffer: ArrayBuffer): string
⋮----
private setState(newState: VoiceSessionState): void
⋮----
private queueAudioData(base64Audio: string): void
⋮----
// Limit queue size to prevent memory issues
⋮----
this.audioQueue.shift(); // Remove oldest
⋮----
private flushAudioQueue(): void
⋮----
// Factory function
export const createGeminiVoiceProvider = (): VoiceProvider =>

================
File: src/renderer/src/voice/index.ts
================
// Future exports: export * from './anthropic';

================
File: src/renderer/src/voice/openai.ts
================
import { VoiceProvider, VoiceSession, VoiceSessionConfig, VoiceSessionState } from './types';
⋮----
export class OpenAIVoiceProvider implements VoiceProvider
⋮----
async startSession(config: VoiceSessionConfig): Promise<VoiceSession>
⋮----
private async connectWebRTC(): Promise<void>
⋮----
// Create a peer connection
⋮----
// Add local audio track for microphone input in the browser
⋮----
// Set up data channel for sending and receiving events
⋮----
// Start the session using the Session Description Protocol (SDP)
⋮----
// eslint-disable-next-line no-console
⋮----
// Listen for server events
⋮----
// eslint-disable-next-line no-console
⋮----
// eslint-disable-next-line no-console
⋮----
async stopSession(): Promise<void>
⋮----
private setState(newState: VoiceSessionState): void
⋮----
// Factory function
export const createOpenAIVoiceProvider = (): VoiceProvider =>

================
File: src/renderer/src/voice/types.ts
================
export interface VoiceProvider {
  name: string;
  startSession(config: VoiceSessionConfig): Promise<VoiceSession>;
  stopSession(): Promise<void>;
  onTranscription?: (text: string) => void;
  onError?: (error: Error) => void;
  onSessionStateChange?: (state: VoiceSessionState) => void;
}
⋮----
startSession(config: VoiceSessionConfig): Promise<VoiceSession>;
stopSession(): Promise<void>;
⋮----
export interface VoiceSessionConfig {
  token: string;
  model: string;
  mediaStream: MediaStream;
  idleTimeoutMs: number;
  onTranscription: (text: string) => void;
  onError: (error: Error) => void;
  onSessionStateChange?: (state: VoiceSessionState) => void;
  onStopRecording?: () => void;
}
⋮----
export interface VoiceSession {
  isActive: boolean;
  provider: VoiceProvider;
}
⋮----
export enum VoiceSessionState {
  IDLE = 'idle',
  CONNECTING = 'connecting',
  ACTIVE = 'active',
  ERROR = 'error',
  CLOSED = 'closed',
}

================
File: src/renderer/src/App.tsx
================
import { AnimatePresence, motion } from 'framer-motion';
import { lazy, Suspense, useEffect, useState } from 'react';
import { HashRouter as Router, Navigate, Route, Routes, useLocation } from 'react-router-dom';
import { HotkeysProvider } from 'react-hotkeys-hook';
import { ToastContainer } from 'react-toastify';
import { useTranslation } from 'react-i18next';
import { BrowserBootstrap, THEMES } from '@common/types';
import { IconContext } from 'react-icons';
⋮----
import { Home } from '@/pages/Home';
import { Logs } from '@/pages/Logs';
import { ContextMenuProvider, useContextMenu } from '@/contexts/ContextMenuContext';
⋮----
import { ROUTES } from '@/utils/routes';
⋮----
import { TooltipProvider } from '@/components/ui/Tooltip';
import { ApiProvider, useApi } from '@/contexts/ApiContext';
import { ModelProviderProvider } from '@/contexts/ModelProviderContext';
import { AgentsProvider } from '@/contexts/AgentsContext';
import { McpServersProvider } from '@/contexts/McpServersContext';
import { ModalOverlayUrlViewer } from '@/components/common/ModalOverlayUrlViewer';
import { InputPromptDialog } from '@/components/common/InputPromptDialog';
import { UpdatedFilesDiff } from '@/pages/UpdatedFilesDiff';
import { ExtensionsProvider } from '@/contexts/ExtensionsContext';
import { DiffsWorkerPoolProvider } from '@/contexts/DiffsWorkerPoolContext';
import { useSettingsStore } from '@/stores/settingsStore';
import { ReadonlyApp } from '@/pages/ReadonlyApp';
import { loadBrowserBootstrap } from '@/api/readonly-browser-api';
import { CommandPalette } from '@/components/common/CommandPalette';
import { useCommandPaletteHotkeys } from '@/hooks/useCommandPaletteHotkeys';
import { usePaletteCommands } from '@/hooks/usePaletteCommands';
⋮----
const ModalOverlayUrlHandler = () =>
⋮----
const SettingsInitializer = () =>
⋮----
const ThemeAndFontManager = () =>
⋮----
// Remove all theme classes first
⋮----
// Add the current theme class, default to dark

================
File: src/renderer/src/env.d.ts
================
/// <reference types="vite/client" />
⋮----
import { ApplicationAPI } from '@common/api';
⋮----
interface Window {
    api: ApplicationAPI;
  }

================
File: src/renderer/src/fonts.css
================
@font-face {

================
File: src/renderer/src/main.css
================
@tailwind base;
@tailwind components;
@tailwind utilities;
⋮----
.react-resizable {
⋮----
.react-resizable-handle {
⋮----
.react-resizable-handle::before {
⋮----
@layer base {
⋮----
html {
⋮----
.break-words {
⋮----
:root {
⋮----
body {
⋮----
* {
⋮----
code {
⋮----
.cm-fat-cursor {
⋮----
.cm-placeholder {
⋮----
.cm-line {
⋮----
.cm-tooltip-autocomplete {
⋮----
.cm-tooltip-autocomplete > ul {
⋮----
.cm-tooltip-autocomplete > ul > li {
⋮----
.cm-tooltip-autocomplete-detail {
⋮----
.cm-tooltip-autocomplete-chip {
⋮----
.cm-tooltip-autocomplete > ul > li:hover {
⋮----
.cm-tooltip-autocomplete > ul > li[aria-selected] {
⋮----
/* Scrollbar styles for Webkit browsers */
.cm-tooltip-autocomplete::-webkit-scrollbar {
⋮----
.cm-tooltip-autocomplete::-webkit-scrollbar-track {
⋮----
.cm-tooltip-autocomplete::-webkit-scrollbar-thumb {
⋮----
.cm-tooltip-autocomplete::-webkit-scrollbar-thumb:hover {
⋮----
::selection {

================
File: src/renderer/src/main.tsx
================
import ReactDOM from 'react-dom/client';
⋮----
import App from './App';
⋮----
import { AppErrorBoundary } from '@/components/common/AppErrorBoundary';
import { DevelopmentPerformanceCleanup } from '@/components/common/DevelopmentPerformanceCleanup';
⋮----
const registerServiceWorker = () =>
⋮----
// eslint-disable-next-line no-console

================
File: src/renderer/index.html
================
<!doctype html>
<html>
  <head>
<!--    <script-->
<!--      crossOrigin="anonymous"-->
<!--      src="https://unpkg.com/react-scan/dist/auto.global.js"-->
<!--    ></script>-->
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
    <title>AiderDesk</title>
    <!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
    <meta
      http-equiv="Content-Security-Policy"
      content="default-src 'self'; script-src 'self' blob: 'unsafe-eval' https://unpkg.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://fonts.gstatic.com; img-src 'self' data: file: https:; font-src 'self' https://fonts.gstatic.com; connect-src 'self' data: http: https: ws: wss:; frame-src http: https:;"
    />
    <link rel="stylesheet" href="/src/fonts.css">
    <link rel="icon" type="image/x-icon" href="/favicon.ico">
    <link rel="manifest" href="/manifest.json">
    <meta name="theme-color" content="#3b82f6">
    <meta name="apple-mobile-web-app-capable" content="yes">
    <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
    <meta name="apple-mobile-web-app-title" content="AiderDesk">
    <link rel="apple-touch-icon" href="/icon-192.png">
  </head>

  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>

================
File: src/renderer/progress.html
================
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Starting...</title>
  <style>
    @font-face {
      font-family: 'Sono';
      src: url('./src/fonts/Sono.ttf') format('truetype');
      font-weight: 400;
      font-style: normal;
    }
  </style>
  <style>
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }

    body {
      padding: 32px;
      background: transparent;
      color: #f1f3f5;
      font-family: 'Sono', monospace;
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      height: 100vh;
      overflow: hidden;
      position: relative;
      -webkit-app-region: drag;
    }

    .container {
      position: relative;
      background: #222431;
      background-size: cover;
      background-position: center;
      background-repeat: no-repeat;
      border: 1px solid #3d4166;
      border-radius: 12px;
      padding: 32px;
      width: 100%;
      aspect-ratio: 500 / 330;
      display: flex;
      flex-direction: column;
      justify-content: flex-end;
      box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
      -webkit-app-region: no-drag;
    }

    #splash-video {
      position: absolute;
      inset: 0;
      width: 100%;
      height: 100%;
      object-fit: cover;
      border-radius: 12px;
      z-index: 0;
    }

    .detail {
      font-size: 13px;
      font-weight: 400;
      /*color: #999ba3;*/
      text-align: center;
      min-height: 20px;
      margin-bottom: 28px;
      position: relative;
      z-index: 1;
    }

    .detail-text {
      opacity: 0;
      animation: fadeIn 0.3s ease forwards;
    }

    @keyframes fadeIn {
      to { opacity: 1; }
    }

    .completed .detail {
      color: #22c55edd;
    }

    .progress-container {
      width: 100%;
      height: 2px;
      background: #333652;
      border-radius: 1px;
      overflow: hidden;
      margin-bottom: 20px;
      position: relative;
      z-index: 1;
    }

    .progress-bar {
      position: absolute;
      top: 0;
      left: 0;
      height: 100%;
      background: #f1f3f588;
      border-radius: 1px;
      transition: width 0.3s ease;
    }

    .progress-indeterminate {
      width: 40%;
      animation: indeterminate-slide 1.8s cubic-bezier(0.4, 0, 0.2, 1) infinite;
    }

    @keyframes indeterminate-slide {
      0% {
        left: -40%;
        right: 100%;
      }
      60% {
        left: 100%;
        right: -40%;
      }
      100% {
        left: 100%;
        right: -40%;
      }
    }

    .completed .progress-bar {
      width: 100% !important;
      animation: none;
      background: #22c55e;
    }

    .log-line {
      width: 100%;
      font-size: 11px;
      /*color: #7b7d86;*/
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
      font-family: 'Sono', monospace;
      text-align: center;
      opacity: 0.8;
      min-height: 16px;
      transition: opacity 0.3s ease;
      position: relative;
      z-index: 1;
    }

    .log-line:not(:empty) {
      opacity: 1;
      animation: slideUp 0.3s ease;
    }

    @keyframes slideUp {
      from {
        transform: translateY(4px);
        opacity: 0;
      }
      to {
        transform: translateY(0);
        opacity: 1;
      }
    }

    .completed .detail {
      color: #22c55e4c;
    }
  </style>
</head>
<body>
  <div class="container">
    <video id="splash-video" autoplay muted loop></video>
    <div class="detail">
      <span class="detail-text" id="detail-text">Initializing...</span>
    </div>

    <div class="progress-container">
      <div class="progress-bar progress-indeterminate" id="progress-bar"></div>
    </div>

    <div class="log-line" id="log-line"></div>
  </div>

  <script>
    const { ipcRenderer } = require('electron');

    ipcRenderer.on('set-splash-image', (_, imagePath) => {
      const video = document.getElementById('splash-video');
      video.src = imagePath;
    });

    ipcRenderer.on('set-detail', (_, value) => {
      const detailTextEl = document.getElementById('detail-text');

      // Trigger fade-in animation by removing and re-adding the element
      detailTextEl.style.animation = 'none';
      setTimeout(() => {
        detailTextEl.textContent = value;
        detailTextEl.style.animation = 'fadeIn 0.4s ease forwards';
      }, 10);
    });

    ipcRenderer.on('set-completed', () => {
      document.body.classList.add('completed');
      const progressBar = document.getElementById('progress-bar');
      progressBar.classList.remove('progress-indeterminate');
    });

    const logLine = document.getElementById('log-line');

    ipcRenderer.on('add-log', (_, message) => {
      logLine.textContent = message;
    });
  </script>
</body>
</html>

================
File: electron-builder.yml
================
appId: com.hotovo.aider-desk
productName: aider-desk
directories:
  buildResources: build
files:
  - '!**/.vscode/*'
  - '!src/*'
  - '!resources/*'
  - '!docs-site/*'
  - '!electron.vite.config.{js,ts,mjs,cjs}'
  - '!{.eslintignore,.eslintrc.cjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}'
  - '!{.env,.env.*,.npmrc,pnpm-lock.yaml}'
  - '!{tsconfig.json,tsconfig.node.json,tsconfig.web.json}'
  - '!.aider-desk/*'
  - '!llms.txt'
  - '!packages/*'
  - '!node_modules/react-icons/**'
  - '!node_modules/mermaid/**'
  - '!node_modules/@mermaid-js/**'
  - '!node_modules/@shikijs/**'
extraResources:
  - from: ./resources/connector
    to: connector
    filter:
      - '**/*'
  - from: ./resources/prompts
    to: prompts
    filter:
      - '**/*'
  - from: ./resources/skills
    to: skills
    filter:
      - '**/*'
beforePack: scripts/before-pack.js
afterPack: scripts/after-pack.js
win:
  executableName: aider-desk
  icon: resources/icon.png
  extraResources:
    - from: ./resources/win
      to: win
      filter:
        - '**/*'
nsis:
  artifactName: ${name}-${version}-setup.${ext}
  shortcutName: ${productName}
  uninstallDisplayName: ${productName}
  createDesktopShortcut: always
mac:
  category: public.app-category.developer-tools
  target:
    - target: default
  artifactName: ${productName}-${version}-macos-${arch}.${ext}
  type: distribution
  hardenedRuntime: true
  entitlements: build/entitlements.mac.plist
  entitlementsInherit: build/entitlements.mac.plist
  extendInfo:
    - NSMicrophoneUsageDescription: Application requests access to the device's microphone.
  gatekeeperAssess: false
  notarize: true
  extraResources:
    - from: ./resources/macos
      to: macos
      filter:
        - '**/*'
dmg:
  artifactName: ${name}-${version}-macos-${arch}.${ext}
linux:
  target:
    - target: AppImage
    - target: deb
    - target: rpm
  maintainer: Hotovo
  category: Utility
  extraResources:
    - from: ./resources/linux
      to: linux
      filter:
        - '**/*'
appImage:
  artifactName: ${name}-${version}-${arch}.${ext}
deb:
  compression: 'gz'
npmRebuild: true
publish:
  provider: github
  releaseType: draft
  vPrefixedTagName: true
  publishAutoUpdate: true

================
File: electron.vite.config.ts
================
import { resolve } from 'path';
⋮----
import { defineConfig } from 'electron-vite';
import react from '@vitejs/plugin-react';
import tsconfigPaths from 'vite-tsconfig-paths';

================
File: package.json
================
{
  "name": "aider-desk",
  "version": "0.87.0-dev",
  "description": "Aider desktop application wrapper",
  "main": "./out/main/index.js",
  "author": "Hotovo",
  "homepage": "https://aiderdesk.hotovo.com",
  "repository": {
    "type": "git",
    "url": "https://github.com/hotovo/aider-desk.git"
  },
  "scripts": {
    "format": "prettier --write .",
    "lint": "eslint src --fix --cache",
    "lint:check": "eslint src --cache",
    "typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false",
    "typecheck:web": "tsc --noEmit -p tsconfig.web.json --composite false",
    "typecheck": "npm run typecheck:node && npm run typecheck:web",
    "start": "electron-vite preview",
    "dev": "electron-vite dev",
    "dev:no-hmr": "NO_HMR=true electron-vite dev",
    "build:mcp-server": "npm run build --workspace=packages/mcp-server",
    "publish:mcp-server": "npm publish --workspace=packages/mcp-server",
    "build:extensions": "npm run build --workspace=packages/extensions",
    "publish:extensions": "npm publish --workspace=packages/extensions",
    "build:tree-sitter-utils": "npm run build --workspace=packages/tree-sitter-utils",
    "publish:tree-sitter-utils": "npm publish --workspace=packages/tree-sitter-utils",
    "prebuild:server": "electron-vite build",
    "build:server": "vite build --config vite.config.server.ts && mkdir -p out/resources && cp -r resources/* out/resources",
    "postbuild:server": "npm rebuild",
    "build": "npm run typecheck && electron-vite build",
    "postinstall": "electron-builder install-app-deps && node node_modules/electron/install.js && node scripts/download-probe.mjs && patch-package && npm run build:extensions",
    "build:unpack": "npm run build && electron-builder --dir",
    "build:win": "npm run build && electron-builder --win",
    "build:mac": "npm run build && electron-builder --mac",
    "build:linux": "npm run build && electron-builder --linux",
    "prepare": "husky",
    "start:server": "node out/server/runner.js",
    "test": "npm run test:node && npm run test:web",
    "test:node": "NODE_ENV=test vitest run --config vitest.config.node.ts",
    "test:web": "NODE_ENV=test vitest run --config vitest.config.web.ts",
    "test:watch": "concurrently \"npm run test:node -- --watch\" \"npm run test:web -- --watch\"",
    "test:coverage": "npm run test:node -- --coverage && npm run test:web -- --coverage",
    "test:ui": "vitest --config vitest.config.web.ts --ui",
    "test:e2e": "playwright test --config e2e/playwright.config.ts",
    "test:e2e:ui": "playwright test --config e2e/playwright.config.ts --ui",
    "test:e2e:headed": "playwright test --config e2e/playwright.config.ts --headed",
    "test:e2e:debug": "playwright test --config e2e/playwright.config.ts --debug",
    "validate:locales": "node scripts/validate-locales.mjs",
    "validate": "npm-run-all --parallel lint typecheck test:web test:node validate:locales"
  },
  "dependencies": {
    "@ai-sdk/alibaba": "2.0.60",
    "@ai-sdk/amazon-bedrock": "5.0.105",
    "@ai-sdk/anthropic": "4.0.71",
    "@ai-sdk/azure": "4.0.90",
    "@ai-sdk/cerebras": "3.0.62",
    "@ai-sdk/deepseek": "3.0.58",
    "@ai-sdk/google": "4.0.87",
    "@ai-sdk/google-vertex": "5.0.101",
    "@ai-sdk/groq": "4.0.54",
    "@ai-sdk/mcp": "2.0.66",
    "@ai-sdk/mistral": "4.0.56",
    "@ai-sdk/openai": "4.0.83",
    "@ai-sdk/openai-compatible": "3.0.62",
    "@ai-sdk/otel": "1.0.127",
    "@ai-sdk/provider": "4.0.21",
    "@aiderdesk/extensions": "workspace:*",
    "@aws-crypto/sha256-js": "5.2.0",
    "@aws-sdk/client-bedrock": "3.1059.0",
    "@aws-sdk/credential-provider-node": "3.972.49",
    "@aws-sdk/credential-providers": "3.1059.0",
    "@aws-sdk/types": "3.973.10",
    "@codemirror/autocomplete": "6.20.0",
    "@codemirror/language": "6.12.2",
    "@codemirror/language-data": "6.5.2",
    "@codemirror/search": "6.6.0",
    "@codemirror/state": "6.5.4",
    "@codemirror/view": "6.39.14",
    "@dnd-kit/core": "6.3.1",
    "@dnd-kit/sortable": "10.0.0",
    "@dnd-kit/utilities": "3.2.2",
    "@dotenvx/dotenvx": "1.52.0",
    "@electron-toolkit/preload": "3.0.2",
    "@electron-toolkit/utils": "4.0.0",
    "@google-cloud/aiplatform": "5.14.0",
    "@google/genai": "1.52.0",
    "@headless-tree/core": "1.7.0",
    "@headless-tree/react": "1.7.0",
    "@headlessui/react": "2.2.9",
    "@homebridge/node-pty-prebuilt-multiarch": "0.13.1",
    "@huggingface/transformers": "3.8.1",
    "@lancedb/lancedb": "0.22.3",
    "@legendapp/list": "3.6.0",
    "@openrouter/ai-sdk-provider": "3.1.0",
    "@opentelemetry/auto-instrumentations-node": "0.79.0",
    "@opentelemetry/exporter-trace-otlp-http": "0.219.0",
    "@opentelemetry/sdk-node": "0.221.0",
    "@opentelemetry/sdk-trace-base": "2.10.0",
    "@pierre/diffs": "1.4.3",
    "@posthog/ai": "8.4.0",
    "@radix-ui/react-tooltip": "1.2.8",
    "@reactuses/core": "6.1.11",
    "@replit/codemirror-vim": "6.3.0",
    "@requesty/ai-sdk": "3.5.0",
    "@smithy/eventstream-codec": "2.2.0",
    "@smithy/protocol-http": "3.3.0",
    "@smithy/signature-v4": "2.3.0",
    "@smithy/util-utf8": "2.3.0",
    "@tanstack/react-virtual": "3.13.12",
    "@types/cors": "2.8.19",
    "@types/express": "4.17.25",
    "@types/hast": "3.0.4",
    "@uiw/codemirror-theme-github": "4.25.4",
    "@uiw/react-codemirror": "4.25.1",
    "@uiw/react-color-sketch": "2.9.5",
    "ai": "7.0.127",
    "apache-arrow": "18.1.0",
    "axios": "1.20.0",
    "babel-plugin-react-compiler": "1.0.0",
    "cheerio": "1.2.0",
    "chokidar": "4.0.3",
    "cloudflared": "0.7.1",
    "clsx": "2.1.1",
    "conf": "15.1.0",
    "cors": "2.8.6",
    "diff": "8.0.4",
    "electron-updater": "6.8.9",
    "env-paths": "3.0.0",
    "express": "4.22.3",
    "file-type": "21.3.4",
    "filenamify": "7.0.1",
    "focus-trap-react": "11.0.6",
    "framer-motion": "11.18.2",
    "ghostty-web": "0.4.0",
    "gitdiff-parser": "0.3.1",
    "glob": "11.1.0",
    "global-agent": "4.1.3",
    "google-auth-library": "10.6.2",
    "gpt-tokenizer": "2.9.0",
    "handlebars": "4.7.9",
    "hast-util-to-jsx-runtime": "2.3.6",
    "html-to-image": "1.11.13",
    "i18next": "24.2.3",
    "i18next-browser-languagedetector": "8.2.1",
    "istextorbinary": "9.5.0",
    "jiti": "2.6.1",
    "langfuse-vercel": "3.38.6",
    "lodash": "4.18.1",
    "lowlight": "3.3.0",
    "mark.js": "8.11.1",
    "match-sorter": "8.2.0",
    "mermaid": "11.16.1",
    "ollama-ai-provider-v2": "4.0.1",
    "os-name": "6.1.0",
    "posthog-node": "5.29.1",
    "prismjs": "1.30.0",
    "react-country-flag": "3.1.0",
    "react-datepicker": "8.10.0",
    "react-diff-view": "3.3.2",
    "react-hotkeys-hook": "4.6.2",
    "react-i18next": "15.7.4",
    "react-icons": "5.7.0",
    "react-markdown": "10.1.0",
    "react-resizable": "3.1.3",
    "react-rnd": "10.5.3",
    "react-router-dom": "7.18.2",
    "react-toastify": "10.0.6",
    "recharts": "3.0.2",
    "refractor": "5.0.0",
    "remark-gfm": "4.0.1",
    "simple-git": "4.0.2",
    "slugify": "1.6.6",
    "socket.io": "4.8.3",
    "socket.io-client": "4.8.3",
    "sucrase": "3.35.1",
    "tailwind-merge": "3.4.1",
    "tmp": "0.2.7",
    "tree-kill": "1.2.2",
    "turndown": "7.2.2",
    "undici": "8.11.2",
    "unidiff": "1.0.4",
    "unist-util-map": "4.0.0",
    "use-sync-external-store": "1.6.0",
    "uuid": "14.0.0",
    "winston": "3.19.0",
    "winston-daily-rotate-file": "5.0.0",
    "winston-transport": "4.9.0",
    "ws": "8.21.0",
    "yaml": "2.8.3",
    "yaml-front-matter": "4.1.1",
    "zod": "4.3.6",
    "zustand": "5.0.11"
  },
  "devDependencies": {
    "@electron-toolkit/tsconfig": "2.0.0",
    "@electron/notarize": "3.1.1",
    "@eslint/js": "9.39.5",
    "@modelcontextprotocol/sdk": "1.32.1",
    "@playwright/test": "1.60.0",
    "@tailwindcss/typography": "0.5.19",
    "@testing-library/jest-dom": "6.9.1",
    "@testing-library/react": "16.3.2",
    "@types/handlebars": "4.0.40",
    "@types/lodash": "4.17.23",
    "@types/node": "22.19.15",
    "@types/prismjs": "1.26.6",
    "@types/react": "19.2.18",
    "@types/react-dom": "19.2.4",
    "@types/react-resizable": "3.0.8",
    "@types/refractor": "3.4.1",
    "@types/socket.io": "3.0.1",
    "@types/tmp": "0.2.6",
    "@types/yaml-front-matter": "4.1.3",
    "@vitejs/plugin-react": "5.2.0",
    "@vitest/coverage-v8": "4.1.11",
    "@vitest/ui": "4.1.11",
    "adm-zip": "0.6.1",
    "autoprefixer": "10.4.24",
    "concurrently": "10.0.4",
    "dts-bundle-generator": "9.5.1",
    "electron": "44.7.0",
    "electron-builder": "26.15.7",
    "electron-vite": "5.0.0",
    "encoding": "0.1.13",
    "eslint": "9.39.5",
    "eslint-config-prettier": "9.1.2",
    "eslint-import-resolver-typescript": "3.10.1",
    "eslint-plugin-i18next": "6.1.5",
    "eslint-plugin-import": "2.32.0",
    "eslint-plugin-prettier": "5.5.5",
    "eslint-plugin-react": "7.37.5",
    "eslint-plugin-react-compiler": "19.1.0-rc.2",
    "eslint-plugin-react-hooks": "7.0.1",
    "globals": "15.15.0",
    "husky": "9.1.7",
    "jsdom": "27.4.0",
    "lint-staged": "15.5.2",
    "npm-run-all": "4.1.5",
    "patch-package": "8.0.1",
    "prettier": "3.8.1",
    "react": "19.2.8",
    "react-dom": "19.2.8",
    "sass-embedded": "1.97.3",
    "tailwind-scrollbar": "3.1.0",
    "tailwindcss": "3.4.19",
    "tar": "7.5.22",
    "typescript": "5.9.3",
    "typescript-eslint": "8.57.2",
    "vite": "7.3.6",
    "vite-tsconfig-paths": "5.1.4",
    "vitest": "4.1.11"
  },
  "overrides": {
    "@ai-sdk/provider": "4.0.21",
    "@ai-sdk/provider-utils": "5.0.53",
    "@huggingface/transformers": {
      "sharp": "npm:empty-npm-package@1.0.0"
    },
    "@tootallnate/once": "^3.0.1",
    "jsondiffpatch": "^0.7.3"
  },
  "workspaces": [
    "packages/common",
    "packages/extensions",
    "packages/mcp-server",
    "packages/tree-sitter-utils"
  ],
  "lint-staged": {
    "*.{ts,tsx}": "npm run lint"
  }
}

================
File: tailwind.config.js
================
/** @type {import('tailwindcss').Config} */
⋮----
// Theme-based colors
⋮----
const extractColorVars = (colorObj, colorGroup = '') =>





================================================================
End of Codebase
================================================================
