From f9f7900953f5e399f3e638ef017007c2d2c7f250 Mon Sep 17 00:00:00 2001 From: GCWing Date: Sat, 28 Mar 2026 17:07:10 +0800 Subject: [PATCH] refactor(web-ui): polish Modal, nav search, welcome panel, and Git tool card UI --- .../components/NavPanel/NavSearchDialog.tsx | 22 +- .../components/Modal/Modal.scss | 48 +++- .../components/Modal/Modal.tsx | 32 ++- .../src/flow_chat/components/ChatInput.tsx | 6 +- .../src/flow_chat/components/WelcomePanel.css | 8 +- .../src/flow_chat/components/WelcomePanel.tsx | 4 +- .../flow_chat/tool-cards/GitToolDisplay.scss | 223 +++++++++++------- .../flow_chat/tool-cards/GitToolDisplay.tsx | 79 ++++--- 8 files changed, 277 insertions(+), 145 deletions(-) diff --git a/src/web-ui/src/app/components/NavPanel/NavSearchDialog.tsx b/src/web-ui/src/app/components/NavPanel/NavSearchDialog.tsx index 04d8619f..15838b2c 100644 --- a/src/web-ui/src/app/components/NavPanel/NavSearchDialog.tsx +++ b/src/web-ui/src/app/components/NavPanel/NavSearchDialog.tsx @@ -88,19 +88,29 @@ const NavSearchDialog: React.FC = ({ open, onClose }) => { }, [flowChatState.sessions, openedWorkspacesList]); const results = useMemo((): SearchResultItem[] => { - if (!query.trim()) return []; - const items: SearchResultItem[] = []; + const q = query.trim(); + + if (!q) { + for (const w of projectWorkspaces.slice(0, MAX_PER_GROUP)) { + items.push({ kind: 'workspace', id: w.id, label: w.name, sublabel: w.rootPath }); + } + for (const w of assistantWorkspacesList.slice(0, MAX_PER_GROUP)) { + const displayName = w.identity?.name?.trim() || w.name; + items.push({ kind: 'assistant', id: w.id, label: displayName, sublabel: w.description }); + } + return items; + } const filteredWorkspaces = projectWorkspaces - .filter(w => matchesQuery(query, w.name, w.rootPath)) + .filter(w => matchesQuery(q, w.name, w.rootPath)) .slice(0, MAX_PER_GROUP); for (const w of filteredWorkspaces) { items.push({ kind: 'workspace', id: w.id, label: w.name, sublabel: w.rootPath }); } const filteredAssistants = assistantWorkspacesList - .filter(w => matchesQuery(query, w.name, w.identity?.name, w.description)) + .filter(w => matchesQuery(q, w.name, w.identity?.name, w.description)) .slice(0, MAX_PER_GROUP); for (const w of filteredAssistants) { const displayName = w.identity?.name?.trim() || w.name; @@ -108,7 +118,7 @@ const NavSearchDialog: React.FC = ({ open, onClose }) => { } const filteredSessions = allSessions - .filter(({ session }) => !session.parentSessionId && matchesQuery(query, getTitle(session))) + .filter(({ session }) => !session.parentSessionId && matchesQuery(q, getTitle(session))) .slice(0, MAX_PER_GROUP); for (const { session, workspace } of filteredSessions) { items.push({ @@ -155,7 +165,7 @@ const NavSearchDialog: React.FC = ({ open, onClose }) => { } if (e.key === 'ArrowDown') { e.preventDefault(); - setActiveIndex(i => Math.min(i + 1, results.length - 1)); + setActiveIndex(i => Math.min(i + 1, Math.max(0, results.length - 1))); } else if (e.key === 'ArrowUp') { e.preventDefault(); setActiveIndex(i => Math.max(i - 1, 0)); diff --git a/src/web-ui/src/component-library/components/Modal/Modal.scss b/src/web-ui/src/component-library/components/Modal/Modal.scss index e5475360..d7feef9b 100644 --- a/src/web-ui/src/component-library/components/Modal/Modal.scss +++ b/src/web-ui/src/component-library/components/Modal/Modal.scss @@ -3,6 +3,8 @@ */ @use '../../styles/tokens.scss' as tokens; +$modal-edge-gutter: 8px; +$modal-edge-gutter-sm: 10px; .modal-overlay { position: fixed; @@ -98,20 +100,25 @@ } + &__header-shell { + position: relative; + flex-shrink: 0; + + &--close-only { + min-height: 34px; + } + } + &__header { flex-shrink: 0; display: flex; align-items: center; - justify-content: space-between; + justify-content: flex-start; gap: 6px; - padding: 6px 10px; + padding: 6px $modal-edge-gutter; border-bottom: 1px solid var(--border-subtle); background: transparent; - .modal--content-inset & { - padding-inline: 28px; - } - &--draggable { cursor: move; user-select: none; @@ -121,6 +128,14 @@ background: var(--element-bg-subtle); } } + + &--empty { + min-height: 34px; + } + } + + &.modal--with-close .modal__header:has(.modal__title-group) { + padding-inline-end: calc(#{$modal-edge-gutter} + 22px + 8px); } &__title-group { @@ -128,7 +143,7 @@ align-items: center; gap: 6px; min-width: 0; - flex: 1; + flex: 0 1 auto; } &__title { @@ -144,18 +159,22 @@ display: flex; align-items: center; flex-shrink: 0; - margin-left: auto; } &__close { + position: absolute; + top: 0; + right: $modal-edge-gutter; + bottom: 0; + z-index: 2; display: flex; align-items: center; justify-content: center; width: 22px; height: 22px; padding: 0; - margin-left: auto; + margin: auto 0; background: transparent; border: 1px solid transparent; border-radius: 6px; @@ -163,7 +182,6 @@ cursor: pointer; transition: all 0.2s ease; outline: none; - flex-shrink: 0; svg { display: block; @@ -304,11 +322,15 @@ } &__header { - padding: 12px 14px; + padding: 12px $modal-edge-gutter-sm; + } + + &.modal--with-close .modal__header:has(.modal__title-group) { + padding-inline-end: calc(#{$modal-edge-gutter-sm} + 22px + 10px); } - &.modal--content-inset .modal__header { - padding-inline: 14px; + &__close { + right: $modal-edge-gutter-sm; } &__title { diff --git a/src/web-ui/src/component-library/components/Modal/Modal.tsx b/src/web-ui/src/component-library/components/Modal/Modal.tsx index 19731000..dc3ba1e6 100644 --- a/src/web-ui/src/component-library/components/Modal/Modal.tsx +++ b/src/web-ui/src/component-library/components/Modal/Modal.tsx @@ -257,6 +257,7 @@ export const Modal: React.FC = ({ resizable ? 'modal--resizable' : '', isResizing ? 'modal--resizing' : '', contentInset ? 'modal--content-inset' : '', + showCloseButton ? 'modal--with-close' : '', ] .filter(Boolean) .join(' ')} @@ -265,14 +266,31 @@ export const Modal: React.FC = ({ style={appliedStyle} > {(title || showCloseButton) && ( -
- {title && ( -
-

{title}

- {titleExtra && {titleExtra}} + {(title || (draggable && showCloseButton)) && ( +
+ {title && ( +
+

{title}

+ {titleExtra && {titleExtra}} +
+ )}
)} {showCloseButton && ( diff --git a/src/web-ui/src/flow_chat/components/ChatInput.tsx b/src/web-ui/src/flow_chat/components/ChatInput.tsx index 74076fe0..d5ecef9b 100644 --- a/src/web-ui/src/flow_chat/components/ChatInput.tsx +++ b/src/web-ui/src/flow_chat/components/ChatInput.tsx @@ -1411,7 +1411,9 @@ export const ChatInput: React.FC = ({ containerRef.current && !containerRef.current.contains(target) ) { - if (inputState.value.trim() === '') { + // While IME is composing, React value can still be empty (RichTextInput skips onChange), + // but the editor DOM holds preedit text — collapsing would show space-hint on top of it. + if (inputState.value.trim() === '' && !isImeComposingRef.current) { dispatchInput({ type: 'DEACTIVATE' }); } } @@ -1589,7 +1591,7 @@ export const ChatInput: React.FC = ({ data-testid="chat-input-textarea" /> - {!inputState.isActive && ( + {!inputState.isActive && !inputState.value.trim() && ( = ({ {t('welcome.noWorkspaceHint')}