fix: align assistant workbar controls

Existing titlebar controls could be pushed into a second row, while task controls left uneven or redundant surfaces. The window titlebar is restored, the shared workbar toggle stays directly below the close control, empty conversation task rows disappear, and Task index owns creation plus full-width filters.

Release note: 修复助手工作栏顶部错位与任务入口重复问题;空会话不再显示任务栏,任务索引统一提供新建入口和全宽状态筛选。
This commit is contained in:
mesalogo
2026-08-21 08:43:17 +08:00
parent b29f2d7b0d
commit 97419f3ef7
13 changed files with 405 additions and 348 deletions
+14 -8
View File
@@ -318,10 +318,10 @@
遮罩或隔离其内容。分栏计算排除最左侧主导航,主工作区与工作栏采用相同的 `300px`
最小宽度;拖动保存两者在可用分栏宽度中的比例,任一侧到达最小值后都不得继续侵占。
所有可容纳两侧最小值的尺寸均支持指针和键盘调整,并须保留全部栏目入口。
- 工作栏开关位于窗口关闭按钮下方的当前任务或页面上下文栏最右侧,同一个按钮负责展开与
收起并持续暴露状态;图标中的箭头方向必须随当前动作切换,收起状态提示展开,展开状态
提示收起。按钮保持普通图标按钮视觉,不通过持续高亮表达展开状态。工作栏内部不再重复
关闭按钮。上下文栏应为后续任务状态、范围和高频操作预留空间,不能只作为孤立的按钮容器。
- 工作栏开关固定在窗口关闭按钮正下方,同一个按钮负责展开与收起并持续暴露状态;图标中的
箭头方向必须随当前动作切换,收起状态提示展开,展开状态提示收起。按钮保持普通图标按钮
视觉,不通过持续高亮表达展开状态。工作栏内部不再重复关闭按钮;当前会话没有 Task 时,
不得为了放置该开关而保留一条空上下文栏。
- 停靠工作栏不得获得对话框语义、焦点陷阱或背景遮罩。关闭后焦点返回同一个工作栏开关;
主工作区与工作栏在展开期间都保持可达。
- 对话消息流、输入区和输入区元信息必须在主工作区内部保留对称的左右安全边距。工作栏
@@ -330,11 +330,17 @@
### 6.10 应用顶栏与全局操作
应用顶栏用于窗口级状态、侧栏开关和低频全局操作,不承担页面标题或主要导航。顶栏必须保持紧凑,不能与页面内容争夺注意力。
应用顶栏用于当前会话标题、范围、窗口级状态、侧栏开关和低频全局操作,不承担主要导航。
顶栏必须保持紧凑,不能与页面内容争夺注意力。
- 无边框窗口使用两层顶部结构:`30px` 窗口标题栏只承载拖动区域和窗口控制;其下
`46px` 当前任务或页面上下文栏承载侧栏开关、标题、范围、状态和高频操作。普通图标按钮
使用 `34px × 34px` 点击区域。
- 无边框窗口使用一条紧凑标题栏,保留侧栏开关、当前会话标题、范围、Runtime 状态、主题
开关和窗口控制,不得为了新增工作栏入口把这一整组控件下移。普通图标按钮使用
`34px × 34px` 点击区域。
- 当前会话存在 Task 时,Task 上下文栏位于标题栏下方并承载任务摘要与详情;没有 Task 时
整行隐藏。折叠状态保持为 `46px` 单行表面,与右侧工作栏标签栏等高,左右分隔线必须水平
对齐。该栏不重复放置新建入口。
- “新建任务”只位于工作栏“任务索引”标题后方;任务状态筛选占满工作栏可用宽度,四个选项
等分,不能保留内容宽度的短控件。
- Runtime 状态、同步状态等短标签使用 `--font-caption`,不得放大为正文标题。
- 浅色与深色切换属于持续可用的窗口级操作,直接显示太阳或月亮图标,并通过可访问名称说明将切换到的主题。选择必须持久化,切换不得改变布局。
- 顶栏只直接显示当前任务所需的高频操作。已有侧栏账户设置入口时,不再重复显示 Runtime/设置入口;使用帮助优先放在相关操作附近,而不是为单个帮助项创建“更多”菜单。
@@ -223,9 +223,11 @@ Runtime
应用同一最小值约束。
- 对话内容与输入区始终保留对称的左右安全边距,工作栏边界不得覆盖或裁切输入框、发送
按钮及主工作区右侧边距。
- 唯一展开与收起开关位于窗口关闭按钮下方的当前任务或页面上下文栏最右侧。工作栏内部
不重复关闭入口。开关图标中的箭头方向随当前动作切换,按钮不以持续高亮表达展开状态;
该上下文栏为后续任务状态、范围和高频操作扩展预留位置。
- 唯一展开与收起开关固定在窗口关闭按钮正下方。工作栏内部不重复关闭入口。开关图标中的
箭头方向随当前动作切换,按钮不以持续高亮表达展开状态;当前会话没有 Task 时不渲染空
Task 上下文栏。
- “新建任务”入口位于工作栏“任务索引”标题后方,不在会话 Task 上下文栏重复出现;索引
状态筛选占满可用宽度并等分各筛选项。
- 终端、宽日志和大型成果允许用户切换到底部停靠或独立窗口。
- 应用只建议适合的布局,不因面板内容自动改变用户已经选择的停靠位置。
@@ -759,7 +761,7 @@ Electron、ChildProcess、PTY、SSH Client、Socket 或文件句柄。
- [ ] 宽、中、窄窗口均可访问完整能力目录和用户打开的面板实例。
- [ ] 工作栏在宽、中、窄窗口展开时都推动主工作区回流,不覆盖、遮罩或隔离主内容;唯一
开关始终位于当前任务或页面上下文栏最右侧。
开关始终固定在窗口关闭按钮正下方。
- [ ] 仅使用键盘可以选择能力、面板实例、目标、执行空间和控制动作。
- [ ] 状态不只依赖颜色,徽标具有文字或可访问名称。
- [ ] 终端、HTML、监督证据和高频日志具有可访问替代或降噪行为。
+2 -2
View File
@@ -48,8 +48,8 @@ GoodBuddy 应能够:
- 排除最左侧主导航后,主工作区与右侧栏使用相同的 `300px` 最小宽度,并按用户拖动后的
比例分配可用宽度;任一侧到达最小值后停止拖动。
- 主工作区中的消息流和输入区保留对称的左右安全边距,右侧栏不得覆盖或裁切其右边缘。
- 唯一展开与收起开关位于窗口关闭按钮下方的当前任务或页面上下文栏最右侧,右侧栏内部
不重复关闭入口。
- 唯一展开与收起开关固定在窗口关闭按钮正下方,右侧栏内部不重复关闭入口;当前会话没有
Task 时不保留空的 Task 上下文栏。
- 右侧栏在对话、知识库和活动视图之间保持状态。
- 知识图谱实体详情复用同一右栏容器,不再维护独立布局。
- 九个标准能力固定可达;能力、连接和数据状态可以变化,但应用不按当前上下文自动隐藏入口。
+37 -10
View File
@@ -1121,17 +1121,29 @@ describe('App', () => {
expect(
within(assistantSidebar).getByText('Project: Default project')
).toBeInTheDocument()
fireEvent.click(
within(assistantSidebar).getByRole('button', {
name: 'New custom task'
})
const taskIndexHeading = within(assistantSidebar).getByRole(
'heading',
{ name: 'Task index' }
)
const newTaskButton = within(assistantSidebar).getByRole(
'button',
{ name: 'New task' }
)
expect(taskIndexHeading.parentElement).toContainElement(
newTaskButton
)
fireEvent.click(newTaskButton)
const taskDialog = screen.getByRole('dialog', {
name: 'New custom task'
})
expect(
within(taskDialog).getByText('Default project')
).toBeInTheDocument()
expect(
within(taskDialog).getByRole('button', {
name: 'Current conversation'
})
).toHaveAttribute('aria-pressed', 'true')
fireEvent.click(
within(taskDialog).getByRole('button', {
name: 'Close new custom task'
@@ -8015,9 +8027,9 @@ describe('App', () => {
expect(sidebar).not.toHaveClass('assistant-sidebar--open')
const assistantTrigger =
screen.getByLabelText('切换助手工作栏')
expect(
container.querySelector('.topbar')
).toContainElement(assistantTrigger)
expect(assistantTrigger.parentElement).toBe(
container.querySelector('.app-shell')
)
expect(assistantTrigger).toHaveClass('assistant-sidebar-toggle')
expect(assistantTrigger).not.toHaveClass('icon-button--active')
expect(assistantTrigger).toHaveAttribute('aria-expanded', 'false')
@@ -8032,8 +8044,14 @@ describe('App', () => {
assistantTrigger.querySelector('.lucide-panel-right-close')
).not.toBeInTheDocument()
expect(
container.querySelector('.window-titlebar')
container.querySelector('.topbar')
).toContainElement(screen.getByLabelText('关闭窗口'))
expect(container.querySelector('.topbar')).toContainElement(
container.querySelector('.conversation-title')
)
expect(
container.querySelector('.topbar')
).not.toContainElement(assistantTrigger)
expect(
screen.queryByLabelText('关闭助手工作栏')
).not.toBeInTheDocument()
@@ -8059,9 +8077,18 @@ describe('App', () => {
expect(
screen.getByRole('tab', { name: '任务中心' })
).toHaveAttribute('aria-selected', 'true')
const taskIndexHeading = screen.getByRole('heading', {
name: '任务索引'
})
const newTaskButton = screen.getByRole('button', {
name: '新建任务'
})
expect(taskIndexHeading.parentElement).toContainElement(
newTaskButton
)
expect(
screen.getByRole('heading', { name: '任务索引' })
).toBeInTheDocument()
screen.queryByRole('region', { name: '当前会话的任务' })
).not.toBeInTheDocument()
expect(screen.queryByText('最近任务')).not.toBeInTheDocument()
fireEvent.click(screen.getByRole('tab', { name: '上下文' }))
expect(
+126 -137
View File
@@ -7390,7 +7390,110 @@ function App(): React.JSX.Element {
return (
<div className="app-frame">
<header className="window-titlebar">
<header className="topbar">
<button
className="icon-button sidebar-toggle"
type="button"
aria-label={t('sidebar.toggle')}
onClick={() => setSidebarOpen((open) => !open)}
ref={sidebarToggleRef}
>
<PanelLeft aria-hidden="true" size={18} />
</button>
{view === 'chat' && (
<>
<div
className="conversation-title"
title={activeConversation?.title}
>
<span className="conversation-title__text">
{activeConversation
? getConversationDisplayTitle(
activeConversation,
t('conversation.defaultTitle')
)
:
(activeProject?.kind === 'channel'
? t('conversation.remoteTitle')
: t('conversation.defaultTitle'))}
</span>
{activeConversation?.branch && (
<ConversationBranchBadge
sourceTitle={
conversationTitles.get(
activeConversation.branch.sourceConversationId
) ?? activeConversation.branch.sourceTitle
}
/>
)}
{activeConversation?.remote && (
<b className="conversation-source-badge">
{
projectChannelLabels[
activeConversation.remote.channel
]
}
</b>
)}
</div>
<ScopeBadge
scope={
activeProject
? {
kind: 'project',
projectName:
activeProjectDisplayName ?? activeProject.name
}
: {
kind: 'unavailable',
explanation: t('notices.projectNotLoaded')
}
}
/>
</>
)}
<div className="topbar__actions">
<span
className={
runtime?.available
? 'runtime-status runtime-status--online'
: 'runtime-status'
}
title={runtime?.detail}
>
<span className="runtime-status__dot" />
<span className="runtime-status__label">
{runtime?.label ?? t('runtime.detecting')}
</span>
{runtime?.capability === 'image-generation' && (
<span className="runtime-capability-badge">
{t('runtime.imageGeneration')}
</span>
)}
</span>
<button
aria-label={
resolvedAppearanceTheme === 'dark'
? t('topbar.switchLight')
: t('topbar.switchDark')
}
aria-pressed={resolvedAppearanceTheme === 'dark'}
className="icon-button theme-toggle-button"
onClick={toggleAppearanceTheme}
title={
resolvedAppearanceTheme === 'dark'
? t('topbar.switchLight')
: t('topbar.switchDark')
}
type="button"
>
{resolvedAppearanceTheme === 'dark' ? (
<Sun aria-hidden="true" size={18} />
) : (
<Moon aria-hidden="true" size={18} />
)}
</button>
</div>
<WindowControls onError={handleWindowControlError} />
</header>
<div className="app-shell">
@@ -8045,143 +8148,32 @@ function App(): React.JSX.Element {
/>
)}
{view === 'chat' && (
<button
aria-controls="assistant-sidebar"
aria-expanded={assistantSidebarOpen}
aria-label={t('topbar.toggleAssistantSidebar')}
className="icon-button assistant-sidebar-toggle"
onClick={() =>
setAssistantSidebarOpen((current) => !current)
}
ref={assistantSidebarToggleRef}
title={t('topbar.toggleAssistantSidebar')}
type="button"
>
{assistantSidebarOpen ? (
<PanelRightClose aria-hidden="true" size={18} />
) : (
<PanelRightOpen aria-hidden="true" size={18} />
)}
</button>
)}
<main
aria-hidden={backgroundIsolated ? 'true' : undefined}
className="workspace"
inert={backgroundIsolated}
>
<header
className={
view === 'chat'
? 'topbar topbar--with-assistant-toggle'
: 'topbar'
}
>
<button
className="icon-button sidebar-toggle"
type="button"
aria-label={t('sidebar.toggle')}
onClick={() => setSidebarOpen((open) => !open)}
ref={sidebarToggleRef}
>
<PanelLeft aria-hidden="true" size={18} />
</button>
{view === 'chat' && (
<>
<div
className="conversation-title"
title={activeConversation?.title}
>
<span className="conversation-title__text">
{activeConversation
? getConversationDisplayTitle(
activeConversation,
t('conversation.defaultTitle')
)
:
(activeProject?.kind === 'channel'
? t('conversation.remoteTitle')
: t('conversation.defaultTitle'))}
</span>
{activeConversation?.branch && (
<ConversationBranchBadge
sourceTitle={
conversationTitles.get(
activeConversation.branch.sourceConversationId
) ?? activeConversation.branch.sourceTitle
}
/>
)}
{activeConversation?.remote && (
<b className="conversation-source-badge">
{
projectChannelLabels[
activeConversation.remote.channel
]
}
</b>
)}
</div>
<ScopeBadge
scope={
activeProject
? {
kind: 'project',
projectName:
activeProjectDisplayName ?? activeProject.name
}
: {
kind: 'unavailable',
explanation: t('notices.projectNotLoaded')
}
}
/>
</>
)}
<div className="topbar__actions">
<span
className={
runtime?.available
? 'runtime-status runtime-status--online'
: 'runtime-status'
}
title={runtime?.detail}
>
<span className="runtime-status__dot" />
<span className="runtime-status__label">
{runtime?.label ?? t('runtime.detecting')}
</span>
{runtime?.capability === 'image-generation' && (
<span className="runtime-capability-badge">
{t('runtime.imageGeneration')}
</span>
)}
</span>
<button
aria-label={
resolvedAppearanceTheme === 'dark'
? t('topbar.switchLight')
: t('topbar.switchDark')
}
aria-pressed={resolvedAppearanceTheme === 'dark'}
className="icon-button theme-toggle-button"
onClick={toggleAppearanceTheme}
title={
resolvedAppearanceTheme === 'dark'
? t('topbar.switchLight')
: t('topbar.switchDark')
}
type="button"
>
{resolvedAppearanceTheme === 'dark' ? (
<Sun aria-hidden="true" size={18} />
) : (
<Moon aria-hidden="true" size={18} />
)}
</button>
</div>
{view === 'chat' && (
<button
aria-controls="assistant-sidebar"
aria-expanded={assistantSidebarOpen}
aria-label={t('topbar.toggleAssistantSidebar')}
className="icon-button assistant-sidebar-toggle"
onClick={() =>
setAssistantSidebarOpen((current) => !current)
}
ref={assistantSidebarToggleRef}
title={t('topbar.toggleAssistantSidebar')}
type="button"
>
{assistantSidebarOpen ? (
<PanelRightClose aria-hidden="true" size={18} />
) : (
<PanelRightOpen aria-hidden="true" size={18} />
)}
</button>
)}
</header>
{(view === 'chat' || cachedWorkspaceViewKeys.has('chat')) && (
<KeepAliveRoute
active={view === 'chat'}
@@ -8225,9 +8217,6 @@ function App(): React.JSX.Element {
!conversation.remote ? (
<ConversationTaskStrip
locale={locale}
onCreate={() =>
openCustomTaskDialog('current')
}
onRemoveSchedule={removeAssistantSchedule}
onRunSchedule={runAssistantSchedule}
onSelectTask={setSelectedAssistantTaskId}
@@ -9805,7 +9794,7 @@ function App(): React.JSX.Element {
conversationTitles={conversationTitles}
enabledLibraries={enabledSidebarLibraries}
memories={assistantMemories}
onCreateCustomTask={() => openCustomTaskDialog('new')}
onCreateCustomTask={() => openCustomTaskDialog('current')}
schedules={assistantSchedules}
selectedTaskId={selectedAssistantTaskId}
tasks={productAssistantTasks}
@@ -24,7 +24,6 @@ describe('ConversationTaskStrip', () => {
render(
<ConversationTaskStrip
locale="zh-CN"
onCreate={vi.fn()}
onRemoveSchedule={onRemoveSchedule}
onRunSchedule={onRunSchedule}
onSelectTask={vi.fn()}
@@ -62,6 +61,9 @@ describe('ConversationTaskStrip', () => {
expect(screen.getByText('每日状态', { selector: 'strong' }))
.toBeInTheDocument()
expect(
screen.queryByRole('button', { name: '新建任务' })
).not.toBeInTheDocument()
expect(screen.getByText('Execute')).toBeInTheDocument()
fireEvent.click(screen.getByRole('button', { name: '立即运行' }))
await waitFor(() =>
@@ -87,4 +89,22 @@ describe('ConversationTaskStrip', () => {
expect(onRemoveSchedule).toHaveBeenCalledWith(scheduleId)
)
})
it('hides the conversation task row when there are no tasks', () => {
render(
<ConversationTaskStrip
locale="zh-CN"
onRemoveSchedule={vi.fn(async () => undefined)}
onRunSchedule={vi.fn(async () => undefined)}
onSelectTask={vi.fn()}
onSetScheduleEnabled={vi.fn(async () => undefined)}
schedules={[]}
tasks={[]}
/>
)
expect(
screen.queryByRole('region', { name: '当前会话的任务' })
).not.toBeInTheDocument()
})
})
+98 -113
View File
@@ -2,8 +2,7 @@ import {
ChevronDown,
ChevronUp,
CircleAlert,
ClockFading,
Plus
ClockFading
} from 'lucide-react'
import { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
@@ -18,7 +17,6 @@ import {
type ConversationTaskStripProps = {
locale: string
onCreate: () => void
onRemoveSchedule: (scheduleId: string) => Promise<void>
onRunSchedule: (scheduleId: string) => Promise<void>
onSelectTask: (taskId: string) => void
@@ -33,7 +31,6 @@ type ConversationTaskStripProps = {
export function ConversationTaskStrip({
locale,
onCreate,
onRemoveSchedule,
onRunSchedule,
onSelectTask,
@@ -41,7 +38,7 @@ export function ConversationTaskStrip({
schedules,
selectedTaskId,
tasks
}: ConversationTaskStripProps): React.JSX.Element {
}: ConversationTaskStripProps): React.JSX.Element | null {
const { t } = useTranslation('workspace')
const [manuallyExpanded, setManuallyExpanded] = useState(false)
const [collapsedTaskId, setCollapsedTaskId] = useState('')
@@ -58,6 +55,10 @@ export function ConversationTaskStrip({
manuallyExpanded ||
(Boolean(selectedTaskId) && collapsedTaskId !== selectedTaskId)
if (tasks.length === 0) {
return null
}
return (
<section
aria-label={t('taskStrip.ariaLabel')}
@@ -91,120 +92,104 @@ export function ConversationTaskStrip({
<ChevronDown aria-hidden="true" size={14} />
)}
</button>
<button
className="secondary-button conversation-task-strip__create"
onClick={onCreate}
type="button"
>
<Plus aria-hidden="true" size={13} />
{t('taskStrip.create')}
</button>
</header>
{expanded && (
<div className="conversation-task-strip__content">
{tasks.length === 0 ? (
<p className="conversation-task-strip__empty">
{t('taskStrip.empty')}
</p>
) : (
<>
<div
aria-label={t('taskStrip.taskList')}
className="conversation-task-strip__list"
<div
aria-label={t('taskStrip.taskList')}
className="conversation-task-strip__list"
>
{tasks.map((task) => (
<button
aria-pressed={selectedTask?.id === task.id}
className={
selectedTask?.id === task.id
? 'conversation-task-strip__task conversation-task-strip__task--active'
: 'conversation-task-strip__task'
}
key={task.id}
onClick={() => onSelectTask(task.id)}
type="button"
>
{tasks.map((task) => (
<button
aria-pressed={selectedTask?.id === task.id}
className={
selectedTask?.id === task.id
? 'conversation-task-strip__task conversation-task-strip__task--active'
: 'conversation-task-strip__task'
}
key={task.id}
onClick={() => onSelectTask(task.id)}
type="button"
>
<span
aria-hidden="true"
className={`task-status-dot task-status-dot--${task.status}`}
/>
<span>{task.title}</span>
</button>
))}
</div>
{selectedTask && (
<article className="conversation-task-details">
<header>
<span>
<strong>{selectedTask.title}</strong>
<small>
{t(`task.status.${selectedTask.status}`)}
</small>
</span>
{selectedTask.status === 'failed' && (
<CircleAlert aria-hidden="true" size={15} />
)}
</header>
<dl>
<div>
<dt>{t('task.fields.mode')}</dt>
<dd>
{selectedSchedule
? t(`task.mode.${selectedSchedule.workMode}`)
: selectedTask.workMode
? t(`task.mode.${selectedTask.workMode}`)
: t('task.mode.unavailable')}
</dd>
</div>
<div>
<dt>{t('task.fields.schedule')}</dt>
<dd>
{selectedSchedule
? t(
`sidebar.tasks.schedule.recurrence.${selectedSchedule.recurrence}`
)
: t('task.schedule.none')}
</dd>
</div>
<div>
<dt>{t('task.fields.nextRun')}</dt>
<dd>
{selectedSchedule
? new Date(
selectedSchedule.nextRunAt
<span
aria-hidden="true"
className={`task-status-dot task-status-dot--${task.status}`}
/>
<span>{task.title}</span>
</button>
))}
</div>
{selectedTask && (
<article className="conversation-task-details">
<header>
<span>
<strong>{selectedTask.title}</strong>
<small>
{t(`task.status.${selectedTask.status}`)}
</small>
</span>
{selectedTask.status === 'failed' && (
<CircleAlert aria-hidden="true" size={15} />
)}
</header>
<dl>
<div>
<dt>{t('task.fields.mode')}</dt>
<dd>
{selectedSchedule
? t(`task.mode.${selectedSchedule.workMode}`)
: selectedTask.workMode
? t(`task.mode.${selectedTask.workMode}`)
: t('task.mode.unavailable')}
</dd>
</div>
<div>
<dt>{t('task.fields.schedule')}</dt>
<dd>
{selectedSchedule
? t(
`sidebar.tasks.schedule.recurrence.${selectedSchedule.recurrence}`
)
: t('task.schedule.none')}
</dd>
</div>
<div>
<dt>{t('task.fields.nextRun')}</dt>
<dd>
{selectedSchedule
? new Date(
selectedSchedule.nextRunAt
).toLocaleString(locale)
: t('task.notAvailable')}
</dd>
</div>
<div>
<dt>{t('task.fields.outcome')}</dt>
<dd>
{selectedTask.error ??
(selectedTask.completedAt
? t('task.completedAt', {
time: new Date(
selectedTask.completedAt
).toLocaleString(locale)
: t('task.notAvailable')}
</dd>
</div>
<div>
<dt>{t('task.fields.outcome')}</dt>
<dd>
{selectedTask.error ??
(selectedTask.completedAt
? t('task.completedAt', {
time: new Date(
selectedTask.completedAt
).toLocaleString(locale)
})
: t('task.noOutcome'))}
</dd>
</div>
</dl>
{selectedSchedule && (
<div className="conversation-task-details__actions">
<TaskScheduleActions
onError={setActionError}
onRemoveSchedule={onRemoveSchedule}
onRunSchedule={onRunSchedule}
onSetScheduleEnabled={onSetScheduleEnabled}
schedule={selectedSchedule}
taskTitle={selectedTask.title}
/>
</div>
)}
</article>
})
: t('task.noOutcome'))}
</dd>
</div>
</dl>
{selectedSchedule && (
<div className="conversation-task-details__actions">
<TaskScheduleActions
onError={setActionError}
onRemoveSchedule={onRemoveSchedule}
onRunSchedule={onRunSchedule}
onSetScheduleEnabled={onSetScheduleEnabled}
schedule={selectedSchedule}
taskTitle={selectedTask.title}
/>
</div>
)}
</>
</article>
)}
{actionError && (
<p className="conversation-task-strip__error" role="alert">
@@ -253,11 +253,17 @@ describe('RightAssistantSidebar resizing', () => {
renderSidebar({ tab: 'tasks' })
expect(screen.getByText('等待审批')).toBeInTheDocument()
const taskIndexHeading = screen.getByRole('heading', {
name: '任务索引'
})
const newTaskButton = screen.getByRole('button', {
name: '新建任务'
})
expect(taskIndexHeading.parentElement).toContainElement(
newTaskButton
)
expect(
screen.getByRole('heading', { name: '任务索引' })
).toBeInTheDocument()
expect(
screen.getByRole('button', { name: '新建定制任务' })
screen.getByRole('group', { name: '筛选任务' })
).toBeInTheDocument()
expect(screen.queryByLabelText('定时任务标题')).not.toBeInTheDocument()
expect(screen.queryByText('最近任务')).not.toBeInTheDocument()
+17 -17
View File
@@ -640,19 +640,9 @@ export function RightAssistantSidebar({
) : null}
{tab === 'tasks' && (
<section className="assistant-sidebar__section">
<div className="task-center__heading">
<p className="assistant-sidebar__section-description">
{t('sidebar.tasks.description')}
</p>
<button
className="primary-button"
onClick={onCreateCustomTask}
type="button"
>
<Plus aria-hidden="true" size={13} />
{t('sidebar.tasks.createCustom')}
</button>
</div>
<p className="assistant-sidebar__section-description">
{t('sidebar.tasks.description')}
</p>
<h3>
<ShieldAlert size={15} />
{t('sidebar.tasks.approvalsTitle')}
@@ -694,10 +684,20 @@ export function RightAssistantSidebar({
))
)}
<h3>
<ClockFading size={15} />
{t('sidebar.tasks.taskIndexTitle')}
</h3>
<div className="task-center__index-heading">
<h3>
<ClockFading size={15} />
{t('sidebar.tasks.taskIndexTitle')}
</h3>
<button
className="secondary-button task-center__create"
onClick={onCreateCustomTask}
type="button"
>
<Plus aria-hidden="true" size={13} />
{t('taskStrip.create')}
</button>
</div>
<div className="task-center__filters">
<SegmentedControl
ariaLabel={t('sidebar.tasks.filters.ariaLabel')}
@@ -192,6 +192,39 @@ describe('WorkspacePrimitives', () => {
expect(stylesheet).toMatch(
/\.composer-wrap\s*\{[^}]*width:\s*100%;[^}]*max-width:\s*100%;[^}]*min-width:\s*0;[^}]*padding:\s*var\(--space-3\)\s*max\(var\(--page-gutter\),\s*calc\(\(100% - var\(--content-reading\)\) \/ 2\)\)\s*var\(--space-2\);/u
)
expect(stylesheet).toMatch(
/\.assistant-sidebar__tabs\s*\{[^}]*grid-template-columns:\s*repeat\(5,\s*minmax\(max-content,\s*1fr\)\);/u
)
expect(stylesheet).toMatch(
/\.assistant-sidebar__tab\s*\{[^}]*white-space:\s*nowrap;/u
)
expect(stylesheet).toMatch(
/\.app-frame\s*\{[^}]*display:\s*grid;[^}]*grid-template-rows:\s*58px minmax\(0,\s*1fr\);/u
)
expect(stylesheet).toMatch(
/\.workspace\s*\{[^}]*grid-template-rows:\s*minmax\(0,\s*1fr\);/u
)
expect(stylesheet).toMatch(
/\.assistant-sidebar-toggle\s*\{[^}]*position:\s*absolute;[^}]*top:\s*6px;[^}]*right:\s*6px;/u
)
expect(stylesheet).toMatch(
/\.conversation-task-strip\s*\{[^}]*min-height:\s*46px;[^}]*padding:\s*0\s+max\(/u
)
expect(stylesheet).toMatch(
/\.conversation-task-strip__header\s*\{[^}]*min-height:\s*45px;/u
)
expect(stylesheet).toMatch(
/@media \(max-width: 720px\)\s*\{[\s\S]*?\.conversation-task-strip\s*\{[^}]*padding-right:\s*46px;[^}]*padding-left:\s*var\(--space-3\);/u
)
expect(stylesheet).not.toMatch(
/@media \(max-width: 720px\)\s*\{[\s\S]*?\.conversation-task-strip__header\s*\{[^}]*flex-direction:\s*column;/u
)
expect(stylesheet).toMatch(
/\.task-center__filters \.segmented-control\s*\{[^}]*width:\s*100%;[^}]*overflow:\s*hidden;/u
)
expect(stylesheet).toMatch(
/\.task-center__filters \.segmented-control__option\s*\{[^}]*min-width:\s*0;[^}]*flex:\s*1;/u
)
})
it('floats Runtime context compaction without shifting the composer', () => {
@@ -99,7 +99,6 @@ export const workspace = {
tasks: {
description:
'Review pending approvals and create or manage automations.',
createCustom: 'New custom task',
approvalsTitle: 'Awaiting approval',
noApprovals: 'There are no operations awaiting approval.',
deny: 'Deny',
@@ -248,7 +247,6 @@ export const workspace = {
title: 'Conversation tasks',
count: '{{count}}',
create: 'New task',
empty: 'This conversation has no tasks yet.',
taskList: 'Task list'
},
files: {
@@ -94,7 +94,6 @@ export const workspace = {
},
tasks: {
description: '处理当前待审批操作,并创建和管理自动化任务。',
createCustom: '新建定制任务',
approvalsTitle: '等待审批',
noApprovals: '当前没有等待审批的操作。',
deny: '拒绝',
@@ -236,7 +235,6 @@ export const workspace = {
title: '会话任务',
count: '{{count}} 个',
create: '新建任务',
empty: '当前会话还没有任务。',
taskList: '任务列表'
},
files: {
+41 -48
View File
@@ -196,15 +196,21 @@
.conversation-task-strip {
flex: 0 0 auto;
padding:
var(--space-2)
min-height: 46px;
padding: 0
max(var(--page-gutter), calc((100% - var(--content-reading)) / 2));
padding-right: max(
46px,
var(--page-gutter),
calc((100% - var(--content-reading)) / 2)
);
border-bottom: 1px solid var(--border-subtle);
background: var(--surface-raised);
}
.conversation-task-strip__header {
display: flex;
min-height: 45px;
align-items: center;
justify-content: space-between;
gap: var(--space-2);
@@ -239,12 +245,6 @@
font-size: var(--font-caption);
}
.conversation-task-strip__create {
min-height: 30px;
padding: 0 var(--space-2);
font-size: var(--font-caption);
}
.conversation-task-strip__content {
display: grid;
padding: var(--space-2) 0 var(--space-1);
@@ -381,7 +381,6 @@
opacity: 0.6;
}
.conversation-task-strip__empty,
.conversation-task-strip__error {
margin: 0;
color: var(--text-muted);
@@ -392,19 +391,40 @@
color: var(--danger);
}
.task-center__heading {
display: grid;
align-items: start;
.task-center__index-heading {
display: flex;
min-width: 0;
align-items: center;
margin: 8px 0 2px;
gap: var(--space-2);
}
.task-center__heading .primary-button {
justify-self: start;
.assistant-sidebar__section .task-center__index-heading h3 {
min-width: 0;
margin: 0;
}
.task-center__create {
min-height: 30px;
flex: 0 0 auto;
padding: 0 var(--space-2);
}
.task-center__filters {
min-width: 0;
overflow-x: auto;
overflow: hidden;
}
.task-center__filters .segmented-control {
width: 100%;
overflow: hidden;
}
.task-center__filters .segmented-control__option {
min-width: 0;
flex: 1;
padding-right: var(--space-1);
padding-left: var(--space-1);
}
.task-center__item {
@@ -791,19 +811,9 @@
}
.conversation-task-strip {
padding-right: var(--space-3);
padding-right: 46px;
padding-left: var(--space-3);
}
.conversation-task-strip__header {
align-items: stretch;
flex-direction: column;
}
.conversation-task-strip__create {
width: 100%;
justify-content: center;
}
}
.knowledge-scope__retrieval-mode {
@@ -2266,18 +2276,7 @@ textarea:focus-visible {
min-height: 0;
background: var(--surface-canvas);
color: var(--text-primary);
grid-template-rows: 30px minmax(0, 1fr);
}
.window-titlebar {
display: flex;
min-width: 0;
align-items: stretch;
justify-content: flex-end;
border-bottom: 1px solid var(--border-subtle);
background: var(--surface-subtle);
-webkit-app-region: drag;
user-select: none;
grid-template-rows: 58px minmax(0, 1fr);
}
.app-shell {
@@ -2977,7 +2976,7 @@ textarea:focus-visible {
flex: 1;
overflow: hidden;
background: var(--surface-raised);
grid-template-rows: 46px minmax(0, 1fr);
grid-template-rows: minmax(0, 1fr);
}
.workspace-route-cache {
@@ -3003,10 +3002,6 @@ textarea:focus-visible {
user-select: none;
}
.topbar--with-assistant-toggle {
padding-right: 46px;
}
.topbar :is(button, input, select),
.topbar__actions,
.window-controls {
@@ -3018,7 +3013,7 @@ textarea:focus-visible {
height: 100%;
flex: 0 0 auto;
align-self: stretch;
-webkit-app-region: no-drag;
margin-left: 8px;
}
.window-control {
@@ -3160,7 +3155,7 @@ button > svg {
flex: 0 0 46px;
align-items: center;
gap: 3px;
grid-template-columns: repeat(5, minmax(0, 1fr));
grid-template-columns: repeat(5, minmax(max-content, 1fr));
}
.assistant-sidebar-toggle {
@@ -3180,6 +3175,7 @@ button > svg {
color: var(--text-muted);
cursor: pointer;
font-size: 10px;
white-space: nowrap;
}
.assistant-sidebar__tab:hover,
@@ -12579,7 +12575,6 @@ details.settings-section > :not(summary) + :not(summary) {
:root[data-theme='dark'] :where(
.sidebar,
.window-titlebar,
.topbar,
.assistant-sidebar,
.project-create-card,
@@ -12602,7 +12597,6 @@ details.settings-section > :not(summary) + :not(summary) {
:root[data-theme='dark'] :where(
.sidebar,
.window-titlebar,
.topbar
) {
background: var(--surface-subtle);
@@ -12660,7 +12654,6 @@ details.settings-section > :not(summary) + :not(summary) {
:root[data-theme='dark'] :where(
.primary-nav,
.sidebar-footer,
.window-titlebar,
.topbar,
.assistant-sidebar,
.assistant-sidebar__tabs,