feat: redesign run history views

This commit is contained in:
mesalogo
2026-08-15 18:06:03 +08:00
parent 9cd565fcc4
commit 67d30d9e21
9 changed files with 1392 additions and 394 deletions
+7 -7
View File
@@ -2,7 +2,7 @@
## 1. 目的与适用范围 ## 1. 目的与适用范围
本文定义 GoodBuddy 桌面端的统一界面规则,适用于聊天与最近对话、知识库、智能心跳、任务与活动记录,以及后续新增的一级页面。 本文定义 GoodBuddy 桌面端的统一界面规则,适用于聊天与最近对话、知识库、智能心跳、运行记录,以及后续新增的一级页面。
设计系统解决两类问题: 设计系统解决两类问题:
@@ -501,13 +501,13 @@ GoodBuddy 是可调整窗口大小的桌面应用。响应式设计优先保证
- 状态卡片使用统一状态令牌,不只依赖颜色。 - 状态卡片使用统一状态令牌,不只依赖颜色。
- 运行历史与配置使用明确区块,不以多套相似页签混合导航、开关和筛选。 - 运行历史与配置使用明确区块,不以多套相似页签混合导航、开关和筛选。
### 13.5 任务与活动 ### 13.5 运行记录
- 任务使用 `standard` 壳层,活动记录使用 `dashboard` 壳层 - 使用 `dashboard` 壳层,并通过 `PageTabs` 提供“任务与会话 / 活动时间线 / 用量统计”三个同级视图
- “任务 / 活动”作为同级页面时使用 `PageTabs` - 默认视图按“项目 → 任务或会话 → 活动详情”组织,项目范围持续可见,任务或会话详情可以折叠
- 任务状态筛选使用 `SegmentedControl` 或筛选工具栏,不再模拟页签 - 活动时间线按项目分组、按任务或会话建立横向轨道,所有轨道共享同一执行顺序并按事件时间排列,以带身份名称的节点表达用户、主 Agent、子专家、工具、审批、状态和并行关系;选择节点后显示所属会话、不可变范围快照和完整详情
- 活动记录保留审计字段和范围,支持独立容器横向滚动。 - 用量统计与活动记录分离,支持按项目、会话和模型切换统计维度,宽表格在独立容器横向滚动。
- 批量停止、删除和清空历史遵循破坏性操作政策。 - 活动状态筛选使用 `SegmentedControl`,不与页面页签混合。清空历史遵循破坏性操作政策。
### 13.6 魔法笔记 ### 13.6 魔法笔记
+159 -53
View File
@@ -81,7 +81,7 @@ describe('ActivityPanel', () => {
await i18n.changeLanguage('zh-CN') await i18n.changeLanguage('zh-CN')
}) })
it('renders English interface copy while preserving activity content', async () => { it('renders localized tabs and keeps usage separate from activity', async () => {
await i18n.changeLanguage('en-US') await i18n.changeLanguage('en-US')
const record = makeRecord(1, 'running') const record = makeRecord(1, 'running')
render( render(
@@ -102,19 +102,67 @@ describe('ActivityPanel', () => {
}).format(new Date(record.createdAt)) }).format(new Date(record.createdAt))
expect(screen.getAllByText(englishDate).length).toBeGreaterThan(0) expect(screen.getAllByText(englishDate).length).toBeGreaterThan(0)
expect( expect(
screen.getByRole('heading', { screen.getByRole('heading', { level: 1, name: 'Run history' })
level: 1,
name: 'Tasks and activity'
})
).toBeInTheDocument() ).toBeInTheDocument()
expect( expect(
screen.getByRole('button', { name: 'In progress' }) screen.getByRole('tab', { name: 'Tasks and conversations' })
).toBeInTheDocument() ).toHaveAttribute('aria-selected', 'true')
expect(screen.getByText(record.title)).toBeInTheDocument() expect(screen.getByText(record.title)).toBeInTheDocument()
expect(screen.queryByText('Token usage')).not.toBeInTheDocument()
fireEvent.click(
screen.getByRole('tab', { name: 'Usage analytics' })
)
expect(screen.getByText('Token usage')).toBeInTheDocument() expect(screen.getByText('Token usage')).toBeInTheDocument()
expect(screen.queryByText(record.title)).not.toBeInTheDocument()
}) })
it('filters active and unsuccessful activity and opens its conversation', () => { it('organizes records by project, conversation, and detail', () => {
const request: ActivityRecord = {
...makeRecord(1, 'running'),
kind: 'request',
title: '分析登录问题',
scope: {
kind: 'project',
projectId: 'project-1',
projectName: '项目甲'
}
}
const tool: ActivityRecord = {
...makeRecord(2),
conversationId: request.conversationId,
requestId: request.requestId,
title: '搜索 IPC 代码',
scope: request.scope
}
const { container } = render(
<ActivityPanel
onClear={vi.fn()}
onOpenConversation={vi.fn()}
records={[request, tool, makeRecord(3)]}
tokenUsage={makeTokenUsage()}
/>
)
expect(container.querySelectorAll('.activity-project')).toHaveLength(2)
expect(screen.getByText('项目:项目甲')).toBeInTheDocument()
expect(
screen.getByText('1 个任务或会话 · 2 条活动')
).toBeInTheDocument()
const projectConversation = screen
.getByText('对话:分析登录问题')
.closest('details')
expect(projectConversation).not.toBeNull()
expect(projectConversation).not.toHaveAttribute('open')
fireEvent.click(screen.getByText('对话:分析登录问题'))
expect(projectConversation).toHaveAttribute('open')
expect(
projectConversation?.querySelectorAll('article')
).toHaveLength(2)
})
it('filters active and exceptional activity and opens its conversation', () => {
const onOpenConversation = vi.fn() const onOpenConversation = vi.fn()
render( render(
<ActivityPanel <ActivityPanel
@@ -131,16 +179,15 @@ describe('ActivityPanel', () => {
) )
fireEvent.click(screen.getByRole('button', { name: '进行中' })) fireEvent.click(screen.getByRole('button', { name: '进行中' }))
fireEvent.click(screen.getByText('对话:活动 1')) expect(screen.getByText('对话:活动 1')).toBeInTheDocument()
expect(screen.getByText('活动 1')).toBeInTheDocument()
expect(screen.queryByText('活动 2')).not.toBeInTheDocument() expect(screen.queryByText('活动 2')).not.toBeInTheDocument()
fireEvent.click(screen.getByRole('button', { name: '失败' })) fireEvent.click(screen.getByRole('button', { name: '异常' }))
fireEvent.click(screen.getByText('对话:活动 2')) expect(screen.getByText('对话:活动 2')).toBeInTheDocument()
expect(screen.getByText('活动 2')).toBeInTheDocument() expect(screen.getByText('对话:活动 3')).toBeInTheDocument()
expect(screen.getByText('活动 3')).toBeInTheDocument()
expect(screen.queryByText('活动 1')).not.toBeInTheDocument() expect(screen.queryByText('活动 1')).not.toBeInTheDocument()
fireEvent.click(screen.getByText('对话:活动 2'))
fireEvent.click( fireEvent.click(
screen.getAllByRole('button', { name: '打开所属对话' })[0]! screen.getAllByRole('button', { name: '打开所属对话' })[0]!
) )
@@ -236,35 +283,7 @@ describe('ActivityPanel', () => {
expect(within(item).getByText('进行中')).toBeInTheDocument() expect(within(item).getByText('进行中')).toBeInTheDocument()
}) })
it('groups activity by conversation in collapsible sections', () => { it('shows immutable project and unavailable scope snapshots', () => {
const first = makeRecord(1, 'running')
const second = {
...makeRecord(2, 'failed'),
conversationId: first.conversationId
}
const { container } = render(
<ActivityPanel
onClear={vi.fn()}
onOpenConversation={vi.fn()}
records={[first, second]}
tokenUsage={makeTokenUsage()}
/>
)
const groups =
container.querySelectorAll<HTMLDetailsElement>(
'details.activity-group'
)
expect(groups).toHaveLength(1)
expect(groups[0]).not.toHaveAttribute('open')
expect(within(groups[0]!).getByText('2 条活动')).toBeInTheDocument()
fireEvent.click(within(groups[0]!).getByText('对话:活动 1'))
expect(groups[0]).toHaveAttribute('open')
expect(groups[0]!.querySelectorAll('article')).toHaveLength(2)
})
it('shows immutable scope snapshots on groups and records', () => {
const projectRecord: ActivityRecord = { const projectRecord: ActivityRecord = {
...makeRecord(1), ...makeRecord(1),
scope: { scope: {
@@ -286,11 +305,11 @@ describe('ActivityPanel', () => {
/> />
) )
expect(screen.getAllByText('项目:项目甲')).toHaveLength(2) expect(screen.getByText('项目:项目甲')).toBeInTheDocument()
expect(screen.getAllByText('范围不可用')).toHaveLength(2) expect(screen.getByText('范围不可用')).toBeInTheDocument()
}) })
it('uses the shared page hierarchy and explicit global scope', () => { it('uses shared page tabs and explicit global scope', () => {
render( render(
<ActivityPanel <ActivityPanel
onClear={vi.fn()} onClear={vi.fn()}
@@ -301,15 +320,102 @@ describe('ActivityPanel', () => {
) )
expect( expect(
screen.getByRole('heading', { level: 1, name: '任务与活动' }) screen.getByRole('heading', { level: 1, name: '运行记录' })
).toBeInTheDocument() ).toBeInTheDocument()
expect(screen.getByText('全部项目')).toHaveClass('scope-badge') expect(screen.getByText('全部项目')).toHaveClass('scope-badge')
expect(screen.getByLabelText('Token 用量分组')).toHaveClass( expect(screen.getByLabelText('运行记录视图')).toHaveClass(
'segmented-control' 'page-tabs'
) )
expect(
screen.getByRole('tabpanel', { name: '任务与会话' })
).toBeInTheDocument()
expect(screen.getByLabelText('筛选活动')).toHaveClass( expect(screen.getByLabelText('筛选活动')).toHaveClass(
'segmented-control' 'segmented-control'
) )
expect(
screen.queryByLabelText('Token 用量分组')
).not.toBeInTheDocument()
})
it('renders shared-time project and conversation activity tracks', () => {
const first: ActivityRecord = {
...makeRecord(1, 'running'),
kind: 'request',
title: '分析登录问题',
scope: {
kind: 'project',
projectId: 'project-1',
projectName: '项目甲'
}
}
const second: ActivityRecord = {
...makeRecord(2, 'failed'),
conversationId: first.conversationId,
requestId: first.requestId,
scope: first.scope
}
const third: ActivityRecord = {
...makeRecord(3),
conversationId: 'conversation-parallel',
requestId: 'request-parallel',
kind: 'subagent',
title: '研究专家',
scope: first.scope
}
const fourth: ActivityRecord = {
...makeRecord(4),
conversationId: first.conversationId,
requestId: first.requestId,
kind: 'result',
title: '任务执行完成',
scope: first.scope
}
render(
<ActivityPanel
onClear={vi.fn()}
onOpenConversation={vi.fn()}
records={[first, second, third, fourth]}
tokenUsage={makeTokenUsage()}
/>
)
fireEvent.click(screen.getByRole('tab', { name: '活动时间线' }))
const timeline = screen.getByLabelText(
'按项目和会话分组的并行活动轨道'
)
expect(timeline).toHaveClass('activity-tracks')
expect(
timeline.querySelectorAll('.activity-track')
).toHaveLength(2)
expect(
timeline.querySelectorAll('.activity-track__node')
).toHaveLength(4)
expect(within(timeline).getByText('分析登录问题')).toBeInTheDocument()
expect(
within(timeline).getAllByText('研究专家').length
).toBeGreaterThan(0)
expect(within(timeline).getAllByText('用户').length).toBeGreaterThan(0)
expect(
within(timeline).getAllByText('GoodBuddy').length
).toBeGreaterThan(0)
expect(within(timeline).getByText('项目:项目甲')).toBeInTheDocument()
expect(
within(timeline).getByText(
'所有轨道共享同一执行顺序,节点按发生时间依次展开,点击节点查看身份和活动详情。'
)
).toBeInTheDocument()
fireEvent.click(
screen.getByRole('button', {
name: / 2/u
})
)
const detail = screen.getByLabelText('选中的活动节点详情')
expect(within(detail).getByText('活动 2')).toBeInTheDocument()
expect(
within(detail).getByText('对话:分析登录问题')
).toBeInTheDocument()
expect(within(detail).getByText('项目:项目甲')).toBeInTheDocument()
}) })
it('never renders more than 500 records', () => { it('never renders more than 500 records', () => {
@@ -339,11 +445,10 @@ describe('ActivityPanel', () => {
tokenUsage={makeTokenUsage()} tokenUsage={makeTokenUsage()}
/> />
) )
fireEvent.click(screen.getByRole('tab', { name: '用量统计' }))
const stats = screen.getByLabelText('Token 用量统计') const stats = screen.getByLabelText('Token 用量统计')
expect( expect(within(stats).getByText('150')).toBeInTheDocument()
within(stats).getByText('150')
).toBeInTheDocument()
const projectRow = screen.getByRole('row', { const projectRow = screen.getByRole('row', {
name: '项目甲gpt-5 · openai 100 20 10 40 120' name: '项目甲gpt-5 · openai 100 20 10 40 120'
@@ -354,7 +459,7 @@ describe('ActivityPanel', () => {
).not.toBeInTheDocument() ).not.toBeInTheDocument()
}) })
it('groups token usage and displays fallback labels', () => { it('groups token usage by project, conversation, and model', () => {
render( render(
<ActivityPanel <ActivityPanel
onClear={vi.fn()} onClear={vi.fn()}
@@ -363,6 +468,7 @@ describe('ActivityPanel', () => {
tokenUsage={makeTokenUsage()} tokenUsage={makeTokenUsage()}
/> />
) )
fireEvent.click(screen.getByRole('tab', { name: '用量统计' }))
expect(screen.getByText('项目甲')).toBeInTheDocument() expect(screen.getByText('项目甲')).toBeInTheDocument()
expect(screen.getByText('未归属项目')).toBeInTheDocument() expect(screen.getByText('未归属项目')).toBeInTheDocument()
File diff suppressed because it is too large Load Diff
+7 -4
View File
@@ -3067,10 +3067,13 @@ describe('App', () => {
throw new Error('Missing request') throw new Error('Missing request')
} }
fireEvent.click(screen.getByText('任务与活动')) fireEvent.click(screen.getByText('运行记录'))
fireEvent.click(
await screen.findByRole('tab', { name: '用量统计' })
)
const stats = await screen.findByLabelText('Token 用量统计') const stats = await screen.findByLabelText('Token 用量统计')
expect( expect(
screen.getByRole('heading', { level: 1, name: '任务与活动' }) screen.getByRole('heading', { level: 1, name: '运行记录' })
).toBeInTheDocument() ).toBeInTheDocument()
expect( expect(
screen.queryByRole('button', { name: /^/u }) screen.queryByRole('button', { name: /^/u })
@@ -3841,7 +3844,7 @@ describe('App', () => {
) )
expect(cancelledDot).toHaveClass('message__status-dot') expect(cancelledDot).toHaveClass('message__status-dot')
expect(cancelledDot).not.toHaveClass('message__status-dot--active') expect(cancelledDot).not.toHaveClass('message__status-dot--active')
fireEvent.click(screen.getByText('任务与活动')) fireEvent.click(screen.getByText('运行记录'))
expect((await screen.findAllByText('已取消')).length).toBeGreaterThan(0) expect((await screen.findAllByText('已取消')).length).toBeGreaterThan(0)
fireEvent.click(screen.getByRole('button', { name: '进行中' })) fireEvent.click(screen.getByRole('button', { name: '进行中' }))
expect( expect(
@@ -5170,7 +5173,7 @@ describe('App', () => {
expect(within(statusRegion).getByText('已取消')).toBeInTheDocument() expect(within(statusRegion).getByText('已取消')).toBeInTheDocument()
expect(within(statusRegion).getByText('父任务已停止')).toBeInTheDocument() expect(within(statusRegion).getByText('父任务已停止')).toBeInTheDocument()
fireEvent.click(screen.getByText('任务与活动')) fireEvent.click(screen.getByText('运行记录'))
expect(await screen.findAllByText('子专家')).toHaveLength(4) expect(await screen.findAllByText('子专家')).toHaveLength(4)
expect(screen.getAllByText(//u).length).toBeGreaterThan(0) expect(screen.getAllByText(//u).length).toBeGreaterThan(0)
expect(screen.getAllByText(//u).length).toBeGreaterThan(0) expect(screen.getAllByText(//u).length).toBeGreaterThan(0)
@@ -3,10 +3,16 @@ import type { activity as chineseActivity } from '../zh-CN/activity'
export const activity = { export const activity = {
header: { header: {
eyebrow: 'ACTIVITY AUDIT', eyebrow: 'RUN HISTORY',
title: 'Tasks and activity', title: 'Run history',
description: description:
'View task requests, child experts, tool calls, approval results, and token usage across all projects.' 'Review execution details by project, task, and conversation, or browse the activity timeline and model usage.'
},
tabs: {
ariaLabel: 'Run history views',
tasks: 'Tasks and conversations',
timeline: 'Activity timeline',
usage: 'Usage analytics'
}, },
clear: { clear: {
confirmAriaLabel: 'Confirm clearing activity history', confirmAriaLabel: 'Confirm clearing activity history',
@@ -34,7 +40,7 @@ export const activity = {
filters: { filters: {
all: 'All', all: 'All',
active: 'In progress', active: 'In progress',
failed: 'Failed', failed: 'Exceptions',
ariaLabel: 'Filter activity', ariaLabel: 'Filter activity',
clear: 'Clear filter' clear: 'Clear filter'
}, },
@@ -69,7 +75,32 @@ export const activity = {
ariaLabel: 'Activity totals', ariaLabel: 'Activity totals',
all: 'All', all: 'All',
active: 'In progress', active: 'In progress',
failed: 'Failed' failed: 'Exceptions'
},
timeline: {
ariaLabel: 'Parallel activity tracks grouped by project and conversation',
description:
'Every track shares the same execution order. Nodes are spread by event time. Select a node to inspect its identity and details.',
lanes: 'Project / conversation',
laneAriaLabel: 'Activity track for conversation {{title}}',
nodeAriaLabel: '{{actor}}, {{kind}}, {{status}}, {{title}}, {{time}}',
legendAriaLabel: 'Activity node identity legend',
detailAriaLabel: 'Selected activity node details',
closeDetail: 'Close details',
actors: {
user: 'User',
assistant: 'GoodBuddy',
subagent: 'Child expert',
tool: 'Tool',
approval: 'Approval'
},
nodes: {
user: 'U',
assistant: 'G',
tool: 'T',
approval: 'A',
subagent: 'S'
}
}, },
empty: { empty: {
active: 'There is no pending or running activity.', active: 'There is no pending or running activity.',
@@ -87,6 +118,8 @@ export const activity = {
'The scope could not be determined when this activity was recorded.', 'The scope could not be determined when this activity was recorded.',
conversation: 'Conversation: {{title}}', conversation: 'Conversation: {{title}}',
activityCount: 'Activity items: {{formattedCount}}', activityCount: 'Activity items: {{formattedCount}}',
projectSummary:
'{{conversationCount}} tasks or conversations · {{activityCount}} activity items',
openConversation: 'Open conversation' openConversation: 'Open conversation'
} }
} satisfies TranslationShape<typeof chineseActivity> } satisfies TranslationShape<typeof chineseActivity>
+1 -1
View File
@@ -46,7 +46,7 @@ export const app = {
magicNotes: 'Magic Notes', magicNotes: 'Magic Notes',
knowledge: 'Knowledge', knowledge: 'Knowledge',
heartbeat: 'Smart Heartbeat', heartbeat: 'Smart Heartbeat',
activity: 'Tasks & Activity', activity: 'Run history',
pendingSuggestions: '{{count}} pending suggestions' pendingSuggestions: '{{count}} pending suggestions'
}, },
route: { route: {
@@ -1,9 +1,15 @@
export const activity = { export const activity = {
header: { header: {
eyebrow: 'ACTIVITY AUDIT', eyebrow: 'RUN HISTORY',
title: '任务与活动', title: '运行记录',
description: description:
'查看全部项目中的任务请求、子专家、工具调用、审批结果和 Token 用量。' '按项目、任务与会话查看执行详情,或浏览活动时间线和模型用量。'
},
tabs: {
ariaLabel: '运行记录视图',
tasks: '任务与会话',
timeline: '活动时间线',
usage: '用量统计'
}, },
clear: { clear: {
confirmAriaLabel: '确认清空 {{formattedCount}} 条活动记录', confirmAriaLabel: '确认清空 {{formattedCount}} 条活动记录',
@@ -31,7 +37,7 @@ export const activity = {
filters: { filters: {
all: '全部', all: '全部',
active: '进行中', active: '进行中',
failed: '失败', failed: '异常',
ariaLabel: '筛选活动', ariaLabel: '筛选活动',
clear: '清除筛选' clear: '清除筛选'
}, },
@@ -66,7 +72,32 @@ export const activity = {
ariaLabel: '活动统计', ariaLabel: '活动统计',
all: '全部', all: '全部',
active: '进行中', active: '进行中',
failed: '失败' failed: '异常'
},
timeline: {
ariaLabel: '按项目和会话分组的并行活动轨道',
description:
'所有轨道共享同一执行顺序,节点按发生时间依次展开,点击节点查看身份和活动详情。',
lanes: '项目 / 会话',
laneAriaLabel: '会话 {{title}} 的活动轨道',
nodeAriaLabel: '{{actor}}{{kind}}{{status}}{{title}}{{time}}',
legendAriaLabel: '活动节点身份图例',
detailAriaLabel: '选中的活动节点详情',
closeDetail: '关闭详情',
actors: {
user: '用户',
assistant: 'GoodBuddy',
subagent: '子专家',
tool: '工具',
approval: '审批'
},
nodes: {
user: '用',
assistant: 'G',
tool: '工',
approval: '审',
subagent: '专'
}
}, },
empty: { empty: {
active: '当前没有等待中或正在运行的活动。', active: '当前没有等待中或正在运行的活动。',
@@ -83,6 +114,8 @@ export const activity = {
'创建此活动记录时未能确定其归属范围。', '创建此活动记录时未能确定其归属范围。',
conversation: '对话:{{title}}', conversation: '对话:{{title}}',
activityCount: '{{formattedCount}} 条活动', activityCount: '{{formattedCount}} 条活动',
projectSummary:
'{{conversationCount}} 个任务或会话 · {{activityCount}} 条活动',
openConversation: '打开所属对话' openConversation: '打开所属对话'
} }
} as const } as const
+1 -1
View File
@@ -42,7 +42,7 @@ export const app = {
magicNotes: '魔法笔记', magicNotes: '魔法笔记',
knowledge: '知识库', knowledge: '知识库',
heartbeat: '智能心跳', heartbeat: '智能心跳',
activity: '任务与活动', activity: '运行记录',
pendingSuggestions: '{{count}} 条待处理建议' pendingSuggestions: '{{count}} 条待处理建议'
}, },
route: { route: {
+404 -11
View File
@@ -8445,6 +8445,16 @@ details.settings-section > :not(summary) + :not(summary) {
width: 100%; width: 100%;
min-height: 100%; min-height: 100%;
flex-direction: column; flex-direction: column;
container: activity-panel / inline-size;
}
.activity-panel > .page-tabs {
padding: var(--space-4) 0;
}
.activity-panel__tab-panel {
min-width: 0;
padding-bottom: var(--space-6);
} }
.activity-list { .activity-list {
@@ -8458,7 +8468,6 @@ details.settings-section > :not(summary) + :not(summary) {
padding: 14px; padding: 14px;
border: 1px solid var(--border-default); border: 1px solid var(--border-default);
border-radius: var(--radius-card); border-radius: var(--radius-card);
margin-top: var(--space-4);
background: var(--surface-raised); background: var(--surface-raised);
} }
@@ -8468,11 +8477,11 @@ details.settings-section > :not(summary) + :not(summary) {
gap: 12px; gap: 12px;
} }
.token-usage__header h3 { .token-usage__header h2 {
flex: 1; flex: 1;
margin: 0; margin: 0;
color: var(--text-primary); color: var(--text-primary);
font-size: 13px; font-size: var(--font-section-title);
} }
.token-usage__stats { .token-usage__stats {
@@ -8561,7 +8570,7 @@ details.settings-section > :not(summary) + :not(summary) {
.activity-panel__stats { .activity-panel__stats {
display: grid; display: grid;
margin: 18px 0 14px; margin: 0 0 var(--space-3);
gap: 9px; gap: 9px;
grid-template-columns: repeat(3, 1fr); grid-template-columns: repeat(3, 1fr);
} }
@@ -8585,22 +8594,54 @@ details.settings-section > :not(summary) + :not(summary) {
font-weight: 700; font-weight: 700;
} }
.activity-panel__filters { .activity-panel__toolbar {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
margin-bottom: 12px; align-items: flex-start;
gap: 6px; justify-content: space-between;
margin-bottom: var(--space-3);
gap: var(--space-3);
} }
.activity-groups { .activity-projects {
display: grid; display: grid;
gap: var(--space-4);
}
.activity-project {
overflow: clip;
border: 1px solid var(--border-default);
border-radius: var(--radius-card);
background: var(--surface-raised);
}
.activity-project__header {
display: flex;
min-height: 44px;
align-items: center;
justify-content: space-between;
padding: var(--space-2) var(--space-4);
border-bottom: 1px solid var(--border-subtle);
background: var(--surface-subtle);
gap: var(--space-3); gap: var(--space-3);
} }
.activity-project__header > span:last-child {
color: var(--text-muted);
font-size: var(--font-caption);
}
.activity-conversations {
display: grid;
padding: var(--space-3);
background: var(--surface-canvas);
gap: var(--space-2);
}
.activity-group { .activity-group {
overflow: clip; overflow: clip;
border: 1px solid var(--border-default); border: 1px solid var(--border-default);
border-radius: var(--radius-card); border-radius: var(--radius-control);
background: var(--surface-raised); background: var(--surface-raised);
} }
@@ -8648,11 +8689,15 @@ details.settings-section > :not(summary) + :not(summary) {
.activity-group > .activity-list { .activity-group > .activity-list {
display: grid; display: grid;
padding: var(--space-3); padding: var(--space-3);
background: var(--surface-canvas); border-top: 1px solid var(--border-subtle);
gap: var(--space-2); gap: var(--space-2);
} }
.activity-group .activity-item { .activity-list__item {
list-style: none;
}
.activity-list__item .activity-item {
margin-bottom: 0; margin-bottom: 0;
} }
@@ -8725,6 +8770,23 @@ details.settings-section > :not(summary) + :not(summary) {
white-space: pre-wrap; white-space: pre-wrap;
} }
.activity-item__context {
display: flex;
min-width: 0;
flex-wrap: wrap;
align-items: center;
margin-top: var(--space-2);
color: var(--text-muted);
font-size: var(--font-caption);
gap: var(--space-2);
}
.activity-item__context > span:first-child {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.activity-item__conversation { .activity-item__conversation {
padding: 5px 0; padding: 5px 0;
margin-top: 7px; margin-top: 7px;
@@ -8735,6 +8797,305 @@ details.settings-section > :not(summary) + :not(summary) {
font-weight: 650; font-weight: 650;
} }
.activity-tracks {
display: grid;
min-width: 0;
gap: var(--space-3);
}
.activity-tracks__description {
padding: var(--space-2) var(--space-3);
border-radius: var(--radius-control);
margin: 0;
background: var(--surface-subtle);
color: var(--text-secondary);
font-size: var(--font-caption);
line-height: 1.5;
}
.activity-tracks__legend {
display: flex;
padding: 0;
margin: 0;
color: var(--text-secondary);
flex-wrap: wrap;
font-size: var(--font-caption);
gap: var(--space-3);
list-style: none;
}
.activity-tracks__legend li {
display: inline-flex;
align-items: center;
gap: var(--space-1);
}
.activity-tracks__legend-swatch {
width: 10px;
height: 10px;
border-radius: 50%;
background: var(--accent-solid);
}
.activity-tracks__legend-swatch--assistant {
background: var(--success);
}
.activity-tracks__legend-swatch--subagent {
background: var(--warning);
}
.activity-tracks__legend-swatch--tool {
background: color-mix(
in srgb,
var(--accent) 55%,
var(--success)
);
}
.activity-tracks__legend-swatch--approval {
border-radius: 3px;
background: color-mix(
in srgb,
var(--warning) 65%,
var(--danger)
);
}
.activity-tracks__scroll {
max-width: 100%;
border: 1px solid var(--border-default);
border-radius: var(--radius-card);
overflow-x: auto;
background: var(--surface-raised);
}
.activity-tracks__canvas {
width: 100%;
min-width: 760px;
}
.activity-tracks__axis {
display: grid;
min-height: 38px;
align-items: center;
padding: 0 var(--space-3);
border-bottom: 1px solid var(--border-subtle);
color: var(--text-muted);
font-size: var(--font-caption);
grid-template-columns: 210px minmax(0, 1fr) auto;
}
.activity-tracks__axis strong {
position: sticky;
z-index: 3;
left: var(--space-3);
padding-right: var(--space-3);
background: var(--surface-raised);
color: var(--text-secondary);
}
.activity-tracks__axis span:nth-child(2) {
padding-left: var(--space-3);
}
.activity-tracks__project > header {
display: flex;
min-height: 38px;
align-items: center;
justify-content: space-between;
padding: var(--space-2) var(--space-3);
border-bottom: 1px solid var(--border-subtle);
background: var(--surface-subtle);
gap: var(--space-3);
}
.activity-tracks__project > header > span:last-child {
color: var(--text-muted);
font-size: var(--font-caption);
}
.activity-track {
display: grid;
min-height: 84px;
border-bottom: 1px solid var(--border-subtle);
grid-template-columns: 210px minmax(0, 1fr);
}
.activity-tracks__project:last-child .activity-track:last-child {
border-bottom: 0;
}
.activity-track__label {
position: sticky;
z-index: 2;
left: 0;
display: grid;
min-width: 0;
align-content: center;
padding: var(--space-2) var(--space-3);
border-right: 1px solid var(--border-subtle);
background: var(--surface-raised);
gap: var(--space-1);
}
.activity-track__label > strong {
overflow: hidden;
color: var(--text-primary);
font-size: var(--font-body);
text-overflow: ellipsis;
white-space: nowrap;
}
.activity-track__label > span {
display: flex;
align-items: center;
color: var(--text-muted);
font-size: var(--font-caption);
gap: var(--space-2);
}
.activity-track__rail {
position: relative;
min-width: 0;
background-image: linear-gradient(
to right,
var(--border-subtle) 1px,
transparent 1px
);
background-size: 20% 100%;
}
.activity-track__rail::before {
position: absolute;
top: 50%;
right: var(--space-3);
left: var(--space-3);
height: 2px;
background: var(--border-default);
content: '';
transform: translateY(-50%);
}
.activity-track__node {
position: absolute;
z-index: 1;
top: 50%;
display: flex;
width: 68px;
height: 58px;
padding: 0;
border: 0;
background: transparent;
color: var(--text-secondary);
cursor: pointer;
align-items: center;
flex-direction: column;
font-size: 10px;
gap: 2px;
transform: translate(-50%, -50%);
}
.activity-track__node:hover {
z-index: 3;
color: var(--text-primary);
transform: translate(-50%, -50%) scale(1.05);
}
.activity-track__node:focus-visible {
z-index: 4;
outline: 2px solid var(--accent);
outline-offset: 2px;
}
.activity-track__node > span {
display: grid;
width: 32px;
height: 32px;
border: 2px solid var(--surface-raised);
border-radius: 50%;
background: var(--accent-solid);
color: var(--text-on-accent);
font-weight: 750;
place-items: center;
}
.activity-track__node > small {
display: block;
width: 100%;
overflow: hidden;
font-size: 9px;
line-height: 1.2;
text-align: center;
text-overflow: ellipsis;
white-space: nowrap;
}
.activity-track__node[aria-pressed='true'] > span {
box-shadow: 0 0 0 3px var(--accent-selected);
}
.activity-track__node--actor-assistant > span {
background: var(--success);
}
.activity-track__node--actor-subagent > span {
background: var(--warning);
color: var(--text-primary);
}
.activity-track__node--actor-tool > span {
background: color-mix(
in srgb,
var(--accent) 55%,
var(--success)
);
}
.activity-track__node--actor-approval > span {
border-radius: 6px;
background: color-mix(
in srgb,
var(--warning) 65%,
var(--danger)
);
}
.activity-track__node--running > span {
box-shadow: 0 0 0 3px var(--accent-selected);
}
.activity-track__node--failed > span,
.activity-track__node--denied > span,
.activity-track__node--cancelled > span,
.activity-track__node--interrupted > span {
border-color: var(--danger);
box-shadow: 0 0 0 2px var(--danger-subtle);
}
.activity-track__node--pending > span {
border-color: var(--accent);
background: var(--surface-raised);
color: var(--accent);
}
.activity-tracks__detail {
display: grid;
padding: var(--space-3);
border: 1px solid var(--border-default);
border-radius: var(--radius-card);
background: var(--surface-subtle);
gap: var(--space-2);
}
.activity-tracks__detail > .secondary-button {
justify-self: end;
}
.activity-tracks__detail .activity-item {
margin: 0;
}
.heartbeat-center { .heartbeat-center {
display: flex; display: flex;
width: 100%; width: 100%;
@@ -9820,6 +10181,9 @@ details.settings-section > :not(summary) + :not(summary) {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
}
@container activity-panel (max-width: 720px) {
.activity-group > summary { .activity-group > summary {
grid-template-columns: minmax(0, 1fr) auto; grid-template-columns: minmax(0, 1fr) auto;
} }
@@ -9827,6 +10191,35 @@ details.settings-section > :not(summary) + :not(summary) {
.activity-group > summary time { .activity-group > summary time {
grid-column: 1 / -1; grid-column: 1 / -1;
} }
.activity-panel__toolbar {
align-items: stretch;
flex-direction: column;
}
.activity-panel__toolbar > .segmented-control {
width: fit-content;
}
}
@container activity-panel (max-width: 480px) {
.activity-panel__stats {
grid-template-columns: 1fr;
}
.activity-project__header {
align-items: flex-start;
flex-direction: column;
}
.activity-conversations {
padding: var(--space-2);
}
.activity-item__context > span:first-child {
width: 100%;
white-space: normal;
}
} }
@container heartbeat-center (max-width: 620px) { @container heartbeat-center (max-width: 620px) {