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} /> - +
} )