mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-16 13:03:31 +08:00
The desktop workspace panel and chat transcript were mixing project-level file state with per-turn session history, which made rewind, file attachment, and changed-file previews brittle across multi-turn and historical sessions. This keeps turn checkpoints durable in the transcript, makes workspace refreshes happen at the right lifecycle points, and hardens long file previews without blocking the UI. Constraint: Right-side workspace changes are project working-tree state, while chat turn cards are session checkpoint state. Rejected: Treat every changed-file panel entry as session-local | new sessions must still reveal existing dirty project files. Confidence: high Scope-risk: moderate Directive: Do not couple global workspace status to session checkpoint cards without preserving both product meanings. Tested: cd desktop && bun run test src/components/workspace/WorkspacePanel.test.tsx Tested: cd desktop && bun run lint Tested: git diff --check Tested: cd desktop && bun run test Not-tested: Packaged Tauri runtime smoke test
716 lines
21 KiB
TypeScript
716 lines
21 KiB
TypeScript
import { create } from 'zustand'
|
|
import {
|
|
sessionsApi,
|
|
type WorkspaceDiffResult,
|
|
type WorkspaceReadFileResult,
|
|
type WorkspaceStatusResult,
|
|
type WorkspaceTreeResult,
|
|
} from '../api/sessions'
|
|
|
|
export const WORKSPACE_PANEL_DEFAULT_WIDTH = 860
|
|
export const WORKSPACE_PANEL_MIN_WIDTH = 640
|
|
export const WORKSPACE_PANEL_MAX_WIDTH = 1120
|
|
|
|
export type WorkspacePanelView = 'changed' | 'all'
|
|
export type WorkspacePreviewKind = 'file' | 'diff'
|
|
export type WorkspacePreviewCloseScope = 'current' | 'others' | 'left' | 'right' | 'all'
|
|
export type WorkspacePreviewState =
|
|
| 'loading'
|
|
| WorkspaceReadFileResult['state']
|
|
| WorkspaceDiffResult['state']
|
|
|
|
export type WorkspacePreviewTab = {
|
|
id: string
|
|
path: string
|
|
kind: WorkspacePreviewKind
|
|
title: string
|
|
language?: string
|
|
content?: string
|
|
dataUrl?: string
|
|
mimeType?: string
|
|
previewType?: 'text' | 'image'
|
|
diff?: string
|
|
state?: WorkspacePreviewState
|
|
error?: string
|
|
size?: number
|
|
}
|
|
|
|
export type WorkspacePanelSessionState = {
|
|
isOpen: boolean
|
|
activeView: WorkspacePanelView
|
|
hasUserSelectedView?: boolean
|
|
}
|
|
|
|
type WorkspacePanelLoadingState = {
|
|
statusBySession: Record<string, boolean | undefined>
|
|
treeBySessionPath: Record<string, boolean | undefined>
|
|
previewByTabId: Record<string, boolean | undefined>
|
|
}
|
|
|
|
type WorkspacePanelErrorState = {
|
|
statusBySession: Record<string, string | null | undefined>
|
|
treeBySessionPath: Record<string, string | null | undefined>
|
|
previewByTabId: Record<string, string | null | undefined>
|
|
}
|
|
|
|
type WorkspacePanelStore = {
|
|
panelBySession: Record<string, WorkspacePanelSessionState | undefined>
|
|
width: number
|
|
statusBySession: Record<string, WorkspaceStatusResult | undefined>
|
|
expandedPathsBySession: Record<string, string[] | undefined>
|
|
treeBySessionPath: Record<string, Record<string, WorkspaceTreeResult | undefined> | undefined>
|
|
previewTabsBySession: Record<string, WorkspacePreviewTab[] | undefined>
|
|
activePreviewTabIdBySession: Record<string, string | null | undefined>
|
|
loading: WorkspacePanelLoadingState
|
|
errors: WorkspacePanelErrorState
|
|
|
|
isPanelOpen: (sessionId: string) => boolean
|
|
getActiveView: (sessionId: string) => WorkspacePanelView
|
|
openPanel: (sessionId: string) => void
|
|
closePanel: (sessionId: string) => void
|
|
togglePanel: (sessionId: string) => void
|
|
setWidth: (width: number) => void
|
|
setActiveView: (sessionId: string, view: WorkspacePanelView) => void
|
|
loadStatus: (sessionId: string) => Promise<void>
|
|
loadTree: (sessionId: string, path?: string) => Promise<void>
|
|
toggleTreeNode: (sessionId: string, path: string) => Promise<void>
|
|
openPreview: (sessionId: string, path: string, kind: WorkspacePreviewKind) => Promise<void>
|
|
closePreview: (sessionId: string, tabId: string) => void
|
|
closePreviewTabs: (sessionId: string, tabId: string, scope: WorkspacePreviewCloseScope) => void
|
|
clearSession: (sessionId: string) => void
|
|
resetSessionUi: (sessionId: string) => void
|
|
}
|
|
|
|
const DEFAULT_PANEL_STATE: WorkspacePanelSessionState = {
|
|
isOpen: false,
|
|
activeView: 'changed',
|
|
}
|
|
|
|
const statusRequestIds = new Map<string, number>()
|
|
const treeRequestIds = new Map<string, number>()
|
|
const previewRequestIds = new Map<string, number>()
|
|
|
|
function nextRequestId(store: Map<string, number>, key: string) {
|
|
const requestId = (store.get(key) ?? 0) + 1
|
|
store.set(key, requestId)
|
|
return requestId
|
|
}
|
|
|
|
function invalidateRequest(store: Map<string, number>, key: string) {
|
|
store.set(key, (store.get(key) ?? 0) + 1)
|
|
}
|
|
|
|
function isLatestRequest(store: Map<string, number>, key: string, requestId: number) {
|
|
return store.get(key) === requestId
|
|
}
|
|
|
|
export function clampWorkspacePanelWidth(width: number) {
|
|
if (!Number.isFinite(width)) return WORKSPACE_PANEL_DEFAULT_WIDTH
|
|
const rounded = Math.round(width)
|
|
return Math.min(WORKSPACE_PANEL_MAX_WIDTH, Math.max(WORKSPACE_PANEL_MIN_WIDTH, rounded))
|
|
}
|
|
|
|
function getSessionPanelState(
|
|
panelBySession: Record<string, WorkspacePanelSessionState | undefined>,
|
|
sessionId: string,
|
|
) {
|
|
return panelBySession[sessionId] ?? DEFAULT_PANEL_STATE
|
|
}
|
|
|
|
function makeTreeKey(sessionId: string, path: string) {
|
|
return `${sessionId}::${path}`
|
|
}
|
|
|
|
export function getWorkspacePreviewTabId(path: string, kind: WorkspacePreviewKind) {
|
|
return `${kind}:${path}`
|
|
}
|
|
|
|
function makePreviewKey(sessionId: string, tabId: string) {
|
|
return `${sessionId}::${tabId}`
|
|
}
|
|
|
|
function getPathTitle(path: string) {
|
|
if (!path) return 'Workspace'
|
|
const segments = path.split('/').filter(Boolean)
|
|
return segments[segments.length - 1] ?? path
|
|
}
|
|
|
|
function stripSessionKeys<T>(record: Record<string, T>, sessionId: string) {
|
|
const prefix = `${sessionId}::`
|
|
return Object.fromEntries(
|
|
Object.entries(record).filter(([key]) => !key.startsWith(prefix)),
|
|
) as Record<string, T>
|
|
}
|
|
|
|
function removeRecordKey<T>(record: Record<string, T>, key: string) {
|
|
if (!(key in record)) return record
|
|
const { [key]: _removed, ...rest } = record
|
|
return rest
|
|
}
|
|
|
|
function removeRecordKeys<T>(record: Record<string, T>, keys: string[]) {
|
|
let next = record
|
|
for (const key of keys) {
|
|
next = removeRecordKey(next, key)
|
|
}
|
|
return next
|
|
}
|
|
|
|
function invalidateSessionScopedRequests(store: Map<string, number>, sessionId: string) {
|
|
const prefix = `${sessionId}::`
|
|
for (const key of store.keys()) {
|
|
if (key.startsWith(prefix)) {
|
|
invalidateRequest(store, key)
|
|
}
|
|
}
|
|
}
|
|
|
|
function upsertPreviewTab(
|
|
tabs: WorkspacePreviewTab[],
|
|
tabId: string,
|
|
update: WorkspacePreviewTab | ((current: WorkspacePreviewTab) => WorkspacePreviewTab),
|
|
) {
|
|
const index = tabs.findIndex((tab) => tab.id === tabId)
|
|
if (index < 0) return tabs
|
|
|
|
const current = tabs[index]!
|
|
const next = typeof update === 'function' ? update(current) : update
|
|
const nextTabs = [...tabs]
|
|
nextTabs[index] = next
|
|
return nextTabs
|
|
}
|
|
|
|
export const useWorkspacePanelStore = create<WorkspacePanelStore>((set, get) => ({
|
|
panelBySession: {},
|
|
width: WORKSPACE_PANEL_DEFAULT_WIDTH,
|
|
statusBySession: {},
|
|
expandedPathsBySession: {},
|
|
treeBySessionPath: {},
|
|
previewTabsBySession: {},
|
|
activePreviewTabIdBySession: {},
|
|
loading: {
|
|
statusBySession: {},
|
|
treeBySessionPath: {},
|
|
previewByTabId: {},
|
|
},
|
|
errors: {
|
|
statusBySession: {},
|
|
treeBySessionPath: {},
|
|
previewByTabId: {},
|
|
},
|
|
|
|
isPanelOpen: (sessionId) => getSessionPanelState(get().panelBySession, sessionId).isOpen,
|
|
getActiveView: (sessionId) => getSessionPanelState(get().panelBySession, sessionId).activeView,
|
|
|
|
openPanel: (sessionId) =>
|
|
set((state) => ({
|
|
panelBySession: {
|
|
...state.panelBySession,
|
|
[sessionId]: {
|
|
...getSessionPanelState(state.panelBySession, sessionId),
|
|
isOpen: true,
|
|
},
|
|
},
|
|
})),
|
|
|
|
closePanel: (sessionId) =>
|
|
set((state) => ({
|
|
panelBySession: {
|
|
...state.panelBySession,
|
|
[sessionId]: {
|
|
...getSessionPanelState(state.panelBySession, sessionId),
|
|
isOpen: false,
|
|
},
|
|
},
|
|
})),
|
|
|
|
togglePanel: (sessionId) =>
|
|
set((state) => {
|
|
const panel = getSessionPanelState(state.panelBySession, sessionId)
|
|
return {
|
|
panelBySession: {
|
|
...state.panelBySession,
|
|
[sessionId]: {
|
|
...panel,
|
|
isOpen: !panel.isOpen,
|
|
},
|
|
},
|
|
}
|
|
}),
|
|
|
|
setWidth: (width) => set({ width: clampWorkspacePanelWidth(width) }),
|
|
|
|
setActiveView: (sessionId, view) =>
|
|
set((state) => ({
|
|
panelBySession: {
|
|
...state.panelBySession,
|
|
[sessionId]: {
|
|
...getSessionPanelState(state.panelBySession, sessionId),
|
|
activeView: view,
|
|
hasUserSelectedView: true,
|
|
},
|
|
},
|
|
})),
|
|
|
|
loadStatus: async (sessionId) => {
|
|
const requestId = nextRequestId(statusRequestIds, sessionId)
|
|
|
|
set((state) => ({
|
|
loading: {
|
|
...state.loading,
|
|
statusBySession: {
|
|
...state.loading.statusBySession,
|
|
[sessionId]: true,
|
|
},
|
|
},
|
|
errors: {
|
|
...state.errors,
|
|
statusBySession: {
|
|
...state.errors.statusBySession,
|
|
[sessionId]: null,
|
|
},
|
|
},
|
|
}))
|
|
|
|
try {
|
|
const result = await sessionsApi.getWorkspaceStatus(sessionId)
|
|
if (!isLatestRequest(statusRequestIds, sessionId, requestId)) return
|
|
|
|
set((state) => {
|
|
const panel = getSessionPanelState(state.panelBySession, sessionId)
|
|
const nextActiveView =
|
|
!panel.hasUserSelectedView && result.state === 'ok'
|
|
? result.changedFiles.length > 0 ? 'changed' : 'all'
|
|
: panel.activeView
|
|
|
|
return {
|
|
panelBySession: {
|
|
...state.panelBySession,
|
|
[sessionId]: {
|
|
...panel,
|
|
activeView: nextActiveView,
|
|
},
|
|
},
|
|
statusBySession: {
|
|
...state.statusBySession,
|
|
[sessionId]: result,
|
|
},
|
|
loading: {
|
|
...state.loading,
|
|
statusBySession: {
|
|
...state.loading.statusBySession,
|
|
[sessionId]: false,
|
|
},
|
|
},
|
|
errors: {
|
|
...state.errors,
|
|
statusBySession: {
|
|
...state.errors.statusBySession,
|
|
[sessionId]: result.error ?? null,
|
|
},
|
|
},
|
|
}
|
|
})
|
|
} catch (error) {
|
|
if (!isLatestRequest(statusRequestIds, sessionId, requestId)) return
|
|
|
|
set((state) => ({
|
|
loading: {
|
|
...state.loading,
|
|
statusBySession: {
|
|
...state.loading.statusBySession,
|
|
[sessionId]: false,
|
|
},
|
|
},
|
|
errors: {
|
|
...state.errors,
|
|
statusBySession: {
|
|
...state.errors.statusBySession,
|
|
[sessionId]: error instanceof Error ? error.message : 'Failed to load workspace status',
|
|
},
|
|
},
|
|
}))
|
|
}
|
|
},
|
|
|
|
loadTree: async (sessionId, path = '') => {
|
|
const treeKey = makeTreeKey(sessionId, path)
|
|
const requestId = nextRequestId(treeRequestIds, treeKey)
|
|
|
|
set((state) => ({
|
|
loading: {
|
|
...state.loading,
|
|
treeBySessionPath: {
|
|
...state.loading.treeBySessionPath,
|
|
[treeKey]: true,
|
|
},
|
|
},
|
|
errors: {
|
|
...state.errors,
|
|
treeBySessionPath: {
|
|
...state.errors.treeBySessionPath,
|
|
[treeKey]: null,
|
|
},
|
|
},
|
|
}))
|
|
|
|
try {
|
|
const result = await sessionsApi.getWorkspaceTree(sessionId, path)
|
|
if (!isLatestRequest(treeRequestIds, treeKey, requestId)) return
|
|
|
|
set((state) => ({
|
|
treeBySessionPath: {
|
|
...state.treeBySessionPath,
|
|
[sessionId]: {
|
|
...state.treeBySessionPath[sessionId],
|
|
[path]: result,
|
|
},
|
|
},
|
|
loading: {
|
|
...state.loading,
|
|
treeBySessionPath: {
|
|
...state.loading.treeBySessionPath,
|
|
[treeKey]: false,
|
|
},
|
|
},
|
|
errors: {
|
|
...state.errors,
|
|
treeBySessionPath: {
|
|
...state.errors.treeBySessionPath,
|
|
[treeKey]: result.error ?? null,
|
|
},
|
|
},
|
|
}))
|
|
} catch (error) {
|
|
if (!isLatestRequest(treeRequestIds, treeKey, requestId)) return
|
|
|
|
set((state) => ({
|
|
loading: {
|
|
...state.loading,
|
|
treeBySessionPath: {
|
|
...state.loading.treeBySessionPath,
|
|
[treeKey]: false,
|
|
},
|
|
},
|
|
errors: {
|
|
...state.errors,
|
|
treeBySessionPath: {
|
|
...state.errors.treeBySessionPath,
|
|
[treeKey]: error instanceof Error ? error.message : 'Failed to load workspace tree',
|
|
},
|
|
},
|
|
}))
|
|
}
|
|
},
|
|
|
|
toggleTreeNode: async (sessionId, path) => {
|
|
let shouldLoad = false
|
|
|
|
set((state) => {
|
|
const expanded = new Set(state.expandedPathsBySession[sessionId] ?? [])
|
|
if (expanded.has(path)) {
|
|
expanded.delete(path)
|
|
} else {
|
|
expanded.add(path)
|
|
if (!state.treeBySessionPath[sessionId]?.[path]) {
|
|
shouldLoad = true
|
|
}
|
|
}
|
|
|
|
return {
|
|
expandedPathsBySession: {
|
|
...state.expandedPathsBySession,
|
|
[sessionId]: [...expanded],
|
|
},
|
|
}
|
|
})
|
|
|
|
if (shouldLoad) {
|
|
await get().loadTree(sessionId, path)
|
|
}
|
|
},
|
|
|
|
openPreview: async (sessionId, path, kind) => {
|
|
const tabId = getWorkspacePreviewTabId(path, kind)
|
|
const requestKey = makePreviewKey(sessionId, tabId)
|
|
const existing = get().previewTabsBySession[sessionId]?.find((tab) => tab.id === tabId)
|
|
|
|
if (existing) {
|
|
set((state) => ({
|
|
activePreviewTabIdBySession: {
|
|
...state.activePreviewTabIdBySession,
|
|
[sessionId]: tabId,
|
|
},
|
|
}))
|
|
return
|
|
}
|
|
|
|
const requestId = nextRequestId(previewRequestIds, requestKey)
|
|
const baseTab: WorkspacePreviewTab = {
|
|
id: tabId,
|
|
path,
|
|
kind,
|
|
title: getPathTitle(path),
|
|
state: 'loading',
|
|
}
|
|
|
|
set((state) => ({
|
|
previewTabsBySession: {
|
|
...state.previewTabsBySession,
|
|
[sessionId]: [...(state.previewTabsBySession[sessionId] ?? []), baseTab],
|
|
},
|
|
activePreviewTabIdBySession: {
|
|
...state.activePreviewTabIdBySession,
|
|
[sessionId]: tabId,
|
|
},
|
|
loading: {
|
|
...state.loading,
|
|
previewByTabId: {
|
|
...state.loading.previewByTabId,
|
|
[requestKey]: true,
|
|
},
|
|
},
|
|
errors: {
|
|
...state.errors,
|
|
previewByTabId: {
|
|
...state.errors.previewByTabId,
|
|
[requestKey]: null,
|
|
},
|
|
},
|
|
}))
|
|
|
|
try {
|
|
if (kind === 'diff') {
|
|
const result = await sessionsApi.getWorkspaceDiff(sessionId, path)
|
|
if (!isLatestRequest(previewRequestIds, requestKey, requestId)) return
|
|
if (!get().previewTabsBySession[sessionId]?.some((tab) => tab.id === tabId)) return
|
|
|
|
set((state) => {
|
|
const tabs = state.previewTabsBySession[sessionId] ?? []
|
|
return {
|
|
previewTabsBySession: {
|
|
...state.previewTabsBySession,
|
|
[sessionId]: upsertPreviewTab(tabs, tabId, (current) => ({
|
|
...current,
|
|
diff: result.diff ?? '',
|
|
content: undefined,
|
|
language: undefined,
|
|
size: undefined,
|
|
state: result.state,
|
|
error: result.error,
|
|
})),
|
|
},
|
|
loading: {
|
|
...state.loading,
|
|
previewByTabId: {
|
|
...state.loading.previewByTabId,
|
|
[requestKey]: false,
|
|
},
|
|
},
|
|
errors: {
|
|
...state.errors,
|
|
previewByTabId: {
|
|
...state.errors.previewByTabId,
|
|
[requestKey]: result.error ?? null,
|
|
},
|
|
},
|
|
}
|
|
})
|
|
return
|
|
}
|
|
|
|
const result = await sessionsApi.getWorkspaceFile(sessionId, path)
|
|
if (!isLatestRequest(previewRequestIds, requestKey, requestId)) return
|
|
if (!get().previewTabsBySession[sessionId]?.some((tab) => tab.id === tabId)) return
|
|
|
|
set((state) => {
|
|
const tabs = state.previewTabsBySession[sessionId] ?? []
|
|
return {
|
|
previewTabsBySession: {
|
|
...state.previewTabsBySession,
|
|
[sessionId]: upsertPreviewTab(tabs, tabId, (current) => ({
|
|
...current,
|
|
content: result.content,
|
|
dataUrl: result.dataUrl,
|
|
mimeType: result.mimeType,
|
|
previewType: result.previewType ?? 'text',
|
|
diff: undefined,
|
|
language: result.language,
|
|
size: result.size,
|
|
state: result.state,
|
|
error: result.error,
|
|
})),
|
|
},
|
|
loading: {
|
|
...state.loading,
|
|
previewByTabId: {
|
|
...state.loading.previewByTabId,
|
|
[requestKey]: false,
|
|
},
|
|
},
|
|
errors: {
|
|
...state.errors,
|
|
previewByTabId: {
|
|
...state.errors.previewByTabId,
|
|
[requestKey]: result.error ?? null,
|
|
},
|
|
},
|
|
}
|
|
})
|
|
} catch (error) {
|
|
if (!isLatestRequest(previewRequestIds, requestKey, requestId)) return
|
|
if (!get().previewTabsBySession[sessionId]?.some((tab) => tab.id === tabId)) return
|
|
|
|
set((state) => {
|
|
const tabs = state.previewTabsBySession[sessionId] ?? []
|
|
const message = error instanceof Error ? error.message : 'Failed to load workspace preview'
|
|
|
|
return {
|
|
previewTabsBySession: {
|
|
...state.previewTabsBySession,
|
|
[sessionId]: upsertPreviewTab(tabs, tabId, (current) => ({
|
|
...current,
|
|
state: 'error',
|
|
error: message,
|
|
})),
|
|
},
|
|
loading: {
|
|
...state.loading,
|
|
previewByTabId: {
|
|
...state.loading.previewByTabId,
|
|
[requestKey]: false,
|
|
},
|
|
},
|
|
errors: {
|
|
...state.errors,
|
|
previewByTabId: {
|
|
...state.errors.previewByTabId,
|
|
[requestKey]: message,
|
|
},
|
|
},
|
|
}
|
|
})
|
|
}
|
|
},
|
|
|
|
closePreview: (sessionId, tabId) => {
|
|
get().closePreviewTabs(sessionId, tabId, 'current')
|
|
},
|
|
|
|
closePreviewTabs: (sessionId, tabId, scope) => {
|
|
set((state) => {
|
|
const tabs = state.previewTabsBySession[sessionId] ?? []
|
|
const index = tabs.findIndex((tab) => tab.id === tabId)
|
|
if (index < 0) {
|
|
const requestKey = makePreviewKey(sessionId, tabId)
|
|
invalidateRequest(previewRequestIds, requestKey)
|
|
return {
|
|
loading: {
|
|
...state.loading,
|
|
previewByTabId: removeRecordKey(state.loading.previewByTabId, requestKey),
|
|
},
|
|
errors: {
|
|
...state.errors,
|
|
previewByTabId: removeRecordKey(state.errors.previewByTabId, requestKey),
|
|
},
|
|
}
|
|
}
|
|
|
|
let nextTabs: WorkspacePreviewTab[]
|
|
switch (scope) {
|
|
case 'others':
|
|
nextTabs = [tabs[index]!]
|
|
break
|
|
case 'left':
|
|
nextTabs = tabs.slice(index)
|
|
break
|
|
case 'right':
|
|
nextTabs = tabs.slice(0, index + 1)
|
|
break
|
|
case 'all':
|
|
nextTabs = []
|
|
break
|
|
case 'current':
|
|
default:
|
|
nextTabs = tabs.filter((tab) => tab.id !== tabId)
|
|
break
|
|
}
|
|
|
|
const nextTabIds = new Set(nextTabs.map((tab) => tab.id))
|
|
const closingTabIds = tabs.map((tab) => tab.id).filter((id) => !nextTabIds.has(id))
|
|
const requestKeys = closingTabIds.map((id) => makePreviewKey(sessionId, id))
|
|
for (const key of requestKeys) {
|
|
invalidateRequest(previewRequestIds, key)
|
|
}
|
|
|
|
const activeTabId = state.activePreviewTabIdBySession[sessionId] ?? null
|
|
|
|
let nextActiveTabId = activeTabId
|
|
if (scope === 'all' || nextTabs.length === 0) {
|
|
nextActiveTabId = null
|
|
} else if (!activeTabId || !nextTabIds.has(activeTabId)) {
|
|
const targetTab = nextTabs.find((tab) => tab.id === tabId)
|
|
nextActiveTabId = targetTab?.id ?? nextTabs[Math.min(index, nextTabs.length - 1)]?.id ?? null
|
|
} else if (scope === 'others') {
|
|
nextActiveTabId = tabId
|
|
} else if (activeTabId === tabId && scope === 'current') {
|
|
if (nextTabs.length === 0) {
|
|
nextActiveTabId = null
|
|
} else if (index >= nextTabs.length) {
|
|
nextActiveTabId = nextTabs[nextTabs.length - 1]!.id
|
|
} else {
|
|
nextActiveTabId = nextTabs[index]!.id
|
|
}
|
|
}
|
|
|
|
return {
|
|
previewTabsBySession: {
|
|
...state.previewTabsBySession,
|
|
[sessionId]: nextTabs.length > 0 ? nextTabs : undefined,
|
|
},
|
|
activePreviewTabIdBySession: {
|
|
...state.activePreviewTabIdBySession,
|
|
[sessionId]: nextActiveTabId,
|
|
},
|
|
loading: {
|
|
...state.loading,
|
|
previewByTabId: removeRecordKeys(state.loading.previewByTabId, requestKeys),
|
|
},
|
|
errors: {
|
|
...state.errors,
|
|
previewByTabId: removeRecordKeys(state.errors.previewByTabId, requestKeys),
|
|
},
|
|
}
|
|
})
|
|
},
|
|
|
|
clearSession: (sessionId) => {
|
|
invalidateRequest(statusRequestIds, sessionId)
|
|
invalidateSessionScopedRequests(treeRequestIds, sessionId)
|
|
invalidateSessionScopedRequests(previewRequestIds, sessionId)
|
|
|
|
set((state) => ({
|
|
panelBySession: removeRecordKey(state.panelBySession, sessionId),
|
|
statusBySession: removeRecordKey(state.statusBySession, sessionId),
|
|
expandedPathsBySession: removeRecordKey(state.expandedPathsBySession, sessionId),
|
|
treeBySessionPath: removeRecordKey(state.treeBySessionPath, sessionId),
|
|
previewTabsBySession: removeRecordKey(state.previewTabsBySession, sessionId),
|
|
activePreviewTabIdBySession: removeRecordKey(state.activePreviewTabIdBySession, sessionId),
|
|
loading: {
|
|
statusBySession: removeRecordKey(state.loading.statusBySession, sessionId),
|
|
treeBySessionPath: stripSessionKeys(state.loading.treeBySessionPath, sessionId),
|
|
previewByTabId: stripSessionKeys(state.loading.previewByTabId, sessionId),
|
|
},
|
|
errors: {
|
|
statusBySession: removeRecordKey(state.errors.statusBySession, sessionId),
|
|
treeBySessionPath: stripSessionKeys(state.errors.treeBySessionPath, sessionId),
|
|
previewByTabId: stripSessionKeys(state.errors.previewByTabId, sessionId),
|
|
},
|
|
}))
|
|
},
|
|
|
|
resetSessionUi: (sessionId) => {
|
|
get().clearSession(sessionId)
|
|
},
|
|
}))
|