Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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. */
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export { ResourceActions, ResourceContent } from './resource-content'
export { EmbeddedProjectFile, ResourceActions, ResourceContent } from './resource-content'
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -67,14 +68,18 @@ 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'
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'
Expand All @@ -98,6 +103,16 @@ const LOADING_SKELETON = (
</div>
)

const FILE_NOT_FOUND = (
<div className='flex h-full flex-col items-center justify-center gap-3'>
<FileX className='size-[32px] text-[var(--text-icon)]' />
<div className='flex flex-col items-center gap-1'>
<h2 className='text-[20px] text-[var(--text-primary)]'>File not found</h2>
<p className='text-[var(--text-body)] text-small'>This file may have been deleted or moved</p>
</div>
</div>
)

/**
* 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
Expand Down Expand Up @@ -945,19 +960,7 @@ function EmbeddedFile({

if (!file && (listLoading || detail.isFetching)) return LOADING_SKELETON

if (!file) {
return (
<div className='flex h-full flex-col items-center justify-center gap-3'>
<FileX className='size-[32px] text-[var(--text-icon)]' />
<div className='flex flex-col items-center gap-1'>
<h2 className='text-[20px] text-[var(--text-primary)]'>File not found</h2>
<p className='text-[var(--text-body)] text-small'>
This file may have been deleted or moved
</p>
</div>
</div>
)
}
if (!file) return FILE_NOT_FOUND

return (
<div className='flex h-full flex-col overflow-hidden'>
Expand All @@ -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<WorkspaceFileRecord | undefined>(
() => projectFile && { ...projectFile, workspaceId: projectId },
[projectFile, projectId]
)

if (isLoading) return LOADING_SKELETON
if (!file) return FILE_NOT_FOUND

return (
<div className='flex h-full flex-col overflow-hidden'>
<FileViewer
key={file.id}
file={file}
workspaceId={projectId}
contentSource={contentSource}
Comment thread
waleedlatif1 marked this conversation as resolved.
canEdit={false}
readOnly
enableFind
/>
</div>
)
}

interface EmbeddedFolderProps {
workspaceId: string
folderId: string
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 && (
Expand Down Expand Up @@ -345,6 +351,10 @@ function ScopedResourceContent({
}) {
if (props.resource.type === 'search')
return <SearchResourceContent resource={props.resource} onSummarize={onSummarize} />
if (props.resource.type === 'file' && props.resource.owner?.entityType === 'project')
return (
<EmbeddedProjectFile projectId={props.resource.owner.entityId} fileId={props.resource.id} />
)
if (!workspaceId) {
if (props.resource.type === 'generic') return <GenericResourceContent />
if (props.resource.type === 'browser')
Expand Down
2 changes: 1 addition & 1 deletion apps/sim/app/workspace/[workspaceId]/home/home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -20,7 +23,6 @@ import type {
ChatMessage,
ContentBlock,
MothershipResource,
MothershipResourceType,
} from '@/app/workspace/[workspaceId]/home/types'
import {
type MothershipChatHistory,
Expand Down Expand Up @@ -116,15 +118,9 @@ export interface StreamLoopDeps {
setActiveResourceId: Dispatch<SetStateAction<string | null>>

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<MothershipResource, 'type' | 'id' | 'workspaceId'>
) => void
discardPendingResourceUpsert: (resource: ChatResourceAddress) => void
startClientWorkflowTool: (id: string, name: string, args: Record<string, unknown>) => void
startClientLocalFilesystemTool: (id: string, name: string, args: Record<string, unknown>) => void
startClientComputerTool: (id: string, args: Record<string, unknown>, ts?: string) => void
Expand Down
Loading
Loading