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:
mesalogo
2026-09-16 12:51:58 +08:00
parent 6c045bfe2e
commit e84d7bd185
27 changed files with 815 additions and 190 deletions
+31 -1
View File
@@ -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
}