From b16fc900bb239eb7b23c46c4be8ab1b198fdcdc6 Mon Sep 17 00:00:00 2001 From: mesalogo Date: Wed, 23 Sep 2026 21:52:36 +0800 Subject: [PATCH] fix: restore supervision feedback sidebar layout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace unstyled full-width pin and refresh controls with shared icon buttons in the feedback header. Show conversation and task titles instead of raw IDs, with responsive spacing and keyboard focus coverage. Release note: 修复任务中心监督反馈按钮样式和布局,显示会话或任务名称,并改善窄侧栏下的阅读与操作体验。 --- .../src/RightAssistantSidebar.resize.test.tsx | 30 +++++++++++-- src/renderer/src/RightAssistantSidebar.tsx | 31 ++++++++----- src/renderer/src/SupervisionCard.test.tsx | 11 +++++ src/renderer/src/task-center.css | 34 ++++++++++++++ tests/supervisor-layout.electron.test.ts | 6 ++- tests/support/supervisor-layout-driver.mjs | 45 +++++++++++++++++++ .../support/supervisor-layout-regression.tsx | 13 +++++- 7 files changed, 153 insertions(+), 17 deletions(-) diff --git a/src/renderer/src/RightAssistantSidebar.resize.test.tsx b/src/renderer/src/RightAssistantSidebar.resize.test.tsx index 3d8c3f35..a8ed2d3b 100644 --- a/src/renderer/src/RightAssistantSidebar.resize.test.tsx +++ b/src/renderer/src/RightAssistantSidebar.resize.test.tsx @@ -151,6 +151,7 @@ function sidebarElement({ workspaceProjectId, restoreFocusRef, activeConversationId, + conversationTitles = new Map(), supervisionEnabled, supervisionLibraries, conversationStats, @@ -191,6 +192,7 @@ function sidebarElement({ workspaceProjectId?: string restoreFocusRef?: { current: HTMLElement | null } activeConversationId?: string + conversationTitles?: ReadonlyMap supervisionEnabled?: boolean supervisionLibraries?: React.ComponentProps['supervisionLibraries'] conversationStats?: React.ComponentProps['conversationStats'] @@ -221,7 +223,7 @@ function sidebarElement({ browserStates={browserStates} schedules={[]} tasks={tasks} - conversationTitles={new Map()} + conversationTitles={conversationTitles} currentProject={activeProject ?? undefined} projectNames={new Map()} onCreateCustomTask={onCreateCustomTask} @@ -286,15 +288,37 @@ it.each([false, undefined])('does not mount or query supervision when enabled is it('pins the supervision target across conversation switches and restores following when unpinned', async () => { const overview = vi.fn(async () => []) Object.assign(window.goodbuddy, { supervision: { overview } }) - const view = render(sidebarElement({ activeConversationId: 'A', supervisionEnabled: true })) + const conversationTitles = new Map([['A', '会话甲'], ['B', '会话乙']]) + const view = render(sidebarElement({ activeConversationId: 'A', conversationTitles, supervisionEnabled: true })) + expect(screen.getByText('会话:会话甲')).toBeVisible() + const card = within(screen.getByRole('region', { name: '监督反馈' })) + expect(card.getByRole('button', { name: '固定监督目标' })).toHaveAttribute('aria-pressed', 'false') await waitFor(() => expect(overview).toHaveBeenLastCalledWith({ target: { type: 'conversation', conversationId: 'A' } })) fireEvent.click(screen.getByRole('button', { name: '固定监督目标' })) - view.rerender(sidebarElement({ activeConversationId: 'B', supervisionEnabled: true })) + view.rerender(sidebarElement({ activeConversationId: 'B', conversationTitles, supervisionEnabled: true })) + expect(screen.getByText('已固定 · 会话:会话甲')).toBeVisible() + expect(card.getByRole('button', { name: '取消固定监督目标' })).toHaveAttribute('aria-pressed', 'true') expect(screen.getByRole('button', { name: '取消固定监督目标' })).toBeInTheDocument() expect(overview).toHaveBeenLastCalledWith({ target: { type: 'conversation', conversationId: 'A' } }) expect(localStorage.getItem('goodbuddy.workbar-layout.v1')).toContain('"conversationId":"A"') fireEvent.click(screen.getByRole('button', { name: '取消固定监督目标' })) await waitFor(() => expect(overview).toHaveBeenLastCalledWith({ target: { type: 'conversation', conversationId: 'B' } })) + expect(screen.getByText('会话:会话乙')).toBeVisible() +}) + +it('shows the task name while following and pinned, without displaying its raw ID', async () => { + const overview = vi.fn(async () => []) + Object.assign(window.goodbuddy, { supervision: { overview } }) + const tasks = [{ id: 'task-uuid', title: '核对交付清单', conversationId: 'A' }] as AssistantTask[] + const view = render(sidebarElement({ tasks, selectedTaskId: 'task-uuid', supervisionEnabled: true })) + expect(screen.getByText('任务:核对交付清单')).toHaveAttribute('title', 'task-uuid') + fireEvent.click(screen.getByRole('button', { name: '固定监督目标' })) + view.rerender(sidebarElement({ tasks, activeConversationId: 'B', supervisionEnabled: true })) + expect(screen.getByText('已固定 · 任务:核对交付清单')).toBeVisible() + const callsBeforeRefresh = overview.mock.calls.length + fireEvent.click(screen.getByRole('button', { name: '刷新监督回顾' })) + await waitFor(() => expect(overview).toHaveBeenCalledTimes(callsBeforeRefresh + 1)) + await waitFor(() => expect(overview).toHaveBeenLastCalledWith({ target: { type: 'task', taskId: 'task-uuid' } })) }) it.each(['cancel', 'failure'] as const)('retries supervision knowledge preview after %s and shows returned content', async (outcome) => { diff --git a/src/renderer/src/RightAssistantSidebar.tsx b/src/renderer/src/RightAssistantSidebar.tsx index 29e25e94..f84c89e9 100644 --- a/src/renderer/src/RightAssistantSidebar.tsx +++ b/src/renderer/src/RightAssistantSidebar.tsx @@ -9,6 +9,7 @@ import { FileText, Maximize2, Minimize2, + Pin, Plus, RefreshCw, ShieldAlert, @@ -216,6 +217,9 @@ type SupervisionCardProps = { target?: SupervisionTarget activeConversationId?: string conversationTitle?: string + taskTitle?: string + pinned?: boolean + onTogglePinned?: () => void supervisionLibraries?: KnowledgeLibrary[] onContinueSupervision?: (prompt: string, conversationId: string) => Promise onOpenSupervisionConversation?: (conversationId: string) => void @@ -229,6 +233,9 @@ function SupervisionCardContent({ target, activeConversationId, conversationTitle, + taskTitle, + pinned = false, + onTogglePinned, supervisionLibraries: libraries, onContinueSupervision: onContinue, onOpenSupervisionGraph, @@ -310,9 +317,14 @@ function SupervisionCardContent({ } return
-

监督反馈

- {target &&

{target.type === 'conversation' ? `会话:${conversationTitle ?? target.conversationId}` : `任务:${target.taskId}`}

} - +
+

监督反馈

+
+ {onTogglePinned && } + +
+
+ {target &&

{pinned ? '已固定 · ' : ''}{target.type === 'conversation' ? `会话:${conversationTitle?.trim() || '未命名会话'}` : `任务:${taskTitle?.trim() || '未命名任务'}`}

} {!result ?

{message || '暂无监督回顾'}

: <>

{result.summary || '监督回顾没有摘要'}

{result.scope.kind === 'global' ? '全局回顾' : `项目范围:${result.scope.projectIds.join('、')}`} · {new Date(result.timeRange.from).toLocaleString()} – {new Date(result.timeRange.to).toLocaleString()}

@@ -2071,19 +2083,18 @@ export function RightAssistantSidebar({ {instance.appId === 'tasks' && (
{supervisionEnabled && <> - setWorkbarInstances((current) => current.map((item) => item.id === instance.id + ? { ...item, targetRef: item.targetRef ? undefined : selectedTaskId ? { type: 'task', taskId: selectedTaskId } : activeConversationId ? { type: 'conversation', conversationId: activeConversationId } : undefined } + : item))} target={instance.targetRef?.type === 'conversation' || instance.targetRef?.type === 'task' ? instance.targetRef : selectedTaskId ? { type: 'task', taskId: selectedTaskId } : activeConversationId ? { type: 'conversation', conversationId: activeConversationId } : undefined} activeConversationId={instance.targetRef?.type === 'conversation' ? instance.targetRef.conversationId : instance.targetRef?.type === 'task' ? tasks.find((task) => instance.targetRef?.type === 'task' && task.id === instance.targetRef.taskId)?.conversationId : activeConversationId} - conversationTitle={instance.targetRef?.type === 'conversation' ? conversationTitles.get(instance.targetRef.conversationId) : undefined} + conversationTitle={conversationTitles.get(instance.targetRef?.type === 'conversation' ? instance.targetRef.conversationId : instance.targetRef?.type === 'task' ? tasks.find((task) => instance.targetRef?.type === 'task' && task.id === instance.targetRef.taskId)?.conversationId ?? '' : activeConversationId ?? '')} + taskTitle={tasks.find((task) => task.id === (instance.targetRef?.type === 'task' ? instance.targetRef.taskId : selectedTaskId))?.title} supervisionLibraries={supervisionLibraries} onOpenSupervisionGraph={onOpenSupervisionGraph} onContinueSupervision={onContinueSupervision} diff --git a/src/renderer/src/SupervisionCard.test.tsx b/src/renderer/src/SupervisionCard.test.tsx index eeb89f28..8e54fad6 100644 --- a/src/renderer/src/SupervisionCard.test.tsx +++ b/src/renderer/src/SupervisionCard.test.tsx @@ -6,6 +6,17 @@ import i18n from './i18n' afterEach(() => { cleanup(); vi.restoreAllMocks(); window.goodbuddy = {} as never }) const result = (id: string) => ({ id, storyLineId: id, sourceId: `source-${id}`, summary: `${id} recap`, scope: { kind: 'global' }, timeRange: { from: '2026-09-01T00:00:00Z', to: '2026-09-22T00:00:00Z' } }) +it('keeps unknown target IDs out of visible copy and disables actions without a target', async () => { + window.goodbuddy = { supervision: { overview: async () => [] } } as never + const view = render() + expect(screen.getByText('会话:未命名会话')).toHaveAttribute('title', 'raw-uuid') + expect(screen.queryByText(/raw-uuid/)).not.toBeInTheDocument() + await act(async () => {}) + view.rerender() + expect(screen.getByRole('button', { name: '固定监督目标' })).toBeDisabled() + expect(screen.getByRole('button', { name: '刷新监督回顾' })).toBeDisabled() +}) + it('graph navigation exposes only the matched result through a native bilingual button', async () => { const open = vi.fn() const overview = vi.fn().mockResolvedValueOnce([result('pinned')]).mockResolvedValueOnce([]) diff --git a/src/renderer/src/task-center.css b/src/renderer/src/task-center.css index a88acc36..07ae2390 100644 --- a/src/renderer/src/task-center.css +++ b/src/renderer/src/task-center.css @@ -1,3 +1,37 @@ +.assistant-sidebar__section.supervision-card { + min-width: 0; + padding: var(--space-3); + border-bottom: 1px solid var(--border-subtle); + gap: var(--space-2); + font-size: var(--font-body); + overflow-wrap: anywhere; +} + +.supervision-card__header, +.supervision-card__actions { + display: flex; + align-items: center; + gap: var(--space-2); +} + +.supervision-card__header { justify-content: space-between; } +.supervision-card__actions { flex-wrap: wrap; } +.supervision-card__header > .supervision-card__actions { flex: 0 0 auto; } +.supervision-card .supervision-card__header h3, +.supervision-card p { margin: 0; } +.supervision-card__target { color: var(--text-secondary); } +.supervision-card__source { + padding: var(--space-3); + border-radius: var(--radius-control); + background: var(--surface-subtle); +} +.supervision-card__actions select { min-width: 0; max-width: 100%; } +.supervision-card .icon-button:disabled { + background: transparent; + color: var(--text-muted); + cursor: not-allowed; +} + .task-center .task-center__scope { margin: 0 0 var(--space-2); color: var(--text-secondary); diff --git a/tests/supervisor-layout.electron.test.ts b/tests/supervisor-layout.electron.test.ts index 575e9dc0..3c1f31bc 100644 --- a/tests/supervisor-layout.electron.test.ts +++ b/tests/supervisor-layout.electron.test.ts @@ -8,7 +8,9 @@ import { createServer } from 'vite' import react from '@vitejs/plugin-react' import { expect, it } from 'vitest' -it('renders the production supervisor at desktop, narrow, and mobile widths', async () => { +it(process.env.GOODBUDDY_SUPERVISOR_SIDEBAR + ? 'renders the production supervision card at narrow widths in both themes' + : 'renders the production supervisor at desktop, narrow, and mobile widths', async () => { const directory = await mkdtemp(join(tmpdir(), 'goodbuddy-supervisor-')) const css = await readFile( 'src/renderer/src/supervisor-workspace.css', @@ -45,7 +47,7 @@ it('renders the production supervisor at desktop, narrow, and mobile widths', as const env: NodeJS.ProcessEnv = { ...process.env, GOODBUDDY_SUPERVISOR_URL: - server.resolvedUrls!.local[0] + 'supervisor.html', + server.resolvedUrls!.local[0] + 'supervisor.html' + (process.env.GOODBUDDY_SUPERVISOR_SIDEBAR ? '?sidebar=1' : ''), GOODBUDDY_SUPERVISOR_DIRECTORY: directory, GOODBUDDY_SUPERVISOR_TOKENS: JSON.stringify([ ...new Set( diff --git a/tests/support/supervisor-layout-driver.mjs b/tests/support/supervisor-layout-driver.mjs index 003520b1..f02af18a 100644 --- a/tests/support/supervisor-layout-driver.mjs +++ b/tests/support/supervisor-layout-driver.mjs @@ -61,6 +61,51 @@ app await settle() } const reports = [] + if (process.env.GOODBUDDY_SUPERVISOR_SIDEBAR) { + await win.loadURL(process.env.GOODBUDDY_SUPERVISOR_URL) + await wait('!!document.querySelector(".supervision-card")') + await js('document.fonts.ready') + win.show() + win.focus() + for (const theme of ['light', 'dark']) { + await js(`document.documentElement.dataset.theme = '${theme}'`) + for (const width of [480, 300, 200]) { + win.setContentSize(width, 640) + await js('document.querySelector(".supervision-card__header button").focus()') + win.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'Tab' }) + win.webContents.sendInputEvent({ type: 'keyUp', keyCode: 'Tab' }) + await settle() + const report = await js(`(() => { + const card = document.querySelector('.supervision-card'); + const header = card.querySelector('.supervision-card__header'); + const buttons = [...header.querySelectorAll('button')]; + return { + width: innerWidth, scrollWidth: document.documentElement.scrollWidth, + cardWidth: card.clientWidth, cardScrollWidth: card.scrollWidth, + headerHeight: header.getBoundingClientRect().height, + buttons: buttons.map(b => ({ width: b.getBoundingClientRect().width, + height: b.getBoundingClientRect().height, background: getComputedStyle(b).backgroundColor })), + focus: getComputedStyle(document.activeElement).outlineStyle, + text: card.textContent + }; + })()`) + assert(report.scrollWidth <= report.width, `Page overflow: ${JSON.stringify(report)}`) + assert(report.cardScrollWidth <= report.cardWidth, 'Card overflow') + assert.equal(report.headerHeight, 34) + assert(report.buttons.every(b => b.width === 34 && b.height === 34), 'Compact shared buttons') + assert.equal(report.focus, 'solid') + assert(!report.text.includes('simulated-conversation-uuid'), 'Raw UUID visible') + reports.push({ theme, ...report }) + await writeFile(join(artifacts, `sidebar-${theme}-${width}.png`), (await win.webContents.capturePage()).toPNG()) + } + } + assert.deepEqual(errors, []) + await writeFile(join(artifacts, 'sidebar-measurements.json'), JSON.stringify(reports, null, 2)) + console.log(JSON.stringify({ artifacts, cases: reports.length, errors })) + win.destroy() + app.quit() + return + } await open(false) await js( 'document.querySelector(".supervisor-workspace__legend .link-button").click()' diff --git a/tests/support/supervisor-layout-regression.tsx b/tests/support/supervisor-layout-regression.tsx index 42fee2a2..5ffd8f43 100644 --- a/tests/support/supervisor-layout-regression.tsx +++ b/tests/support/supervisor-layout-regression.tsx @@ -1,6 +1,7 @@ // Simulated visual fixture only. No preload, user database, or model is used. import { createRoot } from 'react-dom/client' import { HeartbeatCenter } from '../../src/renderer/src/HeartbeatCenter' +import { SupervisionCard } from '../../src/renderer/src/RightAssistantSidebar' import { PageShell } from '../../src/renderer/src/WorkspacePrimitives' import { UiLocaleProvider } from '../../src/renderer/src/i18n/UiLocaleProvider' import type { SupervisionGraphView } from '../../src/shared/supervision-contracts' @@ -202,6 +203,7 @@ createRoot(document.getElementById('root')!).render( color: 'var(--text-secondary)', display: 'flex', flexWrap: 'wrap', + overflowWrap: 'anywhere', gap: 'var(--space-3)' }} > @@ -212,7 +214,14 @@ createRoot(document.getElementById('root')!).render( ))} - + {params.has('sidebar') ?
+ {}} + /> +
: {}} onRetryLoad={noop} /> - +
} )