release: prepare Desktop 0.13.10
This commit is contained in:
@@ -4372,11 +4372,7 @@ describe("App", () => {
|
||||
const dialog = screen.getByRole("alertdialog", {
|
||||
name: "确认永久删除对话 新对话",
|
||||
});
|
||||
expect(dialog).toHaveTextContent("将永久删除此会话的全部内容");
|
||||
expect(dialog).toHaveTextContent(
|
||||
"如果此会话有正在运行的任务,也会同时停止",
|
||||
);
|
||||
expect(dialog).toHaveTextContent("此操作不可恢复");
|
||||
expect(dialog).not.toHaveTextContent("将永久删除此会话的全部内容");
|
||||
const cancel = screen.getByRole("button", {
|
||||
name: "取消删除对话 新对话",
|
||||
});
|
||||
|
||||
@@ -9373,7 +9373,6 @@ function App(): React.JSX.Element {
|
||||
}
|
||||
disabled={deletingConversationId === conversation.id}
|
||||
icon={<Trash2 aria-hidden="true" size={14} />}
|
||||
message={t("conversation.delete.message")}
|
||||
onCancel={() => setConfirmingConversationId("")}
|
||||
onConfirm={() =>
|
||||
void deleteConversation(conversation.id)
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import {
|
||||
useEffect,
|
||||
useState,
|
||||
type ReactNode
|
||||
} from 'react'
|
||||
@@ -86,6 +87,34 @@ function ControlledShell({
|
||||
}
|
||||
|
||||
describe('WorkbarShell', () => {
|
||||
it('preserves task state and latest props while keeping hidden terminal and browser effects live', () => {
|
||||
const stopped = vi.fn()
|
||||
function Panel({ id, value }: { id: string; value: string }): ReactNode {
|
||||
const [count, setCount] = useState(0)
|
||||
useEffect(() => () => stopped(id), [id])
|
||||
return <button onClick={() => setCount(count + 1)}>{id}:{value}:{count}</button>
|
||||
}
|
||||
const props = {
|
||||
instances: [...DEFAULT_WORKBAR_INSTANCES, terminalOne],
|
||||
onActiveInstanceChange: vi.fn(), onCloseInstance: vi.fn(), onCreateInstance: vi.fn(),
|
||||
onResolveTerminalTarget: (): WorkbarTargetRef => ({ type: 'local' })
|
||||
}
|
||||
const panel = (value: string) => (instance: WorkbarTabInstance): ReactNode =>
|
||||
<Panel id={instance.appId} value={value} />
|
||||
const { rerender } = render(<WorkbarShell {...props}
|
||||
activeInstanceId={DEFAULT_WORKBAR_INSTANCES[0]!.id} renderPanel={panel('old')} />)
|
||||
fireEvent.click(screen.getByRole('button', { name: 'tasks:old:0' }))
|
||||
rerender(<WorkbarShell {...props} activeInstanceId={terminalOne.id} renderPanel={panel('new')} />)
|
||||
expect(stopped).toHaveBeenCalledWith('tasks')
|
||||
expect(stopped).not.toHaveBeenCalledWith('terminal')
|
||||
expect(stopped).not.toHaveBeenCalledWith('browser')
|
||||
rerender(<WorkbarShell {...props}
|
||||
activeInstanceId={DEFAULT_WORKBAR_INSTANCES[0]!.id} renderPanel={panel('latest')} />)
|
||||
expect(screen.getByRole('button', { name: 'tasks:latest:1' })).toBeVisible()
|
||||
expect(stopped).not.toHaveBeenCalledWith('terminal')
|
||||
expect(stopped).not.toHaveBeenCalledWith('browser')
|
||||
})
|
||||
|
||||
it('shows overflow controls, scrolls without switching tabs, and hides them after widening', () => {
|
||||
let resize = (): void => {}
|
||||
vi.stubGlobal('ResizeObserver', class {
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
type LucideIcon
|
||||
} from 'lucide-react'
|
||||
import {
|
||||
Activity,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useId,
|
||||
@@ -77,6 +78,15 @@ export type WorkbarShellProps = {
|
||||
className?: string
|
||||
}
|
||||
|
||||
type WorkbarPanelProps = {
|
||||
instance: WorkbarTabInstance
|
||||
renderPanel: (instance: WorkbarTabInstance) => ReactNode
|
||||
}
|
||||
|
||||
function WorkbarPanel({ instance, renderPanel }: WorkbarPanelProps): React.JSX.Element {
|
||||
return <>{renderPanel(instance)}</>
|
||||
}
|
||||
|
||||
function joinClassNames(...values: (string | false | undefined)[]): string {
|
||||
return values.filter(Boolean).join(' ')
|
||||
}
|
||||
@@ -525,7 +535,11 @@ export function WorkbarShell({
|
||||
role="tabpanel"
|
||||
tabIndex={0}
|
||||
>
|
||||
{renderPanel(instance)}
|
||||
{instance.appId === 'tasks' ? (
|
||||
<Activity mode={!catalogOpen && instance.id === activeInstanceId ? 'visible' : 'hidden'}>
|
||||
<WorkbarPanel instance={instance} renderPanel={renderPanel} />
|
||||
</Activity>
|
||||
) : <WorkbarPanel instance={instance} renderPanel={renderPanel} />}
|
||||
</div>
|
||||
))}
|
||||
|
||||
|
||||
@@ -49,6 +49,16 @@ afterEach(() => {
|
||||
})
|
||||
|
||||
describe('useExecutionStats', () => {
|
||||
it('retains snapshot references for identical cached polling results', async () => {
|
||||
getExecutionStats.mockImplementation(async () => stats(10))
|
||||
const { result } = renderStats()
|
||||
await act(async () => {})
|
||||
const before = result.current
|
||||
await act(async () => { vi.advanceTimersByTime(5_000) })
|
||||
expect(result.current.conversation).toBe(before.conversation)
|
||||
expect(result.current.project).toBe(before.project)
|
||||
expect(getExecutionStats).toHaveBeenCalledTimes(4)
|
||||
})
|
||||
it.each(['project', 'conversation'] as const)(
|
||||
'clears the previous %s selection and ignores its late polling result', async (scope) => {
|
||||
const { result, rerender } = renderStats()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { startTransition, useEffect, useState } from 'react'
|
||||
import type { ExecutionStats } from '../../shared/assistant-contracts'
|
||||
|
||||
// Keep timing evidence in Main; never extrapolate a stopped or disconnected run.
|
||||
@@ -26,11 +26,18 @@ export function useExecutionStats(
|
||||
window.goodbuddy.tasks.getExecutionStats({ projectId })
|
||||
])
|
||||
pending = false
|
||||
if (!cancelled) setSnapshot({
|
||||
conversationId, projectId,
|
||||
conversation: conversation.status === 'fulfilled' ? conversation.value : undefined,
|
||||
project: project.status === 'fulfilled' ? project.value : undefined
|
||||
})
|
||||
if (!cancelled) startTransition(() => setSnapshot((current) => {
|
||||
const next = {
|
||||
conversationId, projectId,
|
||||
conversation: conversation.status === 'fulfilled' ? conversation.value : undefined,
|
||||
project: project.status === 'fulfilled' ? project.value : undefined
|
||||
}
|
||||
// IPC clones cached snapshots. Preserve references when their contents are unchanged
|
||||
// so idle polling does not rerender App and rebuild task duration maps.
|
||||
return current.conversationId === conversationId && current.projectId === projectId &&
|
||||
JSON.stringify(current.conversation) === JSON.stringify(next.conversation) &&
|
||||
JSON.stringify(current.project) === JSON.stringify(next.project) ? current : next
|
||||
}))
|
||||
}
|
||||
void refresh()
|
||||
const interval = window.setInterval(() => { void refresh() }, 5_000)
|
||||
|
||||
Reference in New Issue
Block a user