diff --git a/apps/app/src/app/index.css b/apps/app/src/app/index.css index ec7b080b0..a014b1285 100644 --- a/apps/app/src/app/index.css +++ b/apps/app/src/app/index.css @@ -185,13 +185,12 @@ --dls-text-primary: var(--slate-12); --dls-text-secondary: var(--slate-11); --dls-hover: var(--slate-3); - --dls-active: var(--slate-5); + --dls-active: var(--slate-4); --dls-radius: 16px; --dls-radius-lg: 24px; --dls-shell-shadow: 0 10px 30px rgba(15, 23, 42, 0.06); --dls-card-shadow: 0 8px 24px rgba(15, 23, 42, 0.05); --project-dialog-accent: #1fbac0; - --project-dialog-accent-strong: #087b82; --project-dialog-notice: #f4f4ee; --project-dialog-option-border: #e7e7e8; --background: var(--slate-1); @@ -247,7 +246,7 @@ --dls-text-primary: var(--slate-12); --dls-text-secondary: var(--slate-11); --dls-hover: var(--slate-3); - --dls-active: var(--slate-5); + --dls-active: var(--slate-4); --dls-shell-shadow: 0 18px 48px rgba(0, 0, 0, 0.32); --dls-card-shadow: 0 14px 36px rgba(0, 0, 0, 0.24); } @@ -577,7 +576,6 @@ select:disabled { .dark, [data-theme="dark"] { --project-dialog-accent: #1fbac0; - --project-dialog-accent-strong: #a9e7ea; --project-dialog-notice: var(--slate-3); --project-dialog-option-border: var(--slate-6); --background: var(--slate-1); diff --git a/apps/app/src/components/chat/new-conversation-starter.tsx b/apps/app/src/components/chat/new-conversation-starter.tsx index 84222ebc8..2148e33a2 100644 --- a/apps/app/src/components/chat/new-conversation-starter.tsx +++ b/apps/app/src/components/chat/new-conversation-starter.tsx @@ -1,7 +1,9 @@ "use client"; -import { type ChangeEvent, useEffect, useMemo, useRef, useState } from "react"; -import type { CSSProperties } from "react"; +import { type ChangeEvent, useEffect, useId, useMemo, useRef, useState } from "react"; +import type { CSSProperties, Ref } from "react"; +import { createPortal } from "react-dom"; +import { LazyMotion, domAnimation, m, useReducedMotion, type Transition } from "motion/react"; import { CheckIcon, ChevronDownIcon, @@ -110,12 +112,19 @@ const ANIMATION_CATEGORY_LABELS: Record = { vfx: { en: "VFX", zh: "视觉特效" }, }; -const MODES = [ +const MODES = [ { id: "work", iconSrc: publicAssetUrl("new-conversation-tabs/work.svg"), label: "new_conversation.mode.work" }, { id: "code", iconSrc: publicAssetUrl("new-conversation-tabs/code.svg"), label: "new_conversation.mode.code" }, { id: "design", iconSrc: publicAssetUrl("new-conversation-tabs/design.svg"), label: "new_conversation.mode.design" }, { id: "video", iconSrc: publicAssetUrl("new-conversation-tabs/video.svg"), label: "new_conversation.mode.video" }, -] as const satisfies ReadonlyArray<{ id: NewConversationMode; iconSrc: string; label: string }>; +] as const satisfies ReadonlyArray<{ id: NewConversationMode; iconSrc: string; label: string }>; + +const MODE_TAB_SPRING = { + type: "spring", + mass: 1, + stiffness: 300, + damping: 20, +} satisfies Transition; type StarterAction = { id: string; @@ -265,7 +274,7 @@ function TemplateStrip({ }; return ( -
+

{t("new_conversation.templates.title", { category: categoryLabel })}

