diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-resources-context/mothership-resources-context.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-resources-context/mothership-resources-context.tsx
index ddc208f17ef..0c8a75dd428 100644
--- a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-resources-context/mothership-resources-context.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-resources-context/mothership-resources-context.tsx
@@ -1,10 +1,8 @@
'use client'
import { createContext, type ReactNode, useContext, useMemo } from 'react'
-import type {
- MothershipResource,
- MothershipResourceType,
-} from '@/app/workspace/[workspaceId]/home/types'
+import type { ChatResourceAddress } from '@/lib/mothership/resources/types'
+import type { MothershipResource } from '@/app/workspace/[workspaceId]/home/types'
/**
* Resource-management operations for the Mothership resource panel. Provided
@@ -18,11 +16,7 @@ interface MothershipResourcesContextValue {
/** Adds a resource to the panel and activates it. */
addResource: (resource: MothershipResource) => void
/** Removes a resource from the panel. */
- removeResource: (
- resourceType: MothershipResourceType,
- resourceId: string,
- workspaceId?: string
- ) => void
+ removeResource: (resource: ChatResourceAddress) => void
/** Replaces the resource list with a new ordering. */
reorderResources: (resources: MothershipResource[]) => void
/** Collapses the resource panel. */
diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/index.ts b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/index.ts
index 3eafad11021..b2223026cdd 100644
--- a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/index.ts
+++ b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/index.ts
@@ -1,5 +1,5 @@
export { AddResourceDropdown, useAvailableResources } from './add-resource-dropdown'
-export { ResourceActions, ResourceContent } from './resource-content'
+export { EmbeddedProjectFile, ResourceActions, ResourceContent } from './resource-content'
export type { ResourceTypeConfig } from './resource-registry'
export {
getResourceConfig,
diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/index.ts b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/index.ts
index 9dd2b9e9da3..48a7e6131ad 100644
--- a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/index.ts
+++ b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/index.ts
@@ -1 +1 @@
-export { ResourceActions, ResourceContent } from './resource-content'
+export { EmbeddedProjectFile, ResourceActions, ResourceContent } from './resource-content'
diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/resource-content.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/resource-content.tsx
index ee06410f14c..f6bb90f4623 100644
--- a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/resource-content.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/resource-content.tsx
@@ -35,6 +35,7 @@ import type { FilePreviewSession } from '@/lib/mothership/request/session'
import { stopRunToolForExecution } from '@/lib/mothership/tools/client/run-tool-execution'
import { canonicalWorkspaceFilePath } from '@/lib/mothership/vfs/path-utils'
import { type FileDownloadSource, triggerFileDownload } from '@/lib/uploads/client/download'
+import type { WorkspaceFileRecord } from '@/lib/uploads/contexts/workspace'
import { getFileExtension, getMimeTypeFromExtension } from '@/lib/uploads/utils/file'
import {
FileViewer,
@@ -67,6 +68,7 @@ import { useUsageLimits } from '@/app/workspace/[workspaceId]/w/[workflowId]/com
import { useWorkflowExecution } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution'
import { useFolders } from '@/hooks/queries/folders'
import { useLogByExecutionId, useLogDetail } from '@/hooks/queries/logs'
+import { useProjectFile } from '@/hooks/queries/projects'
import { exportTable } from '@/hooks/queries/tables'
import { fetchWorkflowEnvelope } from '@/hooks/queries/utils/fetch-workflow-envelope'
import { workflowKeys } from '@/hooks/queries/utils/workflow-keys'
@@ -74,7 +76,10 @@ import { mapWorkflow } from '@/hooks/queries/utils/workflow-list-query'
import { useWorkflowTest } from '@/hooks/queries/workflow-tests'
import { useWorkflows, WORKFLOW_STATE_STALE_TIME } from '@/hooks/queries/workflows'
import { useAddressedWorkspaceFileRecord, useWorkspaceFiles } from '@/hooks/queries/workspace-files'
-import { createWorkspaceFileContentSource } from '@/hooks/use-file-content-source'
+import {
+ createProjectFileContentSource,
+ createWorkspaceFileContentSource,
+} from '@/hooks/use-file-content-source'
import { useSettingsNavigation } from '@/hooks/use-settings-navigation'
import { useExecutionStore } from '@/stores/execution/store'
import { useTableViewPinStore } from '@/stores/table/view-pin/store'
@@ -98,6 +103,16 @@ const LOADING_SKELETON = (
)
+const FILE_NOT_FOUND = (
+
+
+
+
File not found
+
This file may have been deleted or moved
+
+
+)
+
/**
* Opens an internal app link the way the host expects: a new browser tab on the
* web, and the current view in the desktop app, whose shell would otherwise turn
@@ -945,19 +960,7 @@ function EmbeddedFile({
if (!file && (listLoading || detail.isFetching)) return LOADING_SKELETON
- if (!file) {
- return (
-
-
-
-
File not found
-
- This file may have been deleted or moved
-
-
-
- )
- }
+ if (!file) return FILE_NOT_FOUND
return (
@@ -983,6 +986,44 @@ function EmbeddedFile({
)
}
+interface EmbeddedProjectFileProps {
+ projectId: string
+ fileId: string
+}
+
+/**
+ * Read-only view of a Project-owned file. Bytes come through the Project file routes, so the viewer
+ * never addresses a workspace with a Project id; editing stays on the Project surface.
+ */
+export function EmbeddedProjectFile({ projectId, fileId }: EmbeddedProjectFileProps) {
+ const { data: projectFile, isLoading } = useProjectFile(projectId, fileId)
+ const contentSource = useMemo(
+ () => createProjectFileContentSource(projectId, fileId),
+ [projectId, fileId]
+ )
+ const file = useMemo
(
+ () => projectFile && { ...projectFile, workspaceId: projectId },
+ [projectFile, projectId]
+ )
+
+ if (isLoading) return LOADING_SKELETON
+ if (!file) return FILE_NOT_FOUND
+
+ return (
+
+
+
+ )
+}
+
interface EmbeddedFolderProps {
workspaceId: string
folderId: string
diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-tabs/resource-tabs.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-tabs/resource-tabs.tsx
index e2cb65ed72d..f892938a589 100644
--- a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-tabs/resource-tabs.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-tabs/resource-tabs.tsx
@@ -460,7 +460,7 @@ export function ResourceTabs({
)
continue
}
- onRemoveResource(r.type, r.id, r.workspaceId)
+ onRemoveResource(r)
if (r.type === 'browser') {
sendBrowserPanelAction('close-tab', { tabId: r.id }, desktopScopeId)
}
diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/mothership-view.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/mothership-view.tsx
index be794b83269..e36605cac7c 100644
--- a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/mothership-view.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/mothership-view.tsx
@@ -5,7 +5,10 @@ import { cn } from '@sim/emcn'
import type { WorkspaceSearchFilters } from '@/lib/api/contracts/knowledge'
import type { MothershipTableViewContext } from '@/lib/api/contracts/mothership-resources'
import type { FilePreviewSession } from '@/lib/mothership/request/session'
-import { getChatResourceSelectionId } from '@/lib/mothership/resources/types'
+import {
+ getChatResourceSelectionId,
+ getChatResourceWorkspaceId,
+} from '@/lib/mothership/resources/types'
import type { FileDownloadSource } from '@/lib/uploads/client/download'
import { getFileExtension } from '@/lib/uploads/utils/file'
import { SIM_PAGE_CONTENT_TYPE } from '@/lib/workspace-files/page-compile'
@@ -34,7 +37,7 @@ import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/provide
import { useWorkspacePermissionsQuery } from '@/hooks/queries/workspace'
import { useWorkspaceFiles } from '@/hooks/queries/workspace-files'
import { useUserPermissions } from '@/hooks/use-user-permissions'
-import { ResourceActions, ResourceContent, ResourceTabs } from './components'
+import { EmbeddedProjectFile, ResourceActions, ResourceContent, ResourceTabs } from './components'
/**
* Panels that are kept mounted across resource switches rather than rebuilt.
@@ -130,7 +133,7 @@ export const MothershipView = memo(
ref
) {
const active = resources.find((r) => getChatResourceSelectionId(r) === activeResourceId) ?? null
- const activeWorkspaceId = active?.workspaceId ?? workspaceId
+ const activeWorkspaceId = active ? getChatResourceWorkspaceId(active, workspaceId) : workspaceId
const inheritedPermissions = useUserPermissionsContext()
const permissions = useWorkspacePermissionsQuery(organizationId ? activeWorkspaceId : undefined)
const scopedPermissions = useUserPermissions(
@@ -206,6 +209,7 @@ export const MothershipView = memo(
const isActiveFilePreviewable =
canEdit &&
active?.type === 'file' &&
+ Boolean(activeWorkspaceId) &&
// Dashboards store extensionless names, so the record's type decides once it loads.
(activeFile
? isPreviewable(activeFile)
@@ -316,7 +320,9 @@ export const MothershipView = memo(
previewSession={previewForActive}
isAgentResponding={isAgentResponding}
previewContextKey={chatId}
- onNotFound={(resourceId) => removeResource('log', resourceId, active.workspaceId)}
+ onNotFound={(resourceId) =>
+ removeResource({ type: 'log', id: resourceId, workspaceId: active.workspaceId })
+ }
/>
)}
{!active && (
@@ -345,6 +351,10 @@ function ScopedResourceContent({
}) {
if (props.resource.type === 'search')
return
+ if (props.resource.type === 'file' && props.resource.owner?.entityType === 'project')
+ return (
+
+ )
if (!workspaceId) {
if (props.resource.type === 'generic') return
if (props.resource.type === 'browser')
diff --git a/apps/sim/app/workspace/[workspaceId]/home/home.tsx b/apps/sim/app/workspace/[workspaceId]/home/home.tsx
index 9cbebd085ea..7ecc6146c1d 100644
--- a/apps/sim/app/workspace/[workspaceId]/home/home.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/home/home.tsx
@@ -351,7 +351,7 @@ function HomeContent({ chatId, userName, userId }: HomeProps) {
return otherResolved?.type === resolved.type && otherResolved.id === resolved.id
})
if (stillReferenced) return
- removeResource(resolved.type, resolved.id)
+ removeResource(resolved)
}
function openWorkspaceResource(resource: MothershipResource) {
diff --git a/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/handle-complete-event.ts b/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/handle-complete-event.ts
index 2b2742fd60d..c2cf8544492 100644
--- a/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/handle-complete-event.ts
+++ b/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/handle-complete-event.ts
@@ -24,6 +24,6 @@ export function handleCompleteEvent(ctx: StreamLoopContext, parsed: CompleteEven
(item) => getChatResourceKey(item) === getChatResourceKey(streamedSearch)
)
if (streamedSearch && !wasVisible) {
- ctx.deps.removeResource('search', streamedSearch.id, streamedSearch.workspaceId)
+ ctx.deps.removeResource(streamedSearch)
}
}
diff --git a/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/handle-resource-event.test.ts b/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/handle-resource-event.test.ts
index e0f457806eb..a603f404210 100644
--- a/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/handle-resource-event.test.ts
+++ b/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/handle-resource-event.test.ts
@@ -85,7 +85,11 @@ describe('handleResourceEvent removal', () => {
handleResourceEvent(ctx, removeEvent('workflow', 'wf-1'))
- expect(deps.removeResource).toHaveBeenCalledWith('workflow', 'wf-1')
+ expect(deps.removeResource).toHaveBeenCalledWith({
+ type: 'workflow',
+ id: 'wf-1',
+ title: 'wf-1',
+ })
expect(mocks.removeWorkflowFromActiveCache).toHaveBeenCalledWith(
deps.queryClient,
'ws-1',
diff --git a/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/handle-resource-event.ts b/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/handle-resource-event.ts
index 1930055f347..83b5590b7c4 100644
--- a/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/handle-resource-event.ts
+++ b/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/handle-resource-event.ts
@@ -29,6 +29,7 @@ import {
mothershipChatKeys,
refreshMothershipChatDetail,
} from '@/hooks/queries/mothership-chats'
+import { invalidateProjectFileQueries } from '@/hooks/queries/projects'
import { knowledgeKeys } from '@/hooks/queries/utils/knowledge-keys'
import { removeWorkflowFromActiveCache } from '@/hooks/queries/utils/workflow-cache'
import { useTableViewPinStore } from '@/stores/table/view-pin/store'
@@ -189,6 +190,9 @@ export function handleResourceEvent(ctx: StreamLoopContext, parsed: ResourceEven
onResourceEvent?.(getChatResourceSelectionId(resource))
return
}
+ const owner = 'owner' in payload.resource ? payload.resource.owner : undefined
+ if (owner?.entityType === 'project' && payload.resource.type === 'file' && payload.resource.id)
+ invalidateProjectFileQueries(queryClient, owner.entityId, payload.resource.id)
const workspaceId = payload.resource.workspaceId ?? chatWorkspaceId
if (!workspaceId || (chatWorkspaceId && workspaceId !== chatWorkspaceId)) return
// Browser and terminal tabs are projected from the desktop app's live
@@ -203,8 +207,8 @@ export function handleResourceEvent(ctx: StreamLoopContext, parsed: ResourceEven
if (payload.op === 'remove' && payload.effectId) {
// The server applied this removal before publishing it, so a client add
// still queued for the resource must not restore it, live or on recovery.
- const { type, id } = payload.resource
- ctx.deps.discardPendingResourceUpsert({ type, id, workspaceId: payload.resource.workspaceId })
+ const { type, id, workspaceId: resourceWorkspaceId, owner } = payload.resource
+ ctx.deps.discardPendingResourceUpsert({ type, id, workspaceId: resourceWorkspaceId, owner })
}
if (payload.replay || ctx.deps.options.deferFlushes) {
refreshRecoveredChatResources(ctx)
@@ -259,8 +263,7 @@ export function handleResourceEvent(ctx: StreamLoopContext, parsed: ResourceEven
ctx.deps.setActiveResourceId((current) =>
current === getChatResourceSelectionId(resource) ? null : current
)
- } else if (resource.workspaceId) removeResource(resourceType, resource.id, resource.workspaceId)
- else removeResource(resourceType, resource.id)
+ } else removeResource(resource)
if (resourceType === 'workflow') {
removeWorkflowFromActiveCache(queryClient, workspaceId, resource.id)
}
diff --git a/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/stream-context.ts b/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/stream-context.ts
index 270fa35fa2e..7dd84f357f5 100644
--- a/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/stream-context.ts
+++ b/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/stream-context.ts
@@ -5,7 +5,10 @@ import type { RevealedSimKeysByMessage } from '@/lib/mothership/chat/sim-key-red
import { captureRevealedSimKeys } from '@/lib/mothership/chat/sim-key-redaction'
import type { SyntheticFilePreviewPayload } from '@/lib/mothership/request/session'
import type { FilePreviewSession } from '@/lib/mothership/request/session/file-preview-session-contract'
-import type { MothershipResourceUpdate } from '@/lib/mothership/resources/types'
+import type {
+ ChatResourceAddress,
+ MothershipResourceUpdate,
+} from '@/lib/mothership/resources/types'
import {
createTurnModel,
type TurnModel,
@@ -20,7 +23,6 @@ import type {
ChatMessage,
ContentBlock,
MothershipResource,
- MothershipResourceType,
} from '@/app/workspace/[workspaceId]/home/types'
import {
type MothershipChatHistory,
@@ -116,15 +118,9 @@ export interface StreamLoopDeps {
setActiveResourceId: Dispatch>
addResource: (resource: MothershipResourceUpdate) => boolean
- removeResource: (
- resourceType: MothershipResourceType,
- resourceId: string,
- workspaceId?: string
- ) => void
+ removeResource: (resource: ChatResourceAddress) => void
/** Drops a queued client write that would re-add a resource the server already removed. */
- discardPendingResourceUpsert: (
- resource: Pick
- ) => void
+ discardPendingResourceUpsert: (resource: ChatResourceAddress) => void
startClientWorkflowTool: (id: string, name: string, args: Record) => void
startClientLocalFilesystemTool: (id: string, name: string, args: Record) => void
startClientComputerTool: (id: string, args: Record, ts?: string) => void
diff --git a/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.dom.test.tsx b/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.dom.test.tsx
index c8c733515d7..d1b78592733 100644
--- a/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.dom.test.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.dom.test.tsx
@@ -95,7 +95,10 @@ import {
type MothershipStreamV1EventEnvelope,
} from '@/lib/mothership/generated/mothership-stream-v1'
import type { PersistedStreamEventEnvelope } from '@/lib/mothership/request/session/contract'
-import { getChatResourceSelectionId } from '@/lib/mothership/resources/types'
+import {
+ getChatResourceSelectionId,
+ type MothershipResource,
+} from '@/lib/mothership/resources/types'
import { ChatSurfaceProvider } from '@/app/workspace/[workspaceId]/home/components/chat-surface-context'
import { collectCitedMessageSources } from '@/app/workspace/[workspaceId]/home/components/message-content/message-sources'
import { ModelSelector } from '@/app/workspace/[workspaceId]/home/components/user-input/components/model-selector'
@@ -938,6 +941,84 @@ describe('useChat remount send recovery', () => {
})
})
+ describe('closing a file tab', () => {
+ const projectFile: MothershipResource = {
+ type: 'file',
+ id: 'shared-file',
+ title: 'brief.md',
+ owner: { entityType: 'project', entityId: 'project-1' },
+ }
+ const workspaceFile: MothershipResource = {
+ type: 'file',
+ id: 'shared-file',
+ title: 'notes.md',
+ workspaceId: 'ws-1',
+ owner: { entityType: 'workspace', entityId: 'ws-1' },
+ }
+
+ /** Opens a chat holding `resources` and records each resource DELETE body. */
+ function renderChatRecordingRemovals(resources: MothershipResource[], selected?: string) {
+ const history: MothershipChatHistory = {
+ id: 'chat-file-tabs',
+ title: 'Files',
+ messages: [],
+ activeStreamId: null,
+ resources,
+ }
+ const removals: unknown[] = []
+ mockRequestJson.mockImplementation(async (contract: AnyApiRouteContract, input) => {
+ if (contract.path !== '/api/mothership/chat/resources') return { chats: [] }
+ removals.push((input as { body: unknown }).body)
+ return { resources: [] }
+ })
+ const { getResult } = renderUseChatInChat(history.id, history, undefined, selected)
+ return { history, removals, getResult }
+ }
+
+ it('closes a Project file tab by its owner and leaves a workspace file with the same id', async () => {
+ const { history, removals, getResult } = renderChatRecordingRemovals(
+ [projectFile, workspaceFile],
+ getChatResourceSelectionId(projectFile)
+ )
+
+ await act(async () => {
+ getResult().removeResource(projectFile)
+ await sleep(0)
+ })
+
+ expect(removals).toEqual([
+ {
+ chatId: history.id,
+ resourceType: 'file',
+ resourceId: 'shared-file',
+ owner: { entityType: 'project', entityId: 'project-1' },
+ },
+ ])
+ expect(getResult().resources).toEqual([workspaceFile])
+ expect(getResult().activeResourceId).not.toBe(getChatResourceSelectionId(projectFile))
+ })
+
+ it('closes a workspace file tab named only by type and id, deleting it as stored', async () => {
+ const { history, removals, getResult } = renderChatRecordingRemovals([workspaceFile])
+
+ await act(async () => {
+ getResult().removeResource({ type: 'file', id: 'shared-file' })
+ await sleep(0)
+ })
+
+ expect(removals).toEqual([
+ {
+ chatId: history.id,
+ resourceType: 'file',
+ resourceId: 'shared-file',
+ workspaceId: 'ws-1',
+ owner: { entityType: 'workspace', entityId: 'ws-1' },
+ },
+ ])
+ expect(getResult().resources).toEqual([])
+ })
+ })
+
it('restores an explicitly selected Search tab while reconnecting an active turn', async () => {
const shape = resolveDeploymentShape()
seedDeploymentShape({ ...shape, features: shape.features })
diff --git a/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts b/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts
index c8879bc4ca0..e8c28ad699d 100644
--- a/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts
+++ b/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts
@@ -70,6 +70,7 @@ import type { FilePreviewSession } from '@/lib/mothership/request/session/file-p
import { canDisplayResource } from '@/lib/mothership/resources/availability'
import { ResourcePersistenceQueue } from '@/lib/mothership/resources/client-persistence-queue'
import {
+ type ChatResourceAddress,
chatResourceKeyFor,
getChatResourceSelectionId,
isAddressableResource,
@@ -162,7 +163,6 @@ import type {
ContentBlock,
FileAttachmentForApi,
MothershipResource,
- MothershipResourceType,
QueuedMessage,
SendPayload,
ToolCallInfo,
@@ -319,11 +319,7 @@ export interface UseChatReturn {
setActiveResourceId: (id: string | null) => void
addResource: (resource: MothershipResourceUpdate) => boolean
setTableViewContext: (tableId: string, context: MothershipTableViewContext) => void
- removeResource: (
- resourceType: MothershipResourceType,
- resourceId: string,
- workspaceId?: string
- ) => void
+ removeResource: (resource: ChatResourceAddress) => void
reorderResources: (resources: MothershipResource[]) => void
messageQueue: QueuedMessage[]
removeFromQueue: (id: string) => void
@@ -818,6 +814,8 @@ export function useChat(
* make the tabs disappear for the desktop app too.
*/
const undisplayableResourcesRef = useRef([])
+ const workspaceIdRef = useRef(workspaceId)
+ workspaceIdRef.current = workspaceId
const resourcePersistenceQueueRef = useRef(null)
const pendingResourceReordersRef = useRef