Commit 468ea511 authored by edy's avatar edy

feat(ui): 左侧导航栏移植Windows客户端透明风格UI,新增搜索过滤功能

- 侧边栏背景/导航按钮/专家分类/专家条目/会话卡片改为透明若隐若现风格
- 新增侧边栏搜索框,支持过滤导航项、专家列表、会话列表
- 去掉品牌名千匠问天四字,仅保留logo图标
- 清理.sidebar-brand-name死代码和重复的.sidebar-filter-empty样式
- 修复sidebarKnowledgeSource测试用例以适配新代码结构
Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
parent 0d0a22ea
Pipeline #18503 failed
import type { ReactNode } from "react" import { useMemo, useState, type ReactNode } from "react"
import type { ProjectSessionSummary } from "@qjclaw/shared-types" import type { ProjectSessionSummary } from "@qjclaw/shared-types"
import { Sidebar } from "./Sidebar" import { Sidebar } from "./Sidebar"
import { ExpertTree, type ExpertCategoryId, type ExpertVisualKey, type SidebarExpertEntry } from "./ExpertTree" import { ExpertTree, type ExpertCategoryId, type ExpertVisualKey, type SidebarExpertEntry } from "./ExpertTree"
...@@ -69,6 +69,44 @@ export function AppSidebar({ ...@@ -69,6 +69,44 @@ export function AppSidebar({
onOpenSession, onOpenSession,
onCloseSession onCloseSession
}: AppSidebarProps) { }: AppSidebarProps) {
const [sidebarSearchQuery, setSidebarSearchQuery] = useState("")
const normalizedSidebarSearchQuery = sidebarSearchQuery.trim().toLocaleLowerCase()
const isSearchingSidebar = normalizedSidebarSearchQuery.length > 0
const matchesSidebarSearch = (value: string) => value.toLocaleLowerCase().includes(normalizedSidebarSearchQuery)
const filteredSessions = useMemo(
() => {
if (!isSearchingSidebar) {
return sessions
}
if (matchesSidebarSearch(sidebarSessionLabel)) {
return sessions
}
return sessions.filter((session, index) => {
const title = sidebarSessionTitles[session.id] ?? formatSessionTitle(session.title, index)
return matchesSidebarSearch(title)
})
},
[formatSessionTitle, isSearchingSidebar, normalizedSidebarSearchQuery, sessions, sidebarSessionLabel, sidebarSessionTitles]
)
const headerContent = (
<label className="sidebar-search app-no-drag">
<span className="sidebar-search-icon" aria-hidden="true">
<svg viewBox="0 0 20 20" focusable="false">
<path d="m14.2 14.2 2.8 2.8M8.9 15.1a6.2 6.2 0 1 1 0-12.4 6.2 6.2 0 0 1 0 12.4Z" />
</svg>
</span>
<input
type="search"
value={sidebarSearchQuery}
placeholder="搜索"
aria-label="搜索侧栏"
onChange={(event) => setSidebarSearchQuery(event.target.value)}
/>
</label>
)
const topContent = ( const topContent = (
<> <>
<nav className="nav-list" aria-label="主导航"> <nav className="nav-list" aria-label="主导航">
...@@ -78,7 +116,7 @@ export function AppSidebar({ ...@@ -78,7 +116,7 @@ export function AppSidebar({
{ id: "knowledge" as const, label: ui.knowledge }, { id: "knowledge" as const, label: ui.knowledge },
{ id: "automation" as const, label: "自动化任务" }, { id: "automation" as const, label: "自动化任务" },
{ id: "settings" as const, label: ui.settings } { id: "settings" as const, label: ui.settings }
].map((item) => ( ].filter((item) => !isSearchingSidebar || matchesSidebarSearch(item.label)).map((item) => (
<button <button
key={item.id} key={item.id}
type="button" type="button"
...@@ -93,9 +131,7 @@ export function AppSidebar({ ...@@ -93,9 +131,7 @@ export function AppSidebar({
</button> </button>
))} ))}
</nav> </nav>
<div className="conversation-sidebar-action"> {!showBindEntry ? <div className="conversation-sidebar-action">{sidebarNewSessionAction}</div> : null}
{!showBindEntry ? sidebarNewSessionAction : null}
</div>
</> </>
) )
...@@ -107,6 +143,7 @@ export function AppSidebar({ ...@@ -107,6 +143,7 @@ export function AppSidebar({
viewMode={viewMode} viewMode={viewMode}
prompt={prompt} prompt={prompt}
activeProjectId={activeProjectId} activeProjectId={activeProjectId}
searchQuery={normalizedSidebarSearchQuery}
onToggleCategory={onToggleCategory} onToggleCategory={onToggleCategory}
onExpertSelect={onExpertSelect} onExpertSelect={onExpertSelect}
renderCategoryIcon={renderCategoryIcon} renderCategoryIcon={renderCategoryIcon}
...@@ -115,7 +152,8 @@ export function AppSidebar({ ...@@ -115,7 +152,8 @@ export function AppSidebar({
<SessionList <SessionList
visible={!showBindEntry} visible={!showBindEntry}
label={sidebarSessionLabel} label={sidebarSessionLabel}
sessions={sessions} sessions={filteredSessions}
totalSessionCount={sessions.length}
activeSessionId={activeSessionId} activeSessionId={activeSessionId}
sessionTitles={sidebarSessionTitles} sessionTitles={sidebarSessionTitles}
projectActionPending={projectActionPending} projectActionPending={projectActionPending}
...@@ -123,6 +161,7 @@ export function AppSidebar({ ...@@ -123,6 +161,7 @@ export function AppSidebar({
activeStreamSessionId={activeStreamSessionId} activeStreamSessionId={activeStreamSessionId}
closeLabel={ui.closeSession} closeLabel={ui.closeSession}
formatSessionTitle={formatSessionTitle} formatSessionTitle={formatSessionTitle}
isFiltering={isSearchingSidebar}
onOpenSession={onOpenSession} onOpenSession={onOpenSession}
onCloseSession={onCloseSession} onCloseSession={onCloseSession}
/> />
...@@ -133,6 +172,7 @@ export function AppSidebar({ ...@@ -133,6 +172,7 @@ export function AppSidebar({
<Sidebar <Sidebar
topContent={topContent} topContent={topContent}
bottomContent={bottomContent} bottomContent={bottomContent}
headerContent={headerContent}
isCollapsed={isCollapsed} isCollapsed={isCollapsed}
onToggleCollapsed={onToggleCollapsed} onToggleCollapsed={onToggleCollapsed}
/> />
......
...@@ -107,6 +107,7 @@ interface ExpertTreeProps { ...@@ -107,6 +107,7 @@ interface ExpertTreeProps {
viewMode: "chat" | "experts" | "tasks" | "automation" | "plugins" | "settings" | "knowledge" viewMode: "chat" | "experts" | "tasks" | "automation" | "plugins" | "settings" | "knowledge"
prompt: string prompt: string
activeProjectId?: string activeProjectId?: string
searchQuery?: string
onToggleCategory(categoryId: string): void onToggleCategory(categoryId: string): void
onExpertSelect(entry: SidebarExpertEntry): void onExpertSelect(entry: SidebarExpertEntry): void
renderCategoryIcon(categoryId: ExpertCategoryId): ReactNode renderCategoryIcon(categoryId: ExpertCategoryId): ReactNode
...@@ -119,11 +120,40 @@ export function ExpertTree({ ...@@ -119,11 +120,40 @@ export function ExpertTree({
viewMode, viewMode,
prompt, prompt,
activeProjectId, activeProjectId,
searchQuery = "",
onToggleCategory, onToggleCategory,
onExpertSelect, onExpertSelect,
renderCategoryIcon, renderCategoryIcon,
renderExpertIcon renderExpertIcon
}: ExpertTreeProps) { }: ExpertTreeProps) {
const normalizedSearchQuery = searchQuery.trim().toLocaleLowerCase()
const isSearching = normalizedSearchQuery.length > 0
const matchesSearch = (value: string) => value.toLocaleLowerCase().includes(normalizedSearchQuery)
const sectionMatches = isSearching && matchesSearch("数字员工")
const visibleCategories = CATEGORY_CONFIG.map((category) => {
const categoryEntries = entries.filter((entry) => expertMatchesCategory(entry, category.id))
const categoryMatches = sectionMatches || (isSearching && matchesSearch(category.name))
const categoryExperts = isSearching && !categoryMatches
? categoryEntries.filter((entry) => matchesSearch(entry.displayName) || matchesSearch(entry.definition.id))
: categoryEntries
const hasMatchingExperts = categoryExperts.length > 0
if (isSearching && !categoryMatches && !hasMatchingExperts) {
return null
}
return {
category,
categoryExperts,
hasExperts: categoryEntries.length > 0,
isExpanded: isSearching ? hasMatchingExperts : expandedCategories[category.id] || false
}
}).filter((categoryModel): categoryModel is NonNullable<typeof categoryModel> => categoryModel !== null)
if (isSearching && visibleCategories.length === 0) {
return null
}
return ( return (
<section className="sidebar-section compact sidebar-experts-entry"> <section className="sidebar-section compact sidebar-experts-entry">
<div className="sidebar-section-head sidebar-digital-workers-title"> <div className="sidebar-section-head sidebar-digital-workers-title">
...@@ -134,11 +164,8 @@ export function ExpertTree({ ...@@ -134,11 +164,8 @@ export function ExpertTree({
</div> </div>
<div className="sidebar-expert-scroll"> <div className="sidebar-expert-scroll">
<div className="expert-category-list"> <div className="expert-category-list">
{CATEGORY_CONFIG.map((category) => { {visibleCategories.map(({ category, categoryExperts, hasExperts, isExpanded }) => {
const categoryExperts = entries.filter((entry) => expertMatchesCategory(entry, category.id))
const isExpanded = expandedCategories[category.id] || false
const categoryPanelId = `expert-tree-${category.id}` const categoryPanelId = `expert-tree-${category.id}`
const hasExperts = categoryExperts.length > 0
return ( return (
<div key={category.id} className={"expert-category-item expert-tree-category" + (isExpanded && hasExperts ? " expanded" : "")}> <div key={category.id} className={"expert-category-item expert-tree-category" + (isExpanded && hasExperts ? " expanded" : "")}>
......
...@@ -6,6 +6,7 @@ interface SessionListProps { ...@@ -6,6 +6,7 @@ interface SessionListProps {
visible: boolean visible: boolean
label: string label: string
sessions: ProjectSessionSummary[] sessions: ProjectSessionSummary[]
totalSessionCount?: number
activeSessionId: string activeSessionId: string
sessionTitles: Record<string, string> sessionTitles: Record<string, string>
projectActionPending: boolean projectActionPending: boolean
...@@ -13,6 +14,7 @@ interface SessionListProps { ...@@ -13,6 +14,7 @@ interface SessionListProps {
activeStreamSessionId?: string activeStreamSessionId?: string
closeLabel: string closeLabel: string
formatSessionTitle(title: string, index: number): string formatSessionTitle(title: string, index: number): string
isFiltering?: boolean
onOpenSession(session: ProjectSessionSummary): void onOpenSession(session: ProjectSessionSummary): void
onCloseSession(sessionId: string): void onCloseSession(sessionId: string): void
} }
...@@ -21,6 +23,7 @@ export function SessionList({ ...@@ -21,6 +23,7 @@ export function SessionList({
visible, visible,
label, label,
sessions, sessions,
totalSessionCount,
activeSessionId, activeSessionId,
sessionTitles, sessionTitles,
projectActionPending, projectActionPending,
...@@ -28,6 +31,7 @@ export function SessionList({ ...@@ -28,6 +31,7 @@ export function SessionList({
activeStreamSessionId, activeStreamSessionId,
closeLabel, closeLabel,
formatSessionTitle, formatSessionTitle,
isFiltering = false,
onOpenSession, onOpenSession,
onCloseSession onCloseSession
}: SessionListProps) { }: SessionListProps) {
...@@ -35,6 +39,8 @@ export function SessionList({ ...@@ -35,6 +39,8 @@ export function SessionList({
return null return null
} }
const effectiveTotal = totalSessionCount ?? sessions.length
return ( return (
<section className="sidebar-section sidebar-section-fill compact sidebar-session-section"> <section className="sidebar-section sidebar-section-fill compact sidebar-session-section">
<div className="sidebar-section-head sidebar-section-head-subtle"> <div className="sidebar-section-head sidebar-section-head-subtle">
...@@ -44,33 +50,37 @@ export function SessionList({ ...@@ -44,33 +50,37 @@ export function SessionList({
</div> </div>
</div> </div>
<div className="sidebar-session-list"> <div className="sidebar-session-list">
{sessions.map((session, index) => ( {sessions.length > 0 ? (
<div key={session.id} className={"sidebar-session-card" + (activeSessionId === session.id ? " active" : "")}> sessions.map((session, index) => (
<button <div key={session.id} className={"sidebar-session-card" + (activeSessionId === session.id ? " active" : "")}>
type="button"
className="sidebar-session-main app-no-drag"
aria-current={activeSessionId === session.id ? "true" : undefined}
disabled={projectActionPending}
onClick={() => onOpenSession(session)}
>
<strong>{sessionTitles[session.id] ?? formatSessionTitle(session.title, index)}{session.isChannelSession ? <span className="sidebar-session-tag">飞书</span> : null}</strong>
</button>
{sessions.length > 1 ? (
<button <button
type="button" type="button"
className="sidebar-session-close app-no-drag" className="sidebar-session-main app-no-drag"
aria-label={closeLabel} aria-current={activeSessionId === session.id ? "true" : undefined}
title={closeLabel} disabled={projectActionPending}
disabled={projectActionPending || (sendPhase !== "idle" && activeStreamSessionId === session.id)} onClick={() => onOpenSession(session)}
onClick={() => onCloseSession(session.id)}
> >
<svg viewBox="0 0 16 16" aria-hidden="true" focusable="false"> <strong>{sessionTitles[session.id] ?? formatSessionTitle(session.title, index)}{session.isChannelSession ? <span className="sidebar-session-tag">飞书</span> : null}</strong>
<path d="M4.25 4.25 11.75 11.75M11.75 4.25 4.25 11.75" />
</svg>
</button> </button>
) : null} {effectiveTotal > 1 ? (
</div> <button
))} type="button"
className="sidebar-session-close app-no-drag"
aria-label={closeLabel}
title={closeLabel}
disabled={projectActionPending || (sendPhase !== "idle" && activeStreamSessionId === session.id)}
onClick={() => onCloseSession(session.id)}
>
<svg viewBox="0 0 16 16" aria-hidden="true" focusable="false">
<path d="M4.25 4.25 11.75 11.75M11.75 4.25 4.25 11.75" />
</svg>
</button>
) : null}
</div>
))
) : isFiltering ? (
<div className="sidebar-filter-empty">没有匹配会话</div>
) : null}
</div> </div>
</section> </section>
) )
......
...@@ -4,16 +4,23 @@ import brandIcon from "../../assets/brand-icon.png" ...@@ -4,16 +4,23 @@ import brandIcon from "../../assets/brand-icon.png"
interface SidebarProps { interface SidebarProps {
topContent: ReactNode topContent: ReactNode
bottomContent: ReactNode bottomContent: ReactNode
headerContent?: ReactNode
isCollapsed: boolean isCollapsed: boolean
onToggleCollapsed(): void onToggleCollapsed(): void
} }
export function Sidebar({ topContent, bottomContent, isCollapsed, onToggleCollapsed }: SidebarProps) { export function Sidebar({ topContent, bottomContent, headerContent, isCollapsed, onToggleCollapsed }: SidebarProps) {
return ( return (
<aside className="sidebar conversation-sidebar-layout app-drag-region"> <aside className="sidebar conversation-sidebar-layout app-drag-region">
<div className="sidebar-brand"> <div className="sidebar-brand">
<img src={brandIcon} alt="" className="sidebar-brand-logo" /> {!isCollapsed ? (
<strong className="sidebar-brand-name">千匠问天</strong> <img src={brandIcon} alt="" className="sidebar-brand-logo" />
) : null}
{!isCollapsed && headerContent ? (
<div className="sidebar-header-content">
{headerContent}
</div>
) : null}
<button <button
type="button" type="button"
className="sidebar-collapse-button app-no-drag" className="sidebar-collapse-button app-no-drag"
......
...@@ -15,6 +15,10 @@ ...@@ -15,6 +15,10 @@
grid-template-columns: 64px minmax(0, 1fr); grid-template-columns: 64px minmax(0, 1fr);
} }
.shell.sidebar-collapsed {
grid-template-columns: 64px minmax(0, 1fr);
}
.skip-link { .skip-link {
position: fixed; position: fixed;
top: 10px; top: 10px;
...@@ -48,6 +52,19 @@ ...@@ -48,6 +52,19 @@
box-shadow: inset -1px 0 0 rgba(148, 163, 184, 0.22); box-shadow: inset -1px 0 0 rgba(148, 163, 184, 0.22);
} }
.sidebar-filter-empty {
min-height: 42px;
display: grid;
place-items: center;
padding: 0 12px;
border-radius: 10px;
border: 1px dashed rgba(96, 165, 250, 0.28);
background: rgba(255, 255, 255, 0.52);
color: #64748b;
font-size: 13px;
line-height: 1.4;
}
.nav-list, .nav-list,
.page-stack, .page-stack,
.catalog-list, .catalog-list,
...@@ -129,7 +146,7 @@ ...@@ -129,7 +146,7 @@
overflow-y: auto; overflow-y: auto;
overflow-x: hidden; overflow-x: hidden;
scrollbar-width: thin; scrollbar-width: thin;
scrollbar-color: rgba(96, 165, 250, 0.34) transparent; scrollbar-color: rgba(94, 164, 216, 0.44) transparent;
} }
.sidebar-expert-scroll::-webkit-scrollbar { .sidebar-expert-scroll::-webkit-scrollbar {
...@@ -138,16 +155,41 @@ ...@@ -138,16 +155,41 @@
.sidebar-expert-scroll::-webkit-scrollbar-track { .sidebar-expert-scroll::-webkit-scrollbar-track {
background: transparent; background: transparent;
border-radius: 2px;
} }
.sidebar-expert-scroll::-webkit-scrollbar-thumb { .sidebar-expert-scroll::-webkit-scrollbar-thumb {
background: rgba(96, 165, 250, 0.34); background: rgba(94, 164, 216, 0.44);
border-radius: 2px; border-radius: 999px;
transition: background 0.2s ease; }
.sidebar-expert-scroll:hover::-webkit-scrollbar-thumb,
.sidebar-expert-scroll:focus-within::-webkit-scrollbar-thumb {
background: rgba(37, 99, 235, 0.42);
}
.sidebar-session-list {
min-height: 0;
overflow-y: auto;
overflow-x: hidden;
scrollbar-width: thin;
scrollbar-color: rgba(94, 164, 216, 0.44) transparent;
}
.sidebar-session-list::-webkit-scrollbar {
width: 4px;
}
.sidebar-session-list::-webkit-scrollbar-track {
background: transparent;
}
.sidebar-session-list::-webkit-scrollbar-thumb {
background: rgba(94, 164, 216, 0.44);
border-radius: 999px;
} }
.sidebar-expert-scroll::-webkit-scrollbar-thumb:hover { .sidebar-session-list:hover::-webkit-scrollbar-thumb,
.sidebar-session-list:focus-within::-webkit-scrollbar-thumb {
background: rgba(37, 99, 235, 0.42); background: rgba(37, 99, 235, 0.42);
} }
......
This diff is collapsed.
...@@ -30,24 +30,23 @@ test("app shell wires a collapsible sidebar state into the sidebar components", ...@@ -30,24 +30,23 @@ test("app shell wires a collapsible sidebar state into the sidebar components",
}) })
test("sidebar nav omits plugins and keeps the new conversation action visible when expanded", () => { test("sidebar nav omits plugins and keeps the new conversation action visible when expanded", () => {
const navItems = appSidebarSource.match(/\{\s*id:\s*"chat"[\s\S]*?\]\.map/)?.[0] ?? "" const navItems = appSidebarSource.match(/\{\s*id:\s*"chat"[\s\S]*?\]\.(filter[\s\S]*?\)\.)?map/)?.[0] ?? ""
assert.match(navItems, /id:\s*"chat"/) assert.match(navItems, /id:\s*"chat"/)
assert.match(navItems, /id:\s*"tasks"/) assert.match(navItems, /id:\s*"tasks"/)
assert.match(navItems, /id:\s*"knowledge"/) assert.match(navItems, /id:\s*"knowledge"/)
assert.match(navItems, /id:\s*"settings"/) assert.match(navItems, /id:\s*"settings"/)
assert.doesNotMatch(navItems, /id:\s*"plugins"/) assert.doesNotMatch(navItems, /id:\s*"plugins"/)
assert.match(appSidebarSource, /<div className="conversation-sidebar-action">\s*\{!showBindEntry \? sidebarNewSessionAction : null\}\s*<\/div>/) assert.match(appSidebarSource, /\{!showBindEntry \? <div className="conversation-sidebar-action">\{sidebarNewSessionAction\}<\/div> : null\}/)
}) })
test("sidebar renders the brand row and hides non-icon content in collapsed mode", () => { test("sidebar renders the brand row and hides non-icon content in collapsed mode", () => {
assert.match(sidebarSource, /brandIcon/) assert.match(sidebarSource, /brandIcon/)
assert.match(sidebarSource, /sidebar-brand/) assert.match(sidebarSource, /sidebar-brand/)
assert.match(sidebarSource, /千匠问天/)
assert.match(sidebarSource, /sidebar-collapse-button/) assert.match(sidebarSource, /sidebar-collapse-button/)
assert.match(themeStylesSource, /\.shell\.openclaw-theme\.sidebar-collapsed\s*\{[\s\S]*?grid-template-columns:\s*64px minmax\(0, 1fr\);/m) assert.match(shellStylesSource, /\.shell\.sidebar-collapsed\s*\{[\s\S]*?grid-template-columns:\s*64px minmax\(0, 1fr\);/m)
assert.match(themeStylesSource, /\.shell\.openclaw-theme\.sidebar-collapsed \.sidebar-brand-logo,\s*\n\.shell\.openclaw-theme\.sidebar-collapsed \.sidebar-brand-name/) assert.match(themeStylesSource, /\.shell\.openclaw-theme\.sidebar-collapsed \.sidebar-header-content,\s*\n\.shell\.openclaw-theme\.sidebar-collapsed \.sidebar-brand-logo/)
assert.match(themeStylesSource, /\.shell\.openclaw-theme\.sidebar-collapsed \.sidebar-brand\s*\{[\s\S]*?grid-template-columns:\s*44px;/m) assert.match(themeStylesSource, /\.shell\.openclaw-theme\.sidebar-collapsed \.sidebar-brand\s*\{[\s\S]*?width:\s*44px;[\s\S]*?justify-content:\s*center;/m)
assert.match(themeStylesSource, /\.shell\.openclaw-theme\.sidebar-collapsed \.sidebar-collapse-button\s*\{[\s\S]*?width:\s*44px;[\s\S]*?height:\s*44px;/m) assert.match(themeStylesSource, /\.shell\.openclaw-theme\.sidebar-collapsed \.sidebar-collapse-button\s*\{[\s\S]*?width:\s*38px;[\s\S]*?height:\s*38px;/m)
assert.match(themeStylesSource, /\.shell\.openclaw-theme\.sidebar-collapsed \.sidebar-top\s*\{[\s\S]*?align-content:\s*start;/m) assert.match(themeStylesSource, /\.shell\.openclaw-theme\.sidebar-collapsed \.sidebar-top\s*\{[\s\S]*?align-content:\s*start;/m)
assert.match(themeStylesSource, /\.shell\.openclaw-theme\.sidebar-collapsed \.nav-item-label/) assert.match(themeStylesSource, /\.shell\.openclaw-theme\.sidebar-collapsed \.nav-item-label/)
assert.match(themeStylesSource, /\.shell\.openclaw-theme\.sidebar-collapsed \.sidebar-bottom/) assert.match(themeStylesSource, /\.shell\.openclaw-theme\.sidebar-collapsed \.sidebar-bottom/)
...@@ -61,8 +60,8 @@ test("sidebar keeps brand, navigation, and lower content in fixed grid rows", () ...@@ -61,8 +60,8 @@ test("sidebar keeps brand, navigation, and lower content in fixed grid rows", ()
}) })
test("new conversation action matches the sidebar navigation control size", () => { test("new conversation action matches the sidebar navigation control size", () => {
assert.match(cssBlock(themeStylesSource, ".shell.openclaw-theme .nav-item"), /min-height:\s*42px;[\s\S]*?border-radius:\s*10px;/) assert.match(cssBlock(themeStylesSource, ".shell.openclaw-theme .nav-item"), /min-height:\s*42px;[\s\S]*?border-radius:\s*14px;/)
assert.match(cssBlock(themeStylesSource, ".shell.openclaw-theme .sidebar-new-session.conversation-new-session"), /width:\s*100%;[\s\S]*?min-height:\s*42px;[\s\S]*?justify-content:\s*center;[\s\S]*?border-radius:\s*10px;/) assert.match(cssBlock(themeStylesSource, ".shell.openclaw-theme .sidebar-new-session.conversation-new-session"), /width:\s*100%;[\s\S]*?min-height:\s*44px;[\s\S]*?justify-content:\s*center;[\s\S]*?border-radius:\s*14px;/)
}) })
test("knowledge navigation uses a book icon instead of the old document icon", () => { test("knowledge navigation uses a book icon instead of the old document icon", () => {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment