feat(codex): import and resume sessions from runners (#1088)

* fix codex import resume flow

* fix hub restart session active state

* fix codex transcript workspace scoping

* Address Codex import review findings

* Fix Codex import machine selection

* Update Codex sessions error test

* Address Codex import review findings

* Preserve forked Codex session id on sync

* Make Codex duplicate cleanup source-aware

* Handle Codex archive failures

* Limit existing session flag to Codex

* Preserve Codex import machine binding

* fix: rebase runner Codex import onto current main

* fix: preserve runner-scoped Codex import behavior

---------

Co-authored-by: syy <815728149@qq.com>
This commit is contained in:
NightWatcher314
2026-07-19 14:14:42 +08:00
committed by GitHub
co-authored by syy
parent 651c83a1d9
commit 64834467e3
42 changed files with 2536 additions and 133 deletions
+37 -2
View File
@@ -1,7 +1,7 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { isPermissionModeAllowedForFlavor } from '@hapi/protocol'
import type { ApiClient } from '@/api/client'
import type { CodexCollaborationMode, PermissionMode } from '@/types/api'
import type { CodexCollaborationMode, PermissionMode, SessionResponse, SessionsResponse } from '@/types/api'
import type { ReopenSessionResponse } from '@hapi/protocol/apiTypes'
import { queryKeys } from '@/lib/query-keys'
import { clearMessageWindow } from '@/lib/message-window-store'
@@ -29,6 +29,36 @@ export function useSessionActions(
} {
const queryClient = useQueryClient()
const markSessionActiveInCache = (targetSessionId: string) => {
// 中文注释:恢复/重开接口成功后,session-alive SSE 可能已经先到或稍后才到。
// 这里先乐观更新详情和侧边栏摘要,避免 UI 在下一次 SSE/REST 前仍显示离线。
queryClient.setQueryData<SessionResponse | undefined>(queryKeys.session(targetSessionId), (previous) => {
if (!previous?.session) return previous
return {
...previous,
session: {
...previous.session,
active: true,
activeAt: Math.max(previous.session.activeAt ?? 0, Date.now())
}
}
})
queryClient.setQueryData<SessionsResponse | undefined>(queryKeys.sessions, (previous) => {
if (!previous) return previous
let changed = false
const sessions = previous.sessions.map((summary) => {
if (summary.id !== targetSessionId) return summary
changed = true
return {
...summary,
active: true,
activeAt: Math.max(summary.activeAt ?? 0, Date.now())
}
})
return changed ? { ...previous, sessions } : previous
})
}
const invalidateSession = async () => {
if (!sessionId) return
await queryClient.invalidateQueries({ queryKey: queryKeys.session(sessionId) })
@@ -68,7 +98,12 @@ export function useSessionActions(
}
return await api.reopenSession(sessionId)
},
onSuccess: () => void invalidateSession(),
onSuccess: (result) => {
void (async () => {
await invalidateSession()
markSessionActiveInCache(result.sessionId)
})()
},
})
const switchMutation = useMutation({
+3
View File
@@ -346,6 +346,9 @@ export function useSSE(options: {
? patch.backgroundTaskCount ?? 0
: current.backgroundTaskCount,
model: Object.prototype.hasOwnProperty.call(patch, 'model') ? patch.model ?? null : current.model,
modelReasoningEffort: Object.prototype.hasOwnProperty.call(patch, 'modelReasoningEffort')
? patch.modelReasoningEffort ?? null
: current.modelReasoningEffort,
effort: Object.prototype.hasOwnProperty.call(patch, 'effort') ? patch.effort ?? null : current.effort
}