mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 08:02:28 +00:00
fix(sidebar): keep structured chats visible across workspace surfaces
This commit is contained in:
+33
@@ -3,6 +3,7 @@
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
||||
import { cleanup, renderHook } from '@testing-library/react'
|
||||
import { useAppStore } from '@/store'
|
||||
import type { Tab } from '../../../../shared/tab-types'
|
||||
import { getWorktreeHostIdentity } from '../../../../shared/worktree/host-qualified-identity'
|
||||
import { makeRepo, makeWorktree } from '../worktree-jump-palette-test-fixtures'
|
||||
import { useVisibleWorkspaceKanbanWorktreeIds } from './use-visible-workspace-kanban-worktree-ids'
|
||||
@@ -38,4 +39,36 @@ describe('useVisibleWorkspaceKanbanWorktreeIds', () => {
|
||||
|
||||
expect(result.current).toEqual(new Set([getWorktreeHostIdentity(local)]))
|
||||
})
|
||||
|
||||
it('keeps a structured-chat workspace visible when sleeping workspaces are hidden', () => {
|
||||
const worktree = makeWorktree('chat', 'Chat workspace')
|
||||
const repo = makeRepo()
|
||||
const structuredTab: Tab = {
|
||||
id: 'chat-tab',
|
||||
entityId: 'chat-session',
|
||||
groupId: 'chat-group',
|
||||
worktreeId: worktree.id,
|
||||
contentType: 'agent-session',
|
||||
agentSessionAgent: 'codex',
|
||||
label: 'Chat',
|
||||
customLabel: null,
|
||||
color: null,
|
||||
sortOrder: 0,
|
||||
createdAt: 0
|
||||
}
|
||||
useAppStore.setState({
|
||||
worktreesByRepo: { [repo.id]: [worktree] },
|
||||
unifiedTabsByWorktree: { [worktree.id]: [structuredTab] },
|
||||
showSleepingWorkspaces: false
|
||||
})
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useVisibleWorkspaceKanbanWorktreeIds({
|
||||
allWorktrees: [worktree],
|
||||
repoMap: new Map([[repo.id, repo]])
|
||||
})
|
||||
)
|
||||
|
||||
expect(result.current).toEqual(new Set([getWorktreeHostIdentity(worktree)]))
|
||||
})
|
||||
})
|
||||
|
||||
@@ -11,6 +11,10 @@ import {
|
||||
EMPTY_PAIRED_DEVICE_IDS_BY_ENVIRONMENT,
|
||||
getPairedDeviceIdsByEnvironment
|
||||
} from './workspace-creator-visibility'
|
||||
import {
|
||||
EMPTY_STRUCTURED_CHAT_WORKTREE_IDS,
|
||||
getWorktreeIdsWithStructuredChat
|
||||
} from './visible-worktree-activity-inputs'
|
||||
import { getWorktreeHostIdentity } from '../../../../shared/worktree/host-qualified-identity'
|
||||
|
||||
type UseVisibleWorkspaceKanbanWorktreeIdsParams = {
|
||||
@@ -51,6 +55,11 @@ export function useVisibleWorkspaceKanbanWorktreeIds({
|
||||
const browserTabsByWorktree = useAppStore((s) =>
|
||||
!showSleepingWorkspaces ? s.browserTabsByWorktree : null
|
||||
)
|
||||
const worktreeIdsWithStructuredChat = useAppStore((s) =>
|
||||
showSleepingWorkspaces
|
||||
? EMPTY_STRUCTURED_CHAT_WORKTREE_IDS
|
||||
: getWorktreeIdsWithStructuredChat(s.unifiedTabsByWorktree)
|
||||
)
|
||||
const agentStatusEpoch = useAppStore((s) => (!showSleepingWorkspaces ? s.agentStatusEpoch : 0))
|
||||
// Why: skip the clock entirely when the epoch is the opt-out sentinel, so a
|
||||
// sleeping-workspaces board cannot evict the sample the live boards share.
|
||||
@@ -82,6 +91,7 @@ export function useVisibleWorkspaceKanbanWorktreeIds({
|
||||
ptyIdsByTabId,
|
||||
browserTabsByWorktree,
|
||||
worktreeIdsWithLiveAgent,
|
||||
worktreeIdsWithStructuredChat,
|
||||
hideDefaultBranchWorkspace,
|
||||
hideAutomationGeneratedWorkspaces,
|
||||
hideCliCreatedWorkspaces,
|
||||
@@ -121,6 +131,7 @@ export function useVisibleWorkspaceKanbanWorktreeIds({
|
||||
showSleepingWorkspaces,
|
||||
tabsByWorktree,
|
||||
worktreeIdsWithLiveAgent,
|
||||
worktreeIdsWithStructuredChat,
|
||||
worktreesByRepo
|
||||
])
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ export function getVisibleWorktreeBrowserActivityTabs(
|
||||
return browserProjection.project(browserTabsByWorktree)
|
||||
}
|
||||
|
||||
const EMPTY_WORKTREE_IDS: ReadonlySet<string> = new Set()
|
||||
export const EMPTY_STRUCTURED_CHAT_WORKTREE_IDS: ReadonlySet<string> = new Set()
|
||||
const structuredChatWorktreeIds = new WeakMap<Record<string, Tab[]>, ReadonlySet<string>>()
|
||||
|
||||
/**
|
||||
@@ -51,7 +51,7 @@ export function getWorktreeIdsWithStructuredChat(
|
||||
unifiedTabsByWorktree: Record<string, Tab[]> | null | undefined
|
||||
): ReadonlySet<string> {
|
||||
if (!unifiedTabsByWorktree) {
|
||||
return EMPTY_WORKTREE_IDS
|
||||
return EMPTY_STRUCTURED_CHAT_WORKTREE_IDS
|
||||
}
|
||||
// Keyed on the snapshot, like the tab projection it reads: zustand re-runs every mounted card's
|
||||
// selector on each store write, and this is a whole-store scan.
|
||||
|
||||
@@ -24,7 +24,10 @@ import {
|
||||
import type { Worktree } from '../../../../shared/worktree/types'
|
||||
import { buildWorktreeComparator, sortWorktreesSmart } from './smart-sort'
|
||||
import { isInactiveWorkspace } from '@/lib/worktree-activity-state'
|
||||
export { getWorktreeIdsWithStructuredChat } from './visible-worktree-activity-inputs'
|
||||
export {
|
||||
EMPTY_STRUCTURED_CHAT_WORKTREE_IDS,
|
||||
getWorktreeIdsWithStructuredChat
|
||||
} from './visible-worktree-activity-inputs'
|
||||
// Runtime edge only one way: the builder imports VisibleWorktreeOptions as a type, which erases.
|
||||
import { buildVisibleWorktreeOptionsFromState } from './visible-worktree-options-from-state'
|
||||
import { useAppStore } from '@/store'
|
||||
|
||||
+36
-1
@@ -1,7 +1,7 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { cleanup, renderHook } from '@testing-library/react'
|
||||
import { act, cleanup, renderHook } from '@testing-library/react'
|
||||
import { useAppStore } from '@/store'
|
||||
import { LOCAL_EXECUTION_HOST_ID } from '../../../../../../shared/execution-host'
|
||||
import { getWorktreeHostIdentity } from '../../../../../../shared/worktree/host-qualified-identity'
|
||||
@@ -159,4 +159,39 @@ describe('useVisibleSidebarWorktrees', () => {
|
||||
rerender(Object.assign({}, withSettings(nextSettings), { defaultHostId: 'runtime:other' }))
|
||||
expect(computeVisibleWorktreesCalls.count).toBe(callsAfterFirstRender + 1)
|
||||
})
|
||||
|
||||
it('does not rescan visible worktrees for chat-tab writes when sleeping workspaces are shown', () => {
|
||||
const repo = makeRepo()
|
||||
const worktree = makeWorktree('alpha', 'Alpha workspace')
|
||||
useAppStore.setState({ worktreesByRepo: { [repo.id]: [worktree] } })
|
||||
computeVisibleWorktreesCalls.count = 0
|
||||
|
||||
renderHook(() =>
|
||||
useVisibleSidebarWorktrees({
|
||||
filterState: {
|
||||
showSleepingWorkspaces: true,
|
||||
filterRepoIds: [],
|
||||
hideDefaultBranchWorkspace: false,
|
||||
hideAutomationGeneratedWorkspaces: false,
|
||||
hideCliCreatedWorkspaces: false,
|
||||
hideDetachedHeadWorkspaces: false,
|
||||
hideWorkspacesFromOtherDevices: false,
|
||||
alwaysShowDefaultBranchWorkspace: true,
|
||||
visibleWorkspaceHostIds: null,
|
||||
workspaceHostScope: 'all'
|
||||
},
|
||||
sortBy: 'recent',
|
||||
sortedIds: [worktree.id],
|
||||
repoMap: new Map([[repo.id, repo]]),
|
||||
worktreeLineageById: {},
|
||||
defaultHostId: LOCAL_EXECUTION_HOST_ID,
|
||||
agentSendTargetWorktreeId: null
|
||||
})
|
||||
)
|
||||
const callsAfterFirstRender = computeVisibleWorktreesCalls.count
|
||||
|
||||
act(() => useAppStore.setState({ unifiedTabsByWorktree: { [worktree.id]: [] } }))
|
||||
|
||||
expect(computeVisibleWorktreesCalls.count).toBe(callsAfterFirstRender)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
getPairedDeviceIdsByEnvironment
|
||||
} from '../../workspace-creator-visibility'
|
||||
import {
|
||||
EMPTY_STRUCTURED_CHAT_WORKTREE_IDS,
|
||||
getVisibleWorktreeBrowserActivityTabs,
|
||||
getVisibleWorktreeTerminalActivityTabs,
|
||||
getWorktreeIdsWithStructuredChat
|
||||
@@ -72,7 +73,9 @@ export function useVisibleSidebarWorktrees(args: {
|
||||
!showSleepingWorkspaces ? getVisibleWorktreeBrowserActivityTabs(s.browserTabsByWorktree) : null
|
||||
)
|
||||
const worktreeIdsWithStructuredChat = useAppStore((s) =>
|
||||
getWorktreeIdsWithStructuredChat(s.unifiedTabsByWorktree)
|
||||
showSleepingWorkspaces
|
||||
? EMPTY_STRUCTURED_CHAT_WORKTREE_IDS
|
||||
: getWorktreeIdsWithStructuredChat(s.unifiedTabsByWorktree)
|
||||
)
|
||||
|
||||
const recomputedVisibleWorktrees = useMemo(() => {
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
isCliCreatedWorkspace,
|
||||
isDetachedHeadWorkspace,
|
||||
isSleepingSweepExemptWorkspace,
|
||||
EMPTY_STRUCTURED_CHAT_WORKTREE_IDS,
|
||||
getWorktreeIdsWithStructuredChat
|
||||
} from '@/components/sidebar/visible-worktrees'
|
||||
import { isDefaultBranchWorkspace } from '@/components/sidebar/default-branch-workspace'
|
||||
@@ -96,6 +97,9 @@ export function useWorktreeJumpPaletteWorktrees({
|
||||
: EMPTY_PAIRED_DEVICE_IDS_BY_ENVIRONMENT,
|
||||
[hideWorkspacesFromOtherDevices, runtimeEnvironments, runtimeStatusByEnvironmentId]
|
||||
)
|
||||
const worktreeIdsWithStructuredChat = showSleepingWorkspaces
|
||||
? EMPTY_STRUCTURED_CHAT_WORKTREE_IDS
|
||||
: getWorktreeIdsWithStructuredChat(unifiedTabsByWorktree)
|
||||
const emptyQueryVisibleWorktrees = useMemo(
|
||||
() =>
|
||||
allWorktrees.filter((worktree) => {
|
||||
@@ -132,7 +136,7 @@ export function useWorktreeJumpPaletteWorktrees({
|
||||
ptyIdsByTabId,
|
||||
browserTabsByWorktree,
|
||||
worktreeIdsWithLiveAgent,
|
||||
getWorktreeIdsWithStructuredChat(unifiedTabsByWorktree)
|
||||
worktreeIdsWithStructuredChat
|
||||
)
|
||||
) {
|
||||
return false
|
||||
@@ -154,7 +158,7 @@ export function useWorktreeJumpPaletteWorktrees({
|
||||
showSleepingWorkspaces,
|
||||
tabsByWorktree,
|
||||
worktreeIdsWithLiveAgent,
|
||||
unifiedTabsByWorktree
|
||||
worktreeIdsWithStructuredChat
|
||||
]
|
||||
)
|
||||
const { visibleWorktreesForState, switchableWorktreesForRows } = useMemo(
|
||||
|
||||
Reference in New Issue
Block a user