diff --git a/src/web-ui/src/flow_chat/tool-cards/CreatePlanDisplay.scss b/src/web-ui/src/flow_chat/tool-cards/CreatePlanDisplay.scss index 4b1c3827c..7abb7a50b 100644 --- a/src/web-ui/src/flow_chat/tool-cards/CreatePlanDisplay.scss +++ b/src/web-ui/src/flow_chat/tool-cards/CreatePlanDisplay.scss @@ -49,6 +49,7 @@ display: flex; align-items: center; justify-content: space-between; + gap: var(--flowchat-inline-gap, 0.35rem); padding: var(--flowchat-card-pad-y, 0.625rem) var(--flowchat-card-pad-x, 0.75rem); background: linear-gradient( 90deg, @@ -57,19 +58,32 @@ rgba(255, 255, 255, 0.02) 100% ); border-bottom: 1px solid var(--border-base); - - &--clickable { + + .create-plan-header-main { + display: flex; + align-items: center; + flex: 1; + min-width: 0; + padding: 0; + background: transparent; + border: none; + color: inherit; + text-align: left; + + &--clickable { + cursor: pointer; + transition: background 0.15s ease; + } + + &:disabled { + cursor: default; + } + } + + .create-plan-header-main--clickable { cursor: pointer; - transition: background 0.15s ease; - + &:hover { - background: linear-gradient( - 90deg, - rgba(245, 158, 11, 0.13) 0%, - rgba(245, 158, 11, 0.08) 50%, - rgba(255, 255, 255, 0.05) 100% - ); - .file-name { color: var(--tool-card-text-primary); } @@ -79,6 +93,7 @@ .header-left { display: flex; align-items: center; + min-width: 0; gap: var(--flowchat-inline-gap, 0.35rem); } @@ -102,6 +117,42 @@ font-size: var(--flowchat-font-size-xs); font-family: var(--tool-card-font-mono); color: var(--tool-card-text-secondary); + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .create-plan-header-folder-btn-wrapper { + display: inline-flex; + flex-shrink: 0; + } + + .create-plan-header-folder-btn { + display: inline-flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + width: 30px; + height: 30px; + padding: 0; + border: 1px solid transparent; + border-radius: 6px; + background: transparent; + color: var(--tool-card-text-muted); + cursor: pointer; + transition: all 0.15s ease; + + &:hover:not(:disabled) { + border-color: rgba(245, 158, 11, 0.35); + background: rgba(245, 158, 11, 0.07); + color: #f59e0b; + } + + &:disabled { + opacity: 0.45; + cursor: default; + } } &--loading-shimmer { diff --git a/src/web-ui/src/flow_chat/tool-cards/CreatePlanDisplay.tsx b/src/web-ui/src/flow_chat/tool-cards/CreatePlanDisplay.tsx index 1d6e98b57..71cf7c6f0 100644 --- a/src/web-ui/src/flow_chat/tool-cards/CreatePlanDisplay.tsx +++ b/src/web-ui/src/flow_chat/tool-cards/CreatePlanDisplay.tsx @@ -7,7 +7,7 @@ import React, { useState, useMemo, useCallback, useEffect, useRef } from 'react'; import { useTranslation } from 'react-i18next'; -import { ClipboardList, Circle, Loader2, CheckCircle, CheckCircle2, PlayCircle, XCircle, ChevronsUpDown, ChevronsDownUp } from 'lucide-react'; +import { ClipboardList, Circle, Loader2, CheckCircle, CheckCircle2, PlayCircle, XCircle, ChevronsUpDown, ChevronsDownUp, FolderOpen } from 'lucide-react'; import type { ToolCardProps } from '../types/flow-chat'; import { ideControl } from '@/shared/services/ide-control/api'; import { flowChatManager } from '@/flow_chat/services/FlowChatManager'; @@ -20,6 +20,8 @@ import { createLogger } from '@/shared/utils/logger'; import { useToolCardHeightContract } from './useToolCardHeightContract'; import { basenamePath, dirnameAbsolutePath } from '@/shared/utils/pathUtils'; import { createTodoRenderItems } from './todoRenderItems'; +import { useOptionalCurrentWorkspace } from '@/infrastructure/contexts/WorkspaceContext'; +import { isRemoteWorkspace } from '@/shared/types'; import './CreatePlanDisplay.scss'; const log = createLogger('PlanDisplay'); @@ -69,6 +71,7 @@ export const PlanDisplay: React.FC = ({ cacheKey, }) => { const { t } = useTranslation('flow-chat'); + const { workspace: currentWorkspace } = useOptionalCurrentWorkspace(); const effectiveCacheKey = cacheKey || planFilePath; const [refreshedData, setRefreshedData] = useState(() => { @@ -108,6 +111,8 @@ export const PlanDisplay: React.FC = ({ () => createTodoRenderItems(planData?.todos ?? []), [planData?.todos], ); + const planDirectoryPath = useMemo(() => dirnameAbsolutePath(planFilePath), [planFilePath]); + const isRevealPlanDisabled = !planFilePath || isRemoteWorkspace(currentWorkspace); // Subscribe to shared build state service for cross-component sync. useEffect(() => { @@ -143,7 +148,6 @@ export const PlanDisplay: React.FC = ({ } const normalizedPlanPath = planFilePath.replace(/\\/g, '/'); - const dirPath = dirnameAbsolutePath(planFilePath); const loadFromFile = async () => { // Skip refresh while writing to avoid feedback loops. @@ -181,13 +185,13 @@ export const PlanDisplay: React.FC = ({ loadFromFile(); } - if (!dirPath) { + if (!planDirectoryPath) { return; } let debounceTimer: ReturnType | null = null; - const unwatch = fileSystemService.watchFileChanges(dirPath, (event) => { + const unwatch = fileSystemService.watchFileChanges(planDirectoryPath, (event) => { const eventPath = event.path.replace(/\\/g, '/'); if (eventPath !== normalizedPlanPath) { return; @@ -212,7 +216,7 @@ export const PlanDisplay: React.FC = ({ clearTimeout(debounceTimer); } }; - }, [effectiveCacheKey, planFilePath, initialName, initialOverview, initialTodos]); + }, [effectiveCacheKey, planFilePath, planDirectoryPath, initialName, initialOverview, initialTodos]); // Build button status transitions: build -> building -> built. const buildStatus = useMemo((): 'build' | 'building' | 'built' => { @@ -246,6 +250,23 @@ export const PlanDisplay: React.FC = ({ } }, [planFilePath]); + const handleRevealPlanInExplorer = useCallback(async (event: React.MouseEvent) => { + event.stopPropagation(); + + if (isRevealPlanDisabled || !planFilePath) { + return; + } + + try { + await workspaceAPI.revealInExplorer(planFilePath); + } catch (error) { + log.warn('Failed to reveal plan file in explorer', { + planFilePath, + error, + }); + } + }, [isRevealPlanDisabled, planFilePath]); + const handleBuild = useCallback(async () => { if (!planFilePath || buildStatus !== 'build') return; @@ -306,6 +327,9 @@ ${JSON.stringify(simpleTodos, null, 2)} }, [applyExpandedState, isTodosExpanded]); const isLoading = status === 'preparing' || status === 'streaming' || status === 'running'; + const revealPlanTooltip = isRevealPlanDisabled + ? t('toolCards.plan.revealPlanUnavailable') + : t('toolCards.plan.revealPlanInExplorer'); if (!planData) { return ( @@ -323,19 +347,37 @@ ${JSON.stringify(simpleTodos, null, 2)} data-tool-card-id={toolCardId ?? ''} className={`create-plan-display status-${status}${isLoading ? ' create-plan-display--plan-generating' : ''}`} > - -
-
-
- +
+ +
-
- + + + + + + + +
diff --git a/src/web-ui/src/locales/en-US/flow-chat.json b/src/web-ui/src/locales/en-US/flow-chat.json index 6fcc21c69..4c69c7a64 100644 --- a/src/web-ui/src/locales/en-US/flow-chat.json +++ b/src/web-ui/src/locales/en-US/flow-chat.json @@ -1263,6 +1263,8 @@ "loadingPlan": "Loading plan...", "generating": "Generating...", "clickToOpenPlan": "Click to open plan file", + "revealPlanInExplorer": "Reveal plan file in file manager", + "revealPlanUnavailable": "Plan files can only be revealed for local workspaces", "remainingTodos": "{{count}} Remaining To-dos", "viewPlan": "View Plan", "build": "Build", diff --git a/src/web-ui/src/locales/zh-CN/flow-chat.json b/src/web-ui/src/locales/zh-CN/flow-chat.json index 72d8fa1e8..4c51c8bdc 100644 --- a/src/web-ui/src/locales/zh-CN/flow-chat.json +++ b/src/web-ui/src/locales/zh-CN/flow-chat.json @@ -1263,6 +1263,8 @@ "loadingPlan": "正在加载计划...", "generating": "生成中...", "clickToOpenPlan": "点击打开计划文件", + "revealPlanInExplorer": "在文件管理器中定位计划文件", + "revealPlanUnavailable": "仅本地工作区支持在文件管理器中定位计划文件", "remainingTodos": "剩余 {{count}} 个待办", "viewPlan": "查看计划", "build": "构建", diff --git a/src/web-ui/src/locales/zh-TW/flow-chat.json b/src/web-ui/src/locales/zh-TW/flow-chat.json index e1fd45157..46844a721 100644 --- a/src/web-ui/src/locales/zh-TW/flow-chat.json +++ b/src/web-ui/src/locales/zh-TW/flow-chat.json @@ -1263,6 +1263,8 @@ "loadingPlan": "正在載入計劃...", "generating": "生成中...", "clickToOpenPlan": "點擊開啟計劃檔案", + "revealPlanInExplorer": "在檔案管理器中定位計劃檔案", + "revealPlanUnavailable": "僅本機工作區支援在檔案管理器中定位計劃檔案", "remainingTodos": "剩餘 {{count}} 個待辦", "viewPlan": "查看計劃", "build": "構建",