From 55713a01b8e96780c5b4eda4b95bb347aa19284b Mon Sep 17 00:00:00 2001 From: GCWing Date: Tue, 17 Feb 2026 21:14:00 +0800 Subject: [PATCH 1/6] feat(ui): add dashed button variant --- .../components/Button/Button.scss | 18 ++++++++++++++++++ .../components/Button/Button.tsx | 4 +++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/web-ui/src/component-library/components/Button/Button.scss b/src/web-ui/src/component-library/components/Button/Button.scss index 7d4a6bea..778e46ff 100644 --- a/src/web-ui/src/component-library/components/Button/Button.scss +++ b/src/web-ui/src/component-library/components/Button/Button.scss @@ -105,6 +105,24 @@ } } + &-dashed { + background: transparent; + color: var(--color-text-secondary, #e5e5e5); + border: 1px dashed var(--border-base, rgba(255, 255, 255, 0.16)); + + &:hover:not(:disabled) { + background: var(--element-bg-subtle, rgba(255, 255, 255, 0.08)); + color: var(--color-text-primary, #ffffff); + border-style: solid; + border-color: var(--border-medium, rgba(255, 255, 255, 0.24)); + } + + &:active:not(:disabled) { + background: var(--element-bg-base, rgba(255, 255, 255, 0.1)); + border-color: var(--border-strong, rgba(255, 255, 255, 0.32)); + } + } + &-icon-only { padding: 0; width: var(--button-height-base, 40px); diff --git a/src/web-ui/src/component-library/components/Button/Button.tsx b/src/web-ui/src/component-library/components/Button/Button.tsx index 3421086c..dd1040fe 100644 --- a/src/web-ui/src/component-library/components/Button/Button.tsx +++ b/src/web-ui/src/component-library/components/Button/Button.tsx @@ -6,7 +6,7 @@ import React, { forwardRef } from 'react'; import './Button.scss'; export interface ButtonProps extends React.ButtonHTMLAttributes { - variant?: 'primary' | 'secondary' | 'ghost' | 'danger' | 'success' | 'accent' | 'ai'; + variant?: 'primary' | 'secondary' | 'ghost' | 'dashed' | 'danger' | 'success' | 'accent' | 'ai'; size?: 'small' | 'medium' | 'large'; isLoading?: boolean; iconOnly?: boolean; @@ -43,6 +43,8 @@ export const Button = forwardRef(({ return 'btn-action btn-action-success'; case 'ghost': return 'btn-ghost'; + case 'dashed': + return 'btn-dashed'; default: return 'btn-secondary'; } From c181cffa8262b98f645a42f6966c32b7ec69741d Mon Sep 17 00:00:00 2001 From: GCWing Date: Tue, 17 Feb 2026 21:14:13 +0800 Subject: [PATCH 2/6] fix(tab): center title and show close button only on hover --- .../app/components/panels/content-canvas/tab-bar/Tab.scss | 5 +++++ .../src/app/components/panels/content-canvas/tab-bar/Tab.tsx | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/web-ui/src/app/components/panels/content-canvas/tab-bar/Tab.scss b/src/web-ui/src/app/components/panels/content-canvas/tab-bar/Tab.scss index f1add210..f1c570ed 100644 --- a/src/web-ui/src/app/components/panels/content-canvas/tab-bar/Tab.scss +++ b/src/web-ui/src/app/components/panels/content-canvas/tab-bar/Tab.scss @@ -95,6 +95,11 @@ transition: color 0.15s ease, font-weight 0.15s ease; } + // Keep title visually centered when tab is not hovered. + &:not(:hover) &__title { + text-align: center; + } + // Active state title color is brighter &.is-active &__title { color: var(--color-text-primary, rgba(255, 255, 255, 0.95)); diff --git a/src/web-ui/src/app/components/panels/content-canvas/tab-bar/Tab.tsx b/src/web-ui/src/app/components/panels/content-canvas/tab-bar/Tab.tsx index 25a4c3f2..eb2415f3 100644 --- a/src/web-ui/src/app/components/panels/content-canvas/tab-bar/Tab.tsx +++ b/src/web-ui/src/app/components/panels/content-canvas/tab-bar/Tab.tsx @@ -119,8 +119,8 @@ export const Tab: React.FC = ({ getStateClassName(tab.state), ].filter(Boolean).join(' '); - // Show close button (hidden in preview, shown on hover) - const showCloseButton = tab.state !== 'preview' || isHovered; + // Show close button only while hovering to avoid reserving layout space. + const showCloseButton = isHovered; return ( From 0a10fb01dc6d625096180ba38d522b6ad955692e Mon Sep 17 00:00:00 2001 From: GCWing Date: Tue, 17 Feb 2026 21:14:58 +0800 Subject: [PATCH 3/6] feat(i18n): add common button labels for editor --- src/web-ui/src/locales/en-US/tools.json | 7 ++++++- src/web-ui/src/locales/zh-CN/tools.json | 7 ++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/web-ui/src/locales/en-US/tools.json b/src/web-ui/src/locales/en-US/tools.json index a865f012..38f0c986 100644 --- a/src/web-ui/src/locales/en-US/tools.json +++ b/src/web-ui/src/locales/en-US/tools.json @@ -35,7 +35,12 @@ "fileNotFound": "File not found", "permissionDenied": "Permission denied", "networkError": "Network error", - "saveFailedWithMessage": "Failed to save file: {{message}}" + "saveFailedWithMessage": "Failed to save file: {{message}}", + "save": "Save", + "cancel": "Cancel", + "edit": "Edit", + "add": "Add", + "delete": "Delete" }, "codeEditor": { "loadingFile": "Loading file...", diff --git a/src/web-ui/src/locales/zh-CN/tools.json b/src/web-ui/src/locales/zh-CN/tools.json index ebd0d58a..13fd6bfc 100644 --- a/src/web-ui/src/locales/zh-CN/tools.json +++ b/src/web-ui/src/locales/zh-CN/tools.json @@ -35,7 +35,12 @@ "fileNotFound": "文件不存在", "permissionDenied": "权限不足", "networkError": "网络错误", - "saveFailedWithMessage": "保存文件失败: {{message}}" + "saveFailedWithMessage": "保存文件失败: {{message}}", + "save": "保存", + "cancel": "取消", + "edit": "编辑", + "add": "新增", + "delete": "删除" }, "codeEditor": { "loadingFile": "正在加载文件...", From 111eff0e48d658db77a368e336d01c5eddedde0c Mon Sep 17 00:00:00 2001 From: GCWing Date: Tue, 17 Feb 2026 21:15:23 +0800 Subject: [PATCH 4/6] feat(planview): support inline todo editing and yaml editor placement --- .../tools/editor/components/PlanViewer.tsx | 347 ++++++++++++------ 1 file changed, 236 insertions(+), 111 deletions(-) diff --git a/src/web-ui/src/tools/editor/components/PlanViewer.tsx b/src/web-ui/src/tools/editor/components/PlanViewer.tsx index 9b2ab171..a73ac69b 100644 --- a/src/web-ui/src/tools/editor/components/PlanViewer.tsx +++ b/src/web-ui/src/tools/editor/components/PlanViewer.tsx @@ -6,7 +6,7 @@ import yaml from 'yaml'; import { MEditor } from '../meditor'; import type { EditorInstance } from '../meditor'; import { createLogger } from '@/shared/utils/logger'; -import { CubeLoading, Button } from '@/component-library'; +import { CubeLoading, Button, Tooltip } from '@/component-library'; import { useI18n } from '@/infrastructure/i18n'; import { workspaceAPI } from '@/infrastructure/api/service-api/WorkspaceAPI'; import { flowChatManager } from '@/flow_chat/services/FlowChatManager'; @@ -33,6 +33,8 @@ interface PlanData { todos: PlanTodo[]; } +type YamlEditorPlacement = 'none' | 'inline' | 'trailing'; + export interface PlanViewerProps { /** File path */ filePath: string; @@ -64,15 +66,21 @@ const PlanViewer: React.FC = ({ }); const [originalContent, setOriginalContent] = useState(''); // Edit mode: display raw yaml frontmatter - const [isEditingYaml, setIsEditingYaml] = useState(false); + const [yamlEditorPlacement, setYamlEditorPlacement] = useState('none'); const [yamlContent, setYamlContent] = useState(''); const [originalYamlContent, setOriginalYamlContent] = useState(''); // Todos list expand/collapse state (collapsed by default) const [isTodosExpanded, setIsTodosExpanded] = useState(false); + const [isInlineTodoEditing, setIsInlineTodoEditing] = useState(false); + const [inlineTodoDrafts, setInlineTodoDrafts] = useState>({}); + const [inlineDeletedTodoKeys, setInlineDeletedTodoKeys] = useState([]); + const [inlineAddedTodos, setInlineAddedTodos] = useState([]); const [isTrailingTodoEditing, setIsTrailingTodoEditing] = useState(false); const [trailingTodoDrafts, setTrailingTodoDrafts] = useState>({}); const [trailingDeletedTodoKeys, setTrailingDeletedTodoKeys] = useState([]); const [trailingAddedTodos, setTrailingAddedTodos] = useState([]); + + const isEditingYaml = yamlEditorPlacement !== 'none'; const editorRef = useRef(null); const yamlEditorRef = useRef(null); @@ -299,18 +307,56 @@ const PlanViewer: React.FC = ({ saveFileContent(); }, [saveFileContent]); - const startTrailingTodoEdit = useCallback(() => { - if (!planData?.todos?.length) return; + const buildTodoDraftsFromPlan = useCallback((todos: PlanTodo[]) => { const drafts: Record = {}; - planData.todos.forEach((todo, index) => { + todos.forEach((todo, index) => { const key = todo.id || String(index); drafts[key] = todo.content; }); - setTrailingTodoDrafts(drafts); + return drafts; + }, []); + + const startInlineTodoEdit = useCallback(() => { + if (!planData?.todos?.length) return; + setInlineTodoDrafts(buildTodoDraftsFromPlan(planData.todos)); + setInlineDeletedTodoKeys([]); + setInlineAddedTodos([]); + setIsInlineTodoEditing(true); + }, [buildTodoDraftsFromPlan, planData]); + + const cancelInlineTodoEdit = useCallback(() => { + setIsInlineTodoEditing(false); + setInlineTodoDrafts({}); + setInlineDeletedTodoKeys([]); + setInlineAddedTodos([]); + }, []); + + const handleAddInlineTodo = useCallback(() => { + const id = `new-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; + const newTodo: PlanTodo = { id, content: '', status: 'pending' }; + setInlineAddedTodos(prev => [...prev, newTodo]); + setInlineTodoDrafts(prev => ({ ...prev, [id]: '' })); + }, []); + + const handleDeleteInlineTodo = useCallback((todoKey: string) => { + if (todoKey.startsWith('new-')) { + setInlineAddedTodos(prev => prev.filter(todo => todo.id !== todoKey)); + setInlineTodoDrafts(prev => { + const { [todoKey]: _removed, ...rest } = prev; + return rest; + }); + return; + } + setInlineDeletedTodoKeys(prev => (prev.includes(todoKey) ? prev : [...prev, todoKey])); + }, []); + + const startTrailingTodoEdit = useCallback(() => { + if (!planData?.todos?.length) return; + setTrailingTodoDrafts(buildTodoDraftsFromPlan(planData.todos)); setTrailingDeletedTodoKeys([]); setTrailingAddedTodos([]); setIsTrailingTodoEditing(true); - }, [planData]); + }, [buildTodoDraftsFromPlan, planData]); const cancelTrailingTodoEdit = useCallback(() => { setIsTrailingTodoEditing(false); @@ -321,36 +367,25 @@ const PlanViewer: React.FC = ({ const handleAddTrailingTodo = useCallback(() => { const id = `new-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; - const newTodo: PlanTodo = { - id, - content: '', - status: 'pending', - }; + const newTodo: PlanTodo = { id, content: '', status: 'pending' }; setTrailingAddedTodos(prev => [...prev, newTodo]); setTrailingTodoDrafts(prev => ({ ...prev, [id]: '' })); }, []); - const saveTrailingTodoEdit = useCallback(async () => { - if (!planData?.todos?.length || !filePath) return; + const handleDeleteTrailingTodo = useCallback((todoKey: string) => { + if (todoKey.startsWith('new-')) { + setTrailingAddedTodos(prev => prev.filter(todo => todo.id !== todoKey)); + setTrailingTodoDrafts(prev => { + const { [todoKey]: _removed, ...rest } = prev; + return rest; + }); + return; + } + setTrailingDeletedTodoKeys(prev => (prev.includes(todoKey) ? prev : [...prev, todoKey])); + }, []); - const nextTodos = planData.todos - .map((todo, index) => ({ todo, key: todo.id || String(index) })) - .filter(({ key }) => !trailingDeletedTodoKeys.includes(key)) - .map(({ todo, key }) => { - const nextContent = (trailingTodoDrafts[key] ?? todo.content).trim(); - return { - ...todo, - content: nextContent || todo.content, - }; - }) - .concat( - trailingAddedTodos - .map(todo => ({ - ...todo, - content: (trailingTodoDrafts[todo.id] ?? todo.content).trim(), - })) - .filter(todo => !!todo.content) - ); + const saveTodoEdits = useCallback(async (nextTodos: PlanTodo[]) => { + if (!filePath || !planData) return; let nextYamlContent = yamlContent; if (yamlContent) { @@ -359,7 +394,7 @@ const PlanViewer: React.FC = ({ parsed.todos = nextTodos; nextYamlContent = yaml.stringify(parsed).trimEnd(); } catch (e) { - log.warn('Failed to parse yaml while saving trailing todo edit', e); + log.warn('Failed to parse yaml while saving todo edit', e); } } @@ -372,28 +407,88 @@ const PlanViewer: React.FC = ({ setYamlContent(nextYamlContent); setOriginalYamlContent(nextYamlContent); setOriginalContent(planContent); + } catch (err) { + log.error('Failed to save todo edit', err); + } + }, [filePath, planContent, planData, workspacePath, yamlContent]); + + const saveInlineTodoEdit = useCallback(async () => { + if (!planData?.todos?.length) return; + const nextTodos = planData.todos + .map((todo, index) => ({ todo, key: todo.id || String(index) })) + .filter(({ key }) => !inlineDeletedTodoKeys.includes(key)) + .map(({ todo, key }) => { + const nextContent = (inlineTodoDrafts[key] ?? todo.content).trim(); + return { ...todo, content: nextContent || todo.content }; + }) + .concat( + inlineAddedTodos + .map(todo => ({ ...todo, content: (inlineTodoDrafts[todo.id] ?? todo.content).trim() })) + .filter(todo => !!todo.content) + ); + await saveTodoEdits(nextTodos); + setIsInlineTodoEditing(false); + setInlineTodoDrafts({}); + setInlineDeletedTodoKeys([]); + setInlineAddedTodos([]); + }, [inlineAddedTodos, inlineDeletedTodoKeys, inlineTodoDrafts, planData, saveTodoEdits]); + + const saveTrailingTodoEdit = useCallback(async () => { + if (!planData?.todos?.length) return; + const nextTodos = planData.todos + .map((todo, index) => ({ todo, key: todo.id || String(index) })) + .filter(({ key }) => !trailingDeletedTodoKeys.includes(key)) + .map(({ todo, key }) => { + const nextContent = (trailingTodoDrafts[key] ?? todo.content).trim(); + return { ...todo, content: nextContent || todo.content }; + }) + .concat( + trailingAddedTodos + .map(todo => ({ ...todo, content: (trailingTodoDrafts[todo.id] ?? todo.content).trim() })) + .filter(todo => !!todo.content) + ); + await saveTodoEdits(nextTodos); + setIsTrailingTodoEditing(false); + setTrailingTodoDrafts({}); + setTrailingDeletedTodoKeys([]); + setTrailingAddedTodos([]); + }, [planData, saveTodoEdits, trailingAddedTodos, trailingDeletedTodoKeys, trailingTodoDrafts]); + + const openYamlEditor = useCallback((source: 'inline' | 'trailing' | 'unknown' = 'unknown') => { + if (source === 'inline') { + setIsInlineTodoEditing(false); + setInlineTodoDrafts({}); + setInlineDeletedTodoKeys([]); + setInlineAddedTodos([]); + setIsTodosExpanded(true); + setYamlEditorPlacement('inline'); + return; + } + if (source === 'trailing') { setIsTrailingTodoEditing(false); setTrailingTodoDrafts({}); setTrailingDeletedTodoKeys([]); setTrailingAddedTodos([]); - } catch (err) { - log.error('Failed to save trailing todo edit', err); - } - }, [planData, trailingAddedTodos, trailingDeletedTodoKeys, trailingTodoDrafts, yamlContent, planContent, workspacePath, filePath]); - - const handleDeleteTrailingTodo = useCallback((todoKey: string) => { - if (todoKey.startsWith('new-')) { - setTrailingAddedTodos(prev => prev.filter(todo => todo.id !== todoKey)); - setTrailingTodoDrafts(prev => { - const { [todoKey]: _removed, ...rest } = prev; - return rest; - }); + setYamlEditorPlacement('trailing'); return; } - setTrailingDeletedTodoKeys(prev => (prev.includes(todoKey) ? prev : [...prev, todoKey])); + setYamlEditorPlacement('inline'); + }, [isInlineTodoEditing, isTodosExpanded, isTrailingTodoEditing, yamlEditorPlacement]); + + const closeYamlEditor = useCallback(() => { + setYamlEditorPlacement('none'); }, []); - const displayedTodos = useMemo(() => { + const displayedInlineTodos = useMemo(() => { + if (!planData?.todos) return []; + if (!isInlineTodoEditing) return planData.todos; + return [ + ...planData.todos.filter((todo, index) => !inlineDeletedTodoKeys.includes(todo.id || String(index))), + ...inlineAddedTodos, + ]; + }, [inlineAddedTodos, inlineDeletedTodoKeys, isInlineTodoEditing, planData]); + + const displayedTrailingTodos = useMemo(() => { if (!planData?.todos) return []; if (!isTrailingTodoEditing) return planData.todos; return [ @@ -403,6 +498,17 @@ const PlanViewer: React.FC = ({ }, [isTrailingTodoEditing, planData, trailingAddedTodos, trailingDeletedTodoKeys]); const renderSharedTodoPanel = useCallback((placement: 'inline' | 'trailing') => { + const isInline = placement === 'inline'; + const isYamlEditingInPanel = yamlEditorPlacement === placement; + const isPanelEditing = isInline ? isInlineTodoEditing : isTrailingTodoEditing; + const panelTodos = isInline ? displayedInlineTodos : displayedTrailingTodos; + const panelDrafts = isInline ? inlineTodoDrafts : trailingTodoDrafts; + const startEdit = isInline ? startInlineTodoEdit : startTrailingTodoEdit; + const cancelEdit = isInline ? cancelInlineTodoEdit : cancelTrailingTodoEdit; + const addTodo = isInline ? handleAddInlineTodo : handleAddTrailingTodo; + const saveEdit = isInline ? saveInlineTodoEdit : saveTrailingTodoEdit; + const deleteTodo = isInline ? handleDeleteInlineTodo : handleDeleteTrailingTodo; + const panelClassName = placement === 'trailing' ? `plan-viewer-todos plan-viewer-todos--trailing ${isEditingYaml ? 'plan-viewer-todos--yaml-editing' : ''}` : `plan-viewer-todos ${isTodosExpanded ? 'plan-viewer-todos--expanded' : ''} ${isEditingYaml ? 'plan-viewer-todos--yaml-editing' : ''}`; @@ -411,65 +517,71 @@ const PlanViewer: React.FC = ({ return (
- {isEditingYaml ? ( - - ) : isTrailingTodoEditing ? ( - <> - - + {isYamlEditingInPanel ? ( + + + ) : isPanelEditing ? ( + <> + + + + + + + + + ) : ( <> - - + + + + + + )}
- {isEditingYaml ? ( + {isYamlEditingInPanel ? (
= ({
) : (
- {displayedTodos.map((todo, index) => ( + {panelTodos.map((todo, index) => (
{getTodoIcon(todo.status)} - {isTrailingTodoEditing ? ( + {isPanelEditing ? ( <> { const key = todo.id || String(index); - setTrailingTodoDrafts(prev => ({ - ...prev, - [key]: e.target.value, - })); + if (isInline) { + setInlineTodoDrafts(prev => ({ ...prev, [key]: e.target.value })); + } else { + setTrailingTodoDrafts(prev => ({ ...prev, [key]: e.target.value })); + } }} /> - + + + ) : ( {todo.content} @@ -528,20 +642,31 @@ const PlanViewer: React.FC = ({
); }, [ + cancelInlineTodoEdit, cancelTrailingTodoEdit, - displayedTodos, + closeYamlEditor, + displayedInlineTodos, + displayedTrailingTodos, + handleAddInlineTodo, handleAddTrailingTodo, + handleDeleteInlineTodo, handleDeleteTrailingTodo, handleSave, handleYamlChange, + isInlineTodoEditing, isEditingYaml, isTodosExpanded, isTrailingTodoEditing, + openYamlEditor, + saveInlineTodoEdit, saveTrailingTodoEdit, + startInlineTodoEdit, startTrailingTodoEdit, t, + inlineTodoDrafts, trailingTodoDrafts, yamlContent, + yamlEditorPlacement, ]); // Build button click handler @@ -669,7 +794,7 @@ ${JSON.stringify(simpleTodos, null, 2)}
- {hasTodos && (isEditingYaml || isTodosExpanded) && renderSharedTodoPanel('inline')} + {hasTodos && (yamlEditorPlacement === 'inline' || isTodosExpanded) && renderSharedTodoPanel('inline')}
@@ -688,7 +813,7 @@ ${JSON.stringify(simpleTodos, null, 2)} basePath={basePath} />
- {hasTodos && isTodosExpanded && !isEditingYaml && renderSharedTodoPanel('trailing')} + {hasTodos && yamlEditorPlacement !== 'inline' && renderSharedTodoPanel('trailing')}
); From e7863821c57ae24bf2fd663b44c76d717066dc2a Mon Sep 17 00:00:00 2001 From: GCWing Date: Tue, 17 Feb 2026 21:15:50 +0800 Subject: [PATCH 5/6] refactor(config): use dashed button for empty state actions --- src/web-ui/src/infrastructure/config/components/MCPConfig.tsx | 3 +-- .../infrastructure/config/components/PromptTemplateConfig.tsx | 3 +-- .../src/tools/lsp/components/LspPluginList/LspPluginList.tsx | 4 +--- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/src/web-ui/src/infrastructure/config/components/MCPConfig.tsx b/src/web-ui/src/infrastructure/config/components/MCPConfig.tsx index aae7af82..59df8379 100644 --- a/src/web-ui/src/infrastructure/config/components/MCPConfig.tsx +++ b/src/web-ui/src/infrastructure/config/components/MCPConfig.tsx @@ -676,9 +676,8 @@ export const MCPConfig: React.FC = () => {
-

{searchKeyword ? t('empty.noMatchingServers') : t('empty.noServers')}

{!searchKeyword && ( - )} diff --git a/src/web-ui/src/infrastructure/config/components/PromptTemplateConfig.tsx b/src/web-ui/src/infrastructure/config/components/PromptTemplateConfig.tsx index 88e63995..efa32ae9 100644 --- a/src/web-ui/src/infrastructure/config/components/PromptTemplateConfig.tsx +++ b/src/web-ui/src/infrastructure/config/components/PromptTemplateConfig.tsx @@ -315,9 +315,8 @@ export const PromptTemplateConfig: React.FC = () => {
{sortedTemplates.length === 0 && (
-

{t('empty.noTemplates')}