feat: retain completed project activity until viewed
Keep successful background conversations in project activity until their chat is visible and loaded. Track completion in memory for the current client session; initial history, failures and cancellations do not create completion notices. Include whole-file companion changes already present in App and shared UI: preserve the workspace beneath Settings, show inline reply-copy feedback, and keep project settings inside the project menu. Include their component tests and documentation; navigation demo source changes remain unstaged. Validation: 77 focused activity tests, typecheck and lint passed; Electron component fixtures passed 112 checks. Full suite: 4365 passed, 3 failed, 67 skipped (offline inventory timeout and two heartbeat migration failures). Commit-time staged whitespace check passed; tests were not rerun. Release note: 后台会话完成后保留已完成提示,进入对应聊天后自动清除,避免错过任务结果。
This commit is contained in:
@@ -17,6 +17,8 @@ import {
|
||||
useState
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { activateModalFocus, trapTabFocus } from './dialog-focus'
|
||||
import type {
|
||||
AssistantExpert,
|
||||
AssistantProject,
|
||||
@@ -821,6 +823,8 @@ export function SettingsPanel({
|
||||
const runtimeCustomizationRef =
|
||||
useRef<RuntimeCustomizationSectionHandle>(null)
|
||||
const keepEditingRef = useRef<HTMLButtonElement>(null)
|
||||
const dialogRef = useRef<HTMLElement>(null)
|
||||
const closeRef = useRef<HTMLButtonElement>(null)
|
||||
const leaveTriggerRef = useRef<HTMLElement | undefined>(undefined)
|
||||
const handleRuntimeCustomizationDirtyChange = useCallback(
|
||||
(dirty: boolean): void => {
|
||||
@@ -1241,6 +1245,7 @@ export function SettingsPanel({
|
||||
}
|
||||
|
||||
const close = (trigger?: HTMLElement): void => {
|
||||
if (saving || testing || clearingLocalData) return
|
||||
if (hasUnsavedDrafts) {
|
||||
leaveTriggerRef.current =
|
||||
trigger ??
|
||||
@@ -1349,6 +1354,13 @@ export function SettingsPanel({
|
||||
keepEditingRef.current?.focus()
|
||||
}, [pendingLeave])
|
||||
|
||||
useEffect(() => {
|
||||
if (open && presentation === 'modal') {
|
||||
return activateModalFocus(() => closeRef.current)
|
||||
}
|
||||
return undefined
|
||||
}, [open, presentation])
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const requestLeave: SettingsLeaveRequester = (proceed) => {
|
||||
if (hasUnsavedDrafts) {
|
||||
@@ -2007,7 +2019,7 @@ export function SettingsPanel({
|
||||
? settings?.defaultModelProfileId
|
||||
: undefined
|
||||
|
||||
return (
|
||||
const panel = (
|
||||
<div
|
||||
className={
|
||||
presentation === 'page'
|
||||
@@ -2020,6 +2032,21 @@ export function SettingsPanel({
|
||||
aria-labelledby="settings-title"
|
||||
aria-modal={presentation === 'modal' ? 'true' : undefined}
|
||||
className="settings-panel"
|
||||
ref={dialogRef}
|
||||
onKeyDown={(event) => {
|
||||
if (
|
||||
presentation !== 'modal' ||
|
||||
event.defaultPrevented ||
|
||||
(event.target as HTMLElement).closest('[role="dialog"]') !== dialogRef.current
|
||||
) return
|
||||
event.stopPropagation()
|
||||
if (event.key === 'Escape') {
|
||||
event.preventDefault()
|
||||
if (pendingLeave) keepEditing()
|
||||
else close()
|
||||
}
|
||||
trapTabFocus(event, dialogRef.current)
|
||||
}}
|
||||
role={presentation === 'modal' ? 'dialog' : 'region'}
|
||||
>
|
||||
<div className="settings-panel__header">
|
||||
@@ -2029,6 +2056,8 @@ export function SettingsPanel({
|
||||
<button
|
||||
aria-label={t('center.close')}
|
||||
className="icon-button"
|
||||
disabled={saving || testing || clearingLocalData}
|
||||
ref={closeRef}
|
||||
onClick={(event) => close(event.currentTarget)}
|
||||
type="button"
|
||||
>
|
||||
@@ -4160,4 +4189,5 @@ export function SettingsPanel({
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
return presentation === 'modal' ? createPortal(panel, document.body) : panel
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user