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:
@@ -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
@@ -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()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user