diff --git a/apps/code/src/renderer/components/MainLayout.tsx b/apps/code/src/renderer/components/MainLayout.tsx index b0867db06..dd54727be 100644 --- a/apps/code/src/renderer/components/MainLayout.tsx +++ b/apps/code/src/renderer/components/MainLayout.tsx @@ -45,6 +45,8 @@ import { GlobalEventHandlers } from "./GlobalEventHandlers"; const log = logger.scope("main-layout"); +const TOURS_ENABLED = false; + export function MainLayout() { const { view, @@ -150,7 +152,7 @@ export function MainLayout() { const settingsOpen = useSettingsDialogStore((s) => s.isOpen); useEffect(() => { - if (isFirstTaskTourDone || settingsOpen) return; + if (!TOURS_ENABLED || isFirstTaskTourDone || settingsOpen) return; const timer = setTimeout(() => startTour(createFirstTaskTour.id), 600); return () => clearTimeout(timer); }, [isFirstTaskTourDone, settingsOpen, startTour]); diff --git a/apps/code/src/renderer/features/task-detail/components/SuggestedTaskChips.tsx b/apps/code/src/renderer/features/task-detail/components/SuggestedTaskChips.tsx new file mode 100644 index 000000000..8e2920709 --- /dev/null +++ b/apps/code/src/renderer/features/task-detail/components/SuggestedTaskChips.tsx @@ -0,0 +1,73 @@ +import { useSetupStore } from "@features/setup/stores/setupStore"; +import type { DiscoveredTask } from "@features/setup/types"; +import { + CATEGORY_CONFIG, + FALLBACK_CATEGORY_CONFIG, +} from "@features/setup/utils/categoryConfig"; +import { Chip } from "@posthog/quill"; +import { Tooltip } from "@radix-ui/themes"; +import { useState } from "react"; + +const COLLAPSED_LIMIT = 4; + +interface SuggestedTaskChipsProps { + onSelect: (task: DiscoveredTask) => void; +} + +export function SuggestedTaskChips({ onSelect }: SuggestedTaskChipsProps) { + const discoveredTasks = useSetupStore((s) => s.discoveredTasks); + const [expanded, setExpanded] = useState(false); + + if (discoveredTasks.length === 0) return null; + + const visible = + expanded || discoveredTasks.length <= COLLAPSED_LIMIT + ? discoveredTasks + : discoveredTasks.slice(0, COLLAPSED_LIMIT); + const hiddenCount = discoveredTasks.length - visible.length; + + return ( +
+ {visible.map((task) => ( + + ))} + {hiddenCount > 0 && ( + setExpanded(true)} + className="cursor-pointer! whitespace-nowrap text-(--gray-11)" + > + +{hiddenCount} more + + )} +
+ ); +} + +function SuggestedTaskChip({ + task, + onSelect, +}: { + task: DiscoveredTask; + onSelect: (task: DiscoveredTask) => void; +}) { + const config = CATEGORY_CONFIG[task.category] ?? FALLBACK_CATEGORY_CONFIG; + const TaskIcon = config.icon; + + return ( + + onSelect(task)} + className="max-w-[220px] cursor-pointer! gap-1.5 whitespace-nowrap pl-2" + > + + {task.title} + + + ); +} diff --git a/apps/code/src/renderer/features/task-detail/components/TaskInput.tsx b/apps/code/src/renderer/features/task-detail/components/TaskInput.tsx index d0302697d..3ab1a5967 100644 --- a/apps/code/src/renderer/features/task-detail/components/TaskInput.tsx +++ b/apps/code/src/renderer/features/task-detail/components/TaskInput.tsx @@ -23,6 +23,8 @@ import { UnifiedModelSelector } from "@features/sessions/components/UnifiedModel import { getCurrentModeFromConfigOptions } from "@features/sessions/stores/sessionStore"; import type { AgentAdapter } from "@features/settings/stores/settingsStore"; import { useSettingsStore } from "@features/settings/stores/settingsStore"; +import { useSetupStore } from "@features/setup/stores/setupStore"; +import type { DiscoveredTask } from "@features/setup/types"; import { useAutoFocusOnTyping } from "@hooks/useAutoFocusOnTyping"; import { useConnectivity } from "@hooks/useConnectivity"; import { @@ -47,6 +49,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { usePreviewConfig } from "../hooks/usePreviewConfig"; import { useTaskCreation } from "../hooks/useTaskCreation"; import { CloudGithubMissingNotice } from "./CloudGithubMissingNotice"; +import { SuggestedTaskChips } from "./SuggestedTaskChips"; import { type WorkspaceMode, WorkspaceModeSelect } from "./WorkspaceModeSelect"; interface TaskInputProps { @@ -539,6 +542,15 @@ export function TaskInput({ editorRef.current?.setContent(text); editorRef.current?.focus(); }, []); + const handleSelectSuggestion = useCallback( + async (task: DiscoveredTask) => { + const ok = await handleSubmit({ + segments: [{ type: "text", text: task.prompt ?? task.title }], + }); + if (ok) useSetupStore.getState().removeDiscoveredTask(task.id); + }, + [handleSubmit], + ); const hasPendingDraft = useCallback( () => !(editorRef.current?.isEmpty() ?? true), [], @@ -825,6 +837,7 @@ export function TaskInput({ )} + diff --git a/apps/code/src/renderer/features/task-detail/hooks/useTaskCreation.ts b/apps/code/src/renderer/features/task-detail/hooks/useTaskCreation.ts index 6169c42ce..52cf459a8 100644 --- a/apps/code/src/renderer/features/task-detail/hooks/useTaskCreation.ts +++ b/apps/code/src/renderer/features/task-detail/hooks/useTaskCreation.ts @@ -4,6 +4,7 @@ import { useTaskInputHistoryStore } from "@features/message-editor/stores/taskIn import type { EditorHandle } from "@features/message-editor/types"; import { contentToXml, + type EditorContent, extractFilePaths, } from "@features/message-editor/utils/content"; import { useSettingsStore } from "@features/settings/stores/settingsStore"; @@ -48,7 +49,7 @@ interface UseTaskCreationOptions { interface UseTaskCreationReturn { isCreatingTask: boolean; canSubmit: boolean; - handleSubmit: () => void; + handleSubmit: (contentOverride?: EditorContent) => Promise; } function prepareTaskInput( @@ -204,96 +205,114 @@ export function useTaskCreation({ !isCreatingTask && !editorIsEmpty; - const handleSubmit = useCallback(async () => { - const editor = editorRef.current; - if (!canSubmit || !editor) return; + const handleSubmit = useCallback( + async (contentOverride?: EditorContent): Promise => { + const editor = editorRef.current; + const allowSubmit = contentOverride + ? isAuthenticated && + isOnline && + hasRequiredPath && + !isCreatingTask && + !!editor + : canSubmit && !!editor; + if (!allowSubmit || !editor) return false; - setIsCreatingTask(true); + setIsCreatingTask(true); - try { - const content = editor.getContent(); - - const plainText = editor.getText()?.trim(); - if (plainText) { - useTaskInputHistoryStore.getState().addPrompt(plainText); - } + try { + const content = contentOverride ?? editor.getContent(); - const input = prepareTaskInput(content, { - selectedDirectory, - selectedRepository, - githubIntegrationId, - githubUserIntegrationId, - workspaceMode, - branch, - executionMode, - adapter, - model, - reasoningLevel, - environmentId, - sandboxEnvironmentId, - signalReportId, - }); + if (!contentOverride) { + const plainText = editor.getText()?.trim(); + if (plainText) { + useTaskInputHistoryStore.getState().addPrompt(plainText); + } + } - if (executionMode) { - useSettingsStore.getState().setLastUsedInitialTaskMode(executionMode); - } + const input = prepareTaskInput(content, { + selectedDirectory, + selectedRepository, + githubIntegrationId, + githubUserIntegrationId, + workspaceMode, + branch, + executionMode, + adapter, + model, + reasoningLevel, + environmentId, + sandboxEnvironmentId, + signalReportId, + }); - const taskService = get(RENDERER_TOKENS.TaskService); - const result = await taskService.createTask(input, (output) => { - invalidateTasks(output.task); - if (signalReportId) { - clearTaskInputReportAssociation(); - } - if (onTaskCreated) { - onTaskCreated(output.task); - } else { - navigateToTask(output.task); + if (executionMode) { + useSettingsStore.getState().setLastUsedInitialTaskMode(executionMode); } - useTourStore.getState().completeTour(createFirstTaskTour.id); - editor.clear(); - }); - - if (result.success) { - void trackTaskCreated(input, selectedDirectory); - } - if (!result.success) { - const title = getErrorTitle(result.failedStep); - toast.error(title, { description: result.error }); - log.error("Task creation failed", { - failedStep: result.failedStep, - error: result.error, + const taskService = get(RENDERER_TOKENS.TaskService); + const result = await taskService.createTask(input, (output) => { + invalidateTasks(output.task); + if (signalReportId) { + clearTaskInputReportAssociation(); + } + if (onTaskCreated) { + onTaskCreated(output.task); + } else { + navigateToTask(output.task); + } + useTourStore.getState().completeTour(createFirstTaskTour.id); + editor.clear(); }); + + if (result.success) { + void trackTaskCreated(input, selectedDirectory); + } + + if (!result.success) { + const title = getErrorTitle(result.failedStep); + toast.error(title, { description: result.error }); + log.error("Task creation failed", { + failedStep: result.failedStep, + error: result.error, + }); + } + return result.success; + } catch (error) { + const description = + error instanceof Error ? error.message : "Unknown error"; + toast.error("Failed to create task", { description }); + log.error("Unexpected error during task creation", { error }); + return false; + } finally { + setIsCreatingTask(false); } - } catch (error) { - const description = - error instanceof Error ? error.message : "Unknown error"; - toast.error("Failed to create task", { description }); - log.error("Unexpected error during task creation", { error }); - } finally { - setIsCreatingTask(false); - } - }, [ - canSubmit, - editorRef, - selectedDirectory, - selectedRepository, - githubIntegrationId, - githubUserIntegrationId, - workspaceMode, - branch, - executionMode, - adapter, - model, - reasoningLevel, - environmentId, - sandboxEnvironmentId, - signalReportId, - clearTaskInputReportAssociation, - invalidateTasks, - navigateToTask, - onTaskCreated, - ]); + }, + [ + canSubmit, + editorRef, + isAuthenticated, + isOnline, + hasRequiredPath, + isCreatingTask, + selectedDirectory, + selectedRepository, + githubIntegrationId, + githubUserIntegrationId, + workspaceMode, + branch, + executionMode, + adapter, + model, + reasoningLevel, + environmentId, + sandboxEnvironmentId, + signalReportId, + clearTaskInputReportAssociation, + invalidateTasks, + navigateToTask, + onTaskCreated, + ], + ); return { isCreatingTask,