@@ -867,9 +876,10 @@ function ShortcutEditor({ definitions, selectedIds, templates, - templatesLoading, - position, - onToggle, + templatesLoading, + position, + panelRef, + onToggle, onMove, onClose, }: { @@ -877,16 +887,20 @@ function ShortcutEditor({ definitions: ReadonlyArray; selectedIds: string[]; templates: TemplateCatalogItem[]; - templatesLoading: boolean; - position: CSSProperties; + templatesLoading: boolean; + position: CSSProperties; + panelRef: Ref; onToggle: (id: string) => void; onMove: (id: string, direction: -1 | 1) => void; onClose: () => void; -}) { - const creativeMode = mode === "design"; - return ( -
); })} -
-
- ); -} +
+ , + document.body, + ); +} -export function newConversationPlaceholder(mode: NewConversationMode) { - return t(`new_conversation.placeholder.${mode}`); +export function newConversationPlaceholder() { + return t("new_conversation.placeholder"); } export function NewConversationStarter({ @@ -995,13 +1010,16 @@ export function NewConversationStarter({ onChangeAnimationParams, onRetryAnimationCatalog, }: NewConversationStarterProps) { - const [activeTemplateCategory, setActiveTemplateCategory] = useState(null); + const modeTabIndicatorId = useId(); + const reduceMotion = useReducedMotion(); + const [activeTemplateCategory, setActiveTemplateCategory] = useState(null); const [hoveredMode, setHoveredMode] = useState(null); const [shortcutEditorOpen, setShortcutEditorOpen] = useState(false); const [shortcutEditorPosition, setShortcutEditorPosition] = useState({}); const [shortcutIds, setShortcutIds] = useState>(DEFAULT_SHORTCUT_IDS); const [savedPromptTemplates, setSavedPromptTemplates] = useState([]); const shortcutEditorRef = useRef(null); + const shortcutEditorPanelRef = useRef(null); const shortcutButtonRef = useRef(null); useEffect(() => { @@ -1042,9 +1060,13 @@ export function NewConversationStarter({ }, [shortcutIds]); useEffect(() => { - if (!shortcutEditorOpen) return; - const closeOnOutsidePress = (event: PointerEvent) => { - if (!shortcutEditorRef.current?.contains(event.target as Node)) setShortcutEditorOpen(false); + if (!shortcutEditorOpen) return; + const closeOnOutsidePress = (event: PointerEvent) => { + if (!(event.target instanceof Node)) return; + if ( + !shortcutEditorRef.current?.contains(event.target) + && !shortcutEditorPanelRef.current?.contains(event.target) + ) setShortcutEditorOpen(false); }; const closeOnEscape = (event: KeyboardEvent) => { if (event.key === "Escape") setShortcutEditorOpen(false); @@ -1058,15 +1080,28 @@ export function NewConversationStarter({ }, [shortcutEditorOpen]); const updateShortcutEditorPosition = () => { - const button = shortcutButtonRef.current; - if (!button || typeof window === "undefined") return; - const rect = button.getBoundingClientRect(); - const right = Math.max(16, window.innerWidth - rect.right); - const opensAbove = rect.bottom > window.innerHeight * 0.58; - setShortcutEditorPosition(opensAbove - ? { right, bottom: Math.max(16, window.innerHeight - rect.top + 8) } - : { right, top: Math.max(16, rect.bottom + 8) }); - }; + const button = shortcutButtonRef.current; + if (!button || typeof window === "undefined") return; + const rect = button.getBoundingClientRect(); + const contentRect = button.closest('[data-slot="sidebar-inset"]')?.getBoundingClientRect(); + const contentLeft = contentRect?.left ?? 0; + const contentRight = contentRect?.right ?? window.innerWidth; + const horizontalMargin = 16; + const width = Math.min(340, Math.max(0, contentRight - contentLeft - horizontalMargin * 2)); + const left = Math.min( + Math.max(rect.right - width, contentLeft + horizontalMargin), + contentRight - horizontalMargin - width, + ); + const verticalMargin = 16; + const gap = 8; + const availableAbove = Math.max(0, rect.top - gap - verticalMargin); + const availableBelow = Math.max(0, window.innerHeight - rect.bottom - gap - verticalMargin); + const opensAbove = availableAbove > availableBelow; + const maxHeight = Math.min(420, opensAbove ? availableAbove : availableBelow); + setShortcutEditorPosition(opensAbove + ? { left, width, maxHeight, bottom: window.innerHeight - rect.top + gap } + : { left, width, maxHeight, top: rect.bottom + gap }); + }; useEffect(() => { if (!shortcutEditorOpen) return; @@ -1132,17 +1167,17 @@ export function NewConversationStarter({ onRequestTemplates?.(); } onSelectMode(mode); - }; - - return ( -
+ }; + + return ( +
-
+
{t("new_conversation.title")} -

{t("new_conversation.subtitle")}

-
- -
- {MODES.map(({ id, iconSrc, label }) => { - const selected = id === selectedMode; - return ( - - ); - })} -
- -
+ onClick={() => selectMode(id)} + onMouseEnter={() => setHoveredMode(id)} + onMouseLeave={() => setHoveredMode(null)} + > + {selected ? ( +
+ +
+ +
+
{actions.map(({ id, label, prompt, templateCategory, icon: ActionIcon }) => { const selectedTemplateAction = templateCategory !== undefined && templateCategory === activeTemplateCategory; const selectedCapabilityAction = !templateCategory && selectedCapabilityId === id; @@ -1246,24 +1294,11 @@ export function NewConversationStarter({ updateShortcutEditorPosition(); setShortcutEditorOpen((open) => !open); }} - > - - - {shortcutEditorOpen ? ( - setShortcutEditorOpen(false)} - /> - ) : null} -
- ) : null} + > + + +
+ ) : null}
{selectedMode === "design" && activeTemplateCategory ? ( @@ -1321,8 +1356,22 @@ export function NewConversationStarter({ onRequestTemplates={onRequestTemplates} /> ) : null} -
-
- ); -} + + {shortcutEditorOpen ? ( + setShortcutEditorOpen(false)} + /> + ) : null} + + ); +} diff --git a/apps/app/src/i18n/locales/en.ts b/apps/app/src/i18n/locales/en.ts index 9755d218e..e4e834098 100644 --- a/apps/app/src/i18n/locales/en.ts +++ b/apps/app/src/i18n/locales/en.ts @@ -275,10 +275,7 @@ export default { "new_conversation.prompt.visual_direction": "Help me turn this idea into a clear visual direction and practical design brief.", "new_conversation.prompt.video_script": "Help me turn this idea into a video. Start with the audience, objective, platform, and desired duration.", "new_conversation.prompt.storyboard": "Help me make a storyboard. Start by asking for the core message, visual style, and available material.", - "new_conversation.placeholder.work": "What would you like to move forward today?", - "new_conversation.placeholder.code": "What would you like to build or solve?", - "new_conversation.placeholder.design": "What idea would you like to bring to life?", - "new_conversation.placeholder.video": "Start with one idea for your video…", + "new_conversation.placeholder": "Choose a direction, or describe the work in your own words.", "new_conversation.animations.title": "Choose animation references", "new_conversation.animations.subtitle": "Select one or more. AI will adapt them to your video idea.", "new_conversation.animations.gsap_library": "GSAP animation and effect library", @@ -1015,7 +1012,8 @@ export default { "composer.plus_attach_files": "Attach files", "composer.plus_menu_label": "Add to this task", "composer.plus_tools": "Tools", - "composer.remote_worker_paste_warning": "This is a remote worker. Sandboxes are remote too. To share files with it, upload them to the Shared folder in the sidebar.", + "composer.remote_worker_paste_warning": "This is a remote worker. Sandboxes are remote too. To share files with it, upload them to the Shared folder in the sidebar.", + "composer.empty_submit_hint": "Can't find a message to send. Try again.", "composer.run_task": "Run task", "composer.work_mode_execute": "Execute", "composer.work_mode_dsh_standard": "Standard", @@ -1744,7 +1742,7 @@ export default { "session.video_delivery_failed": "Video delivery is still incomplete after the automatic repair.", "session.revert_failed": "Could not revert the conversation. Try again once the current run finishes.", "session.branch_failed": "Could not branch this conversation. Try again.", - "session.default_title": "New session", + "session.default_title": "New task", "session.delete": "Delete", "session.delete_named_session_message": "This will permanently delete \"{title}\" and its messages.", "session.delete_session_generic": "This will permanently delete the selected session and its messages.", @@ -1788,7 +1786,7 @@ export default { "session.side_panel.code": "Code", "session.side_panel.files": "Files", "session.side_panel.video": "Video", - "session.new_task": "New conversation", + "session.new_task": "New task", "session.open": "Open", "session.palette_hint_navigate": "Arrow keys to navigate", "session.palette_hint_run": "Enter to run · Esc to close", @@ -2648,18 +2646,20 @@ export default { "projects.ungrouped": "Ungrouped", "projects.new_ungrouped_conversation": "New ungrouped conversation", "projects.create": "New project", - "projects.new_conversation": "New conversation in {project}", - "projects.create_description": "Use a dedicated folder to keep project conversations, files, and settings isolated.", + "projects.new_conversation": "New task in {project}", + "projects.create_description": "Use a dedicated folder to keep project tasks, files, and settings isolated.", "projects.name": "Project name", "projects.name_example": "For example: iPolloWork", "projects.source_folder": "Source folder", - "projects.engine": "Conversation engine", - "projects.default_engine": "Default conversation engine", + "projects.engine": "Project engine", + "projects.choose_engine": "Choose project engine", + "projects.default_engine": "Project engine", "projects.engine_opencode": "OpenCode", "projects.engine_opencode_description": "Default, stable workspace engine", "projects.engine_dsh": "DeepSeek Harness", "projects.engine_dsh_description": "Plugin-first agent environment", - "projects.engine_locked_notice": "The conversation engine is locked to the project and cannot be changed after creation. Confirm before continuing.", + "projects.engine_running": "Engine running", + "projects.engine_locked_notice": "The engine is locked to the project after creation and cannot be changed. Confirm before continuing.", "projects.choose_folder": "Choose project folder", "projects.name_placeholder": "Project name", "projects.rename": "Rename project", diff --git a/apps/app/src/i18n/locales/zh.ts b/apps/app/src/i18n/locales/zh.ts index 360d23963..45cce34b5 100644 --- a/apps/app/src/i18n/locales/zh.ts +++ b/apps/app/src/i18n/locales/zh.ts @@ -230,10 +230,10 @@ export default { "blueprint.welcome_message": "你好,欢迎使用iPolloWork!\n\n大家用iPolloWork在电脑上编写CSV文件、自动化浏览器任务,以及将联系人同步到Notion。\n\n但唯一的限制是你的想象力。\n\n你想做什么?", "blueprint.welcome_title": "欢迎使用iPolloWork", "new_conversation.mode_label": "选择对话方向", - "new_conversation.mode.work": "日常办公", - "new_conversation.mode.code": "代码开发", - "new_conversation.mode.design": "设计创意", - "new_conversation.mode.video": "视频制作", + "new_conversation.mode.work": "工作", + "new_conversation.mode.code": "代码", + "new_conversation.mode.design": "创作", + "new_conversation.mode.video": "视频", "new_conversation.title": "把想法变成成果", "new_conversation.subtitle": "选择一个方向、从常用起点开始,或直接描述你要推进的工作。", "new_conversation.quick_actions_label": "常用起点", @@ -277,10 +277,7 @@ export default { "new_conversation.prompt.visual_direction": "帮我把这个想法梳理成清晰的视觉方向和可执行的设计简报。", "new_conversation.prompt.video_script": "帮我把这个想法做成视频。先确认受众、目标、平台和时长。", "new_conversation.prompt.storyboard": "帮我规划分镜。先问我核心信息、视觉风格和已有素材。", - "new_conversation.placeholder.work": "今天想推进什么?", - "new_conversation.placeholder.code": "想构建什么,或解决什么问题?", - "new_conversation.placeholder.design": "想把什么想法变成作品?", - "new_conversation.placeholder.video": "从一句视频想法开始…", + "new_conversation.placeholder": "选择一个方向,或直接描述你要推进的工作。", "new_conversation.animations.title": "选择动画参考", "new_conversation.animations.subtitle": "可以多选,AI 会结合你的视频内容进行适配。", "new_conversation.animations.gsap_library": "GSAP 动画特效库", @@ -1012,7 +1009,8 @@ export default { "composer.plus_attach_files": "附加文件", "composer.plus_menu_label": "添加到任务", "composer.plus_tools": "工具", - "composer.remote_worker_paste_warning": "这是远程工作区。沙箱也是远程的。要共享文件,请上传到侧边栏的共享文件夹。", + "composer.remote_worker_paste_warning": "这是远程工作区。沙箱也是远程的。要共享文件,请上传到侧边栏的共享文件夹。", + "composer.empty_submit_hint": "找不到要发送的消息。请重试。", "composer.run_task": "运行任务", "composer.work_mode_execute": "执行", "composer.work_mode_dsh_standard": "标准模式", @@ -1466,7 +1464,7 @@ export default { "session.cmd_sessions_title": "搜索会话", "session.cmd_switch": "切换", "session.default_agent": "默认智能体", - "session.default_title": "新建会话", + "session.default_title": "新任务", "session.delete": "删除", "session.delete_named_session_message": "这将永久删除「{title}」及其消息。", "session.delete_session_generic": "这将永久删除所选会话及其消息。", @@ -1504,7 +1502,7 @@ export default { "session.outputs.expand_bundle": "展开包含文件", "session.outputs.collapse_bundle": "收起包含文件", "session.right_panel_add": "添加面板", - "session.new_task": "新建对话", + "session.new_task": "新建任务", "session.open": "打开", "session.palette_hint_navigate": "方向键导航", "session.palette_hint_run": "回车执行 · Esc关闭", @@ -2491,18 +2489,20 @@ export default { "projects.ungrouped": "未分组", "projects.new_ungrouped_conversation": "新建未分组对话", "projects.create": "新建项目", - "projects.new_conversation": "在“{project}”中新建对话", - "projects.create_description": "使用独立文件夹,隔离项目中的对话、文件和配置。", + "projects.new_conversation": "在“{project}”中新建任务", + "projects.create_description": "使用独立文件夹,隔离项目中的任务、文件和配置。", "projects.name": "项目名称", "projects.name_example": "例如:iPolloWork", "projects.source_folder": "源文件夹", - "projects.engine": "对话引擎", - "projects.default_engine": "默认对话引擎", + "projects.engine": "项目引擎", + "projects.choose_engine": "选择项目引擎", + "projects.default_engine": "项目引擎", "projects.engine_opencode": "OpenCode", "projects.engine_opencode_description": "默认、稳定的工作引擎", "projects.engine_dsh": "DeepSeek Harness", "projects.engine_dsh_description": "插件优先的智能体运行环境", - "projects.engine_locked_notice": "对话引擎会随项目锁定,创建后无法在项目内更改,请确认后再继续。", + "projects.engine_running": "引擎运行中", + "projects.engine_locked_notice": "项目创建后将锁定此引擎,无法更改,请确认后继续。", "projects.choose_folder": "选择项目文件夹", "projects.name_placeholder": "项目名称", "projects.rename": "重命名项目", diff --git a/apps/app/src/react-app/domains/session/chat/assets/project-engine-deepseek.png b/apps/app/src/react-app/domains/session/chat/assets/project-engine-deepseek.png new file mode 100644 index 000000000..f1b485bf0 Binary files /dev/null and b/apps/app/src/react-app/domains/session/chat/assets/project-engine-deepseek.png differ diff --git a/apps/app/src/react-app/domains/session/chat/assets/project-engine-opencode.svg b/apps/app/src/react-app/domains/session/chat/assets/project-engine-opencode.svg new file mode 100644 index 000000000..c5ec30734 --- /dev/null +++ b/apps/app/src/react-app/domains/session/chat/assets/project-engine-opencode.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/app/src/react-app/domains/session/chat/assets/project-engine-selected.svg b/apps/app/src/react-app/domains/session/chat/assets/project-engine-selected.svg index b0ef2d376..6dec3aeba 100644 --- a/apps/app/src/react-app/domains/session/chat/assets/project-engine-selected.svg +++ b/apps/app/src/react-app/domains/session/chat/assets/project-engine-selected.svg @@ -1,3 +1,3 @@ - + diff --git a/apps/app/src/react-app/domains/session/chat/session-page.tsx b/apps/app/src/react-app/domains/session/chat/session-page.tsx index 7196cc11a..5a2bdaa4e 100644 --- a/apps/app/src/react-app/domains/session/chat/session-page.tsx +++ b/apps/app/src/react-app/domains/session/chat/session-page.tsx @@ -29,7 +29,8 @@ import { import { getDisplaySessionTitle } from "../../../../app/lib/session-title"; import type { BootPhase } from "../../../../app/lib/startup-boot"; import { openDesktopPath, pickDirectory, revealDesktopItemInDir, saveFile, type WorkspaceInfo } from "../../../../app/lib/desktop"; -import type { +import type { + ComposerAttachment, ComposerDraft, ProviderListItem, TodoItem, @@ -47,7 +48,6 @@ import { getArtifactsFromMessages, } from "@/lib/artifacts"; import { Button } from "@/components/ui/button"; -import { Badge } from "@/components/ui/badge"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { @@ -70,7 +70,14 @@ import { RenameSessionModal } from "../modals/rename-session-modal"; import { AppSidebar } from "../sidebar/app-sidebar"; import type { iPolloWorkSessionType, iPolloWorkTemplateId } from "../sidebar/app-sidebar-provider"; import { readSessionType, sessionTypeForTemplate, setSessionType } from "../sidebar/session-type"; -import { SessionSurface, type SessionSurfaceProps } from "../surface/session-surface"; +import { SessionSurface, type SessionSurfaceProps } from "../surface/session-surface"; +import { ReactSessionComposer } from "../surface/composer/composer"; +import { + NewConversationStarter, + newConversationPlaceholder, + type NewConversationMode, + type StarterCapability, +} from "@/components/chat/new-conversation-starter"; import { replaceDesignSelectionToken } from "../surface/composer/composer-draft"; import { getComposerDraft, useComposerStateStore } from "../surface/composer-state-store"; import { @@ -138,6 +145,8 @@ import { cn } from "@/lib/utils"; import { useActiveEnterpriseConnection } from "@/react-app/domains/enterprise/use-active-enterprise-connection"; import { useInstalledPluginContributions } from "@/react-app/plugin-ui/plugin-ui-contributions"; import type { WorkspaceAppModelContext } from "@/react-app/plugin-ui/workspace-app-frame"; +import projectEngineDeepSeekIcon from "./assets/project-engine-deepseek.png"; +import projectEngineOpenCodeIcon from "./assets/project-engine-opencode.svg"; import projectEngineSelectedIcon from "./assets/project-engine-selected.svg"; import projectEngineUnselectedIcon from "./assets/project-engine-unselected.svg"; @@ -167,29 +176,149 @@ type TemplateSessionData = { hasBrief: boolean; }; -function SessionEngineBadge({ engineId }: { engineId?: string | null }) { +function ProjectEngineBadge({ + engineId, + testId, +}: { + engineId?: string | null; + testId?: string; +}) { const isDeepSeekHarness = engineId?.trim() === DEEPSEEK_HARNESS_ENGINE_ID; const label = t(isDeepSeekHarness ? "projects.engine_dsh" : "projects.engine_opencode"); - const Icon = isDeepSeekHarness ? Zap : Code2; return ( - + + + {[ + { + id: DEFAULT_ENGINE_ID, + name: t("projects.engine_opencode"), + description: t("projects.engine_opencode_description"), + icon: projectEngineOpenCodeIcon, + iconClassName: "h-6 w-[19px] dark:invert", + }, + { + id: DEEPSEEK_HARNESS_ENGINE_ID, + name: t("projects.engine_dsh"), + description: t("projects.engine_dsh_description"), + icon: projectEngineDeepSeekIcon, + iconClassName: "h-6 w-[33px]", + }, + ].map((engine) => { + const selected = value === engine.id; + return ( + + ); + })} + ); } - + export type SessionPageHistoryControls = { canUndo: boolean; canRedo: boolean; @@ -215,7 +344,8 @@ export type SessionPageSidebarProps = { name: string; folderPath: string; engineId: BuiltInWorkspaceEngineId; - }) => Promise | void; + }) => Promise | string | null | void; + onCreateInitialProjectTask: (draft: ComposerDraft) => Promise; onRenameProject: (workspaceId: string, name: string) => Promise | void; onRevealProject: (workspaceId: string) => Promise | void; onDeleteProject: (workspaceId: string) => Promise | void; @@ -243,7 +373,7 @@ export type SessionPageSurfaceProps = Omit< "client" | "workspaceId" | "sessionId" | "opencodeBaseUrl" | "ipolloworkToken" >; -export type SessionPageProps = { +export type SessionPageProps = { selectedSessionId: string | null; selectedWorkspaceId: string; selectedWorkspaceDisplay: { @@ -300,8 +430,177 @@ export type SessionPageProps = { /** Settings content rendered inside the right pane when the settings rail icon is active. */ settingsSlot?: React.ReactNode; terminalOpen?: boolean; - onTerminalOpenChange?: (open: boolean) => void; -}; + onTerminalOpenChange?: (open: boolean) => void; +}; + +function InitialProjectTaskStarter({ + surface, + onSubmit, +}: { + surface: SessionPageSurfaceProps; + onSubmit: (draft: ComposerDraft) => Promise; +}) { + const [draft, setDraft] = useState(""); + const [attachments, setAttachments] = useState([]); + const attachmentsRef = useRef([]); + const [starterMode, setStarterMode] = useState("work"); + const [starterCapability, setStarterCapability] = useState(null); + const [sending, setSending] = useState(false); + + useEffect(() => { + attachmentsRef.current = attachments; + }, [attachments]); + + useEffect(() => () => { + attachmentsRef.current.forEach((attachment) => { + if (attachment.previewUrl) URL.revokeObjectURL(attachment.previewUrl); + }); + }, []); + + const clearSubmittedDraft = (submittedAttachments: ComposerAttachment[]) => { + submittedAttachments.forEach((attachment) => { + if (attachment.previewUrl) URL.revokeObjectURL(attachment.previewUrl); + }); + setDraft(""); + setAttachments([]); + setStarterCapability(null); + }; + + const submitDraft = async (composerDraft: ComposerDraft) => { + setSending(true); + try { + const created = await onSubmit(composerDraft); + if (!created) return false; + clearSubmittedDraft(composerDraft.attachments); + return true; + } finally { + setSending(false); + } + }; + + const submit = async () => { + const text = draft.trim(); + if (!text && attachments.length === 0 && !starterCapability) return; + const parts: ComposerDraft["parts"] = text ? [{ type: "text", text }] : []; + const composerDraft: ComposerDraft = { + mode: "prompt", + parts, + attachments, + text, + resolvedText: text, + capability: starterCapability + ? { id: starterCapability.id, instruction: starterCapability.instruction } + : undefined, + }; + await submitDraft(composerDraft); + }; + + const attachFiles = (files: File[]) => { + const next = files + .filter((file) => file.size <= 25 * 1024 * 1024) + .map((file): ComposerAttachment => { + const image = file.type.startsWith("image/"); + return { + id: `${file.name}-${file.lastModified}-${Math.random().toString(36).slice(2)}`, + name: file.name, + mimeType: file.type || "application/octet-stream", + size: file.size, + kind: image ? "image" : "file", + file, + previewUrl: image ? URL.createObjectURL(file) : undefined, + }; + }); + setAttachments((current) => [...current, ...next]); + }; + + const removeAttachment = (id: string) => { + setAttachments((current) => { + const target = current.find((attachment) => attachment.id === id); + if (target?.previewUrl) URL.revokeObjectURL(target.previewUrl); + return current.filter((attachment) => attachment.id !== id); + }); + }; + + return ( +
+
+
+ { + setStarterMode(mode); + setStarterCapability(null); + }} + onSelectPrompt={(prompt, capability) => { + setStarterCapability(capability ?? null); + if (prompt) setDraft(prompt); + window.dispatchEvent(new Event("ipollowork:focusPrompt")); + }} + /> +
+
+ {}} + busy={sending} + queuedCount={0} + disabled={sending || Boolean(surface.modelUnavailable)} + modelUnavailable={Boolean(surface.modelUnavailable)} + statusLabel="" + modelPickerOpen={surface.modelPickerOpen} + selectedModel={surface.selectedModel} + onModelPickerOpenChange={surface.onModelPickerOpenChange} + onModelChange={surface.onModelChange} + onConfigureModels={surface.onConfigureModels} + onConfigureTokenStar={surface.onConfigureTokenStar} + attachments={attachments} + hasPromptContext={Boolean(starterCapability)} + onAttachFiles={attachFiles} + onRemoveAttachment={removeAttachment} + modelVariantLabel={surface.modelVariantLabel} + modelVariant={surface.modelVariant} + modelBehaviorOptions={surface.modelBehaviorOptions} + onModelVariantChange={surface.onModelVariantChange} + selectedMode={surface.selectedMode} + listModes={surface.listModes} + onSelectMode={surface.onSelectMode} + listAgents={surface.listAgents} + onSelectAgent={surface.onSelectAgent} + listCommands={surface.listCommands} + listExternalAgents={() => Promise.resolve([])} + onOpenSettingsSection={surface.onOpenSettingsSection} + recentFiles={[]} + searchFiles={() => Promise.resolve([])} + onInsertMention={(_kind, value) => setDraft((current) => `${current}@${value} `)} + onPasteText={(text) => setDraft((current) => `${current}${text}`)} + onUnsupportedFileLinks={(links) => setDraft((current) => `${current}${links.join("\n")}`)} + pastedText={[]} + onExpandPastedText={() => {}} + onRemovePastedText={() => {}} + isRemoteWorkspace={false} + isSandboxWorkspace={false} + onUploadInboxFiles={null} + draftScopeKey="initial-project-task" + layout="inline" + inlineAppearance="engine-selected" + placeholder={newConversationPlaceholder()} + endAccessory={( + + )} + /> +
+
+
+ ); +} function getSidebarInitialLoading(props: SessionPageSidebarProps) { if (props.projectSessionLists.some((project) => project.sessions.length > 0)) { @@ -653,6 +952,9 @@ export function SessionPage(props: SessionPageProps) { () => sessionTitleForId(props.sidebar.projectSessionLists, props.selectedSessionId), [props.selectedSessionId, props.sidebar.projectSessionLists], ); + const selectedProjectName = props.selectedWorkspaceDisplay.displayName?.trim() + || props.selectedWorkspaceDisplay.name?.trim() + || t("workspace_list.workspace_fallback"); const [templateSessionRevision, setTemplateSessionRevision] = useState(0); const [templateCatalog, setTemplateCatalog] = useState([]); const [templateResourceScope, setTemplateResourceScope] = useState(() => readActiveWorkContextId()); @@ -2368,7 +2670,9 @@ export function SessionPage(props: SessionPageProps) { () => sessionTitleForId(props.sidebar.projectSessionLists, sessionActionId), [props.sidebar.projectSessionLists, sessionActionId], ); - const showWorkspaceSetupEmptyState = props.workspaces.length === 0 && !props.selectedSessionId; + const hasNamedProject = props.sidebar.projectSessionLists.some((project) => !project.workspace.isDefault); + const showInitialProjectTaskStarter = !hasNamedProject && !props.selectedSessionId && Boolean(props.surface); + const showWorkspaceSetupEmptyState = props.workspaces.length === 0 && !props.selectedSessionId; const showNewConversationChrome = !props.selectedSessionId && !showWorkspaceSetupEmptyState; const showStartupSkeleton = !props.selectedSessionId && @@ -2434,11 +2738,10 @@ export function SessionPage(props: SessionPageProps) { const showHeaderMenu = Boolean( props.selectedSessionId || props.developerMode, ); - const selectedSessionIsDefaultTitle = selectedSessionTitle === t("session.default_title"); - const showMainHeaderTitle = Boolean( - !rightWorkspaceExpanded && - (showWorkspaceSetupEmptyState || (props.selectedSessionId && !selectedSessionIsDefaultTitle)), - ); + const showMainHeaderTitle = Boolean( + !rightWorkspaceExpanded && + (showWorkspaceSetupEmptyState || props.selectedSessionId), + ); const showMainHeaderMenu = showHeaderMenu && showMainHeaderTitle; const mainHeaderHidden = mainWorkspaceView === "extensions" || (showNewConversationChrome && !sidebarVisuallyCollapsed); const visibleWorkspaceWidth = viewportWidth - (shellConfig.sidebar && sidebarOpen ? effectiveLeftSidebarWidth : 0); @@ -2682,13 +2985,16 @@ export function SessionPage(props: SessionPageProps) { ) : null}
- {showMainHeaderTitle ? ( -

- {showWorkspaceSetupEmptyState - ? t("workspace.empty_state_body") - : selectedSessionTitle || t("session.default_title")} -

- ) : null} + {showMainHeaderTitle ? ( + <> + {props.selectedSessionId ? : null} +

+ {showWorkspaceSetupEmptyState + ? t("workspace.empty_state_body") + : selectedSessionTitle || t("session.default_title")} +

+ + ) : null} {showMainHeaderMenu ? ( - {props.selectedSessionId ? ( -
- -
- ) : null} - -
+
) : null} - {mainWorkspaceView === null && showDelayedSessionLoadingState ? ( + {mainWorkspaceView === null && showInitialProjectTaskStarter && props.surface ? ( + + ) : null} + + {mainWorkspaceView === null && !showInitialProjectTaskStarter && showDelayedSessionLoadingState ? (
) : null} - {mainWorkspaceView === null && !showDelayedSessionLoadingState && canRenderReactSurface ? ( + {mainWorkspaceView === null && !showInitialProjectTaskStarter && !showDelayedSessionLoadingState && canRenderReactSurface ? (
{isDesignSession && designWorkspaceEnabled && templateSessionLoading ? ( @@ -2910,6 +3217,12 @@ export function SessionPage(props: SessionPageProps) { templateEntryPath={templateEntryPathForArtifacts} artifactFiles={artifactFiles} artifactContext={artifactContext} + composerEndAccessory={( + + )} artifactCompletionRequirement={pendingVideoArtifactCompletion?.sessionId === props.selectedSessionId ? pendingVideoArtifactCompletion.requirement : undefined} @@ -2965,7 +3278,7 @@ export function SessionPage(props: SessionPageProps) {
) : null} - {mainWorkspaceView === null && !showDelayedSessionLoadingState && !canRenderReactSurface && !showStartupSkeleton ? ( + {mainWorkspaceView === null && !showInitialProjectTaskStarter && !showDelayedSessionLoadingState && !canRenderReactSurface && !showStartupSkeleton ? (
{props.notFoundMessage ? (
@@ -3301,66 +3614,11 @@ export function SessionPage(props: SessionPageProps) {
{t("projects.default_engine")} - { - if (engineId === DEFAULT_ENGINE_ID || engineId === DEEPSEEK_HARNESS_ENGINE_ID) { - setCreateProjectEngineId(engineId); - } - }} + onValueChange={setCreateProjectEngineId} disabled={createProjectBusy} - aria-label={t("projects.default_engine")} - className="grid grid-cols-2 gap-4" - > - {[ - { - id: DEFAULT_ENGINE_ID, - name: t("projects.engine_opencode"), - description: t("projects.engine_opencode_description"), - }, - { - id: DEEPSEEK_HARNESS_ENGINE_ID, - name: t("projects.engine_dsh"), - description: t("projects.engine_dsh_description"), - }, - ].map((engine) => { - const selected = createProjectEngineId === engine.id; - return ( - - ); - })} - + />
{t("projects.engine_locked_notice")} diff --git a/apps/app/src/react-app/domains/session/sidebar/app-sidebar.tsx b/apps/app/src/react-app/domains/session/sidebar/app-sidebar.tsx index 0ce851312..0f4bb5f60 100644 --- a/apps/app/src/react-app/domains/session/sidebar/app-sidebar.tsx +++ b/apps/app/src/react-app/domains/session/sidebar/app-sidebar.tsx @@ -512,9 +512,9 @@ export function AppSidebar(props: AppSidebarProps) { ); const [language, setLanguage] = React.useState(() => currentLocale()); const [projectsExpanded, setProjectsExpanded] = React.useState(true); - const [ungroupedExpanded, setUngroupedExpanded] = React.useState(true); - const ungroupedProject = props.projectSessionLists.find((project) => project.workspace.isDefault); const namedProjects = props.projectSessionLists.filter((project) => !project.workspace.isDefault); + const selectedProject = namedProjects.find((project) => project.workspace.id === props.selectedWorkspaceId) + ?? namedProjects[0]; const primarySidebarActionClass = cn( primarySidebarActionClassName, language === "zh" && "font-medium", @@ -524,9 +524,12 @@ export function AppSidebar(props: AppSidebarProps) { setLocale(nextLanguage); }, []); - const createUngroupedConversation = React.useCallback(async () => { - if (!ungroupedProject) return; - const workspaceId = ungroupedProject.workspace.id; + const createConversationInSelectedProject = React.useCallback(async () => { + if (!selectedProject) { + window.dispatchEvent(new Event("ipollowork:focusPrompt")); + return; + } + const workspaceId = selectedProject.workspace.id; if (props.selectedWorkspaceId !== workspaceId) { const selected = await props.onSelectProject(workspaceId); if (selected === false) return; @@ -536,7 +539,7 @@ export function AppSidebar(props: AppSidebarProps) { props.onCreateTaskInWorkspace, props.onSelectProject, props.selectedWorkspaceId, - ungroupedProject, + selectedProject, ]); const toggleSessionExpanded = React.useCallback((sessionId: string) => { @@ -662,7 +665,7 @@ export function AppSidebar(props: AppSidebarProps) { disabled={props.newTaskDisabled || !props.selectedWorkspaceId} aria-label={t("session.new_task")} aria-keyshortcuts={isMacPlatform() ? "Meta+N" : "Control+N"} - onClick={() => props.onCreateTaskInWorkspace(props.selectedWorkspaceId)} + onClick={() => void createConversationInSelectedProject()} >