Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
947472b
v0.6.29: login improvements, posthog telemetry (#4026)
TheodoreSpeaks Apr 7, 2026
befeac9
feat(files): folders + vfs update
icecrasher321 May 12, 2026
c76f7ae
address comments
icecrasher321 May 12, 2026
fe11df2
address comments
icecrasher321 May 12, 2026
3601e93
cleanup unnused code
icecrasher321 May 12, 2026
237461c
address comments
icecrasher321 May 12, 2026
6031ddd
perf improvements
icecrasher321 May 13, 2026
20971a7
address next set
icecrasher321 May 13, 2026
4055ce0
cycle detect
icecrasher321 May 13, 2026
c2880c5
error handling
icecrasher321 May 13, 2026
26cfe57
path improvements
icecrasher321 May 13, 2026
7f1a01f
cleanup, best practices
waleedlatif1 May 13, 2026
dcd6d28
react query best practices: targeted invalidation, optimistic updates…
waleedlatif1 May 13, 2026
99b0fe4
add shift-click range selection and selection-aware context menu for …
waleedlatif1 May 13, 2026
71858be
add Move submenu to file context menu and fix shift-click anchor update
waleedlatif1 May 13, 2026
339835c
fix move submenu: use folder names with tree-ordered indentation inst…
waleedlatif1 May 13, 2026
8190daa
fix shift-click anchor drift and remove dead stopPropagation constant
waleedlatif1 May 13, 2026
a5c27a1
complete workspace files feature: audit logs, posthog events, folder …
waleedlatif1 May 13, 2026
9e3085e
cleanup: accessibility, emcn design tokens, react best practices acro…
waleedlatif1 May 13, 2026
ad894d7
fix audit and posthog: FOLDER_RESTORED action on restore, fire folder…
waleedlatif1 May 13, 2026
acf24cb
sidebar: add Files section with nested folder tree; polish move UX an…
waleedlatif1 May 13, 2026
557a1c8
remove Files section from sidebar
waleedlatif1 May 13, 2026
e4425bf
restore Files nav item in sidebar workspace section
waleedlatif1 May 13, 2026
aee7f05
fix infinite re-render on files page - revert selection pruning to us…
waleedlatif1 May 13, 2026
cae5b8e
add filefolder resource type for ingesting workspace file folders
waleedlatif1 May 13, 2026
c174d81
export filefolder tree types; add toast feedback for file/folder muta…
waleedlatif1 May 13, 2026
9eb2d37
regenerate migration as 0208 after rebase onto staging
waleedlatif1 May 13, 2026
40b05f2
add workspaceFileFolder to schema mock
waleedlatif1 May 14, 2026
5f2f639
add FILE_MOVED, FOLDER_MOVED, FOLDER_UPDATED to audit mock
waleedlatif1 May 14, 2026
7ab80b5
add filefolder ChatContext kind and wire through schema and resolver
waleedlatif1 May 14, 2026
eebf06d
add filefolder to AgentContextType
waleedlatif1 May 14, 2026
9afcf77
add filefolder to chat context kind registry; fix resolver to use wor…
waleedlatif1 May 14, 2026
2e05ec9
add .deepsec to gitignore
waleedlatif1 May 14, 2026
ddf6297
cleanup: effect, emcn tokens, mutation error handling
waleedlatif1 May 14, 2026
dbce404
lint
waleedlatif1 May 14, 2026
ad68624
fix: remove duplicate handleCopilotStopGeneration from rebase
waleedlatif1 May 14, 2026
7c08719
feat(copilot): folder-aware file context in WORKSPACE.md
waleedlatif1 May 14, 2026
4c64e60
feat(copilot): add move operation to file manage API
waleedlatif1 May 14, 2026
a6556a5
fix(files): make targetFolder optional in move file contract
waleedlatif1 May 14, 2026
d52bdf6
perf(files): parallelize buffer fetches, fix N+1 folder queries, stab…
waleedlatif1 May 14, 2026
13c4f18
fix(files): remove files/ path stripping, fix stale path in optimisti…
waleedlatif1 May 14, 2026
9525ac0
fix(files): revert broken ref opt, clean 409 on restore, null parentI…
waleedlatif1 May 14, 2026
98cf4d3
feat(search): show folder path for files in cmd-k modal, strip extran…
waleedlatif1 May 14, 2026
4fae386
fix(workspace-files): audit fixes — transaction, status codes, contra…
waleedlatif1 May 14, 2026
b4cfd29
fix(vfs): pass folderPath separately so buildWorkspaceMd groups files…
waleedlatif1 May 14, 2026
65dcb06
fix(types): narrow unknown fileInput with Record cast after object guard
waleedlatif1 May 14, 2026
6c52018
fix(routes): replace instanceof Error with toError() across new works…
waleedlatif1 May 14, 2026
56fc9b8
improvement(files): cleanup pass — remove unnecessary useCallbacks, c…
waleedlatif1 May 14, 2026
90a6050
fix(files): apply activeSort to folders, reject drop onto current par…
waleedlatif1 May 14, 2026
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,4 @@ i18n.cache
.claude/launch.json
.claude/worktrees/
.claude/scheduled_tasks.lock
.deepsec/
4 changes: 4 additions & 0 deletions apps/sim/app/(landing)/components/auth-modal/auth-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
Modal,
ModalClose,
ModalContent,
ModalDescription,
ModalTitle,
ModalTrigger,
} from '@/components/emcn'
Expand Down Expand Up @@ -134,6 +135,9 @@ export function AuthModal({ children, defaultView = 'login', source }: AuthModal
<ModalTitle className='sr-only'>
{view === 'login' ? 'Log in' : 'Create account'}
</ModalTitle>
<ModalDescription className='sr-only'>
{view === 'login' ? 'Sign in to your account' : 'Create a new account'}
</ModalDescription>

<div className='relative px-6 pt-6 pb-6'>
<ModalClose className='absolute top-6 right-6 rounded-sm opacity-70 transition-opacity hover:opacity-100'>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
Modal,
ModalBody,
ModalContent,
ModalDescription,
ModalFooter,
ModalHeader,
ModalTrigger,
Expand Down Expand Up @@ -152,6 +153,9 @@ export function DemoRequestModal({ children, theme = 'dark' }: DemoRequestModalP
}
>
<ModalBody>
<ModalDescription className='sr-only'>
Fill out this form to request a demo and talk to the sales team
</ModalDescription>
<div className='space-y-3'>
<div className='grid gap-3 sm:grid-cols-2'>
<LandingField htmlFor='firstName' label='First name' error={errors.firstName}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
Modal,
ModalBody,
ModalContent,
ModalDescription,
ModalFooter,
ModalHeader,
Textarea,
Expand Down Expand Up @@ -83,6 +84,9 @@ export function RequestIntegrationModal() {

{status === 'success' ? (
<ModalBody>
<ModalDescription className='sr-only'>
Integration request submitted successfully
</ModalDescription>
<div className='flex flex-col items-center gap-3 py-6 text-center'>
<div className='flex size-10 items-center justify-center rounded-full bg-[#33C482]/10'>
<svg
Expand All @@ -106,6 +110,10 @@ export function RequestIntegrationModal() {
) : (
<form onSubmit={handleSubmit} className='flex min-h-0 flex-1 flex-col'>
<ModalBody>
<ModalDescription className='sr-only'>
Submit a request for a new integration by entering the integration name and your
email
</ModalDescription>
<div className='space-y-3'>
<div className='flex flex-col gap-1'>
<Label htmlFor='integration-name'>Integration name</Label>
Expand Down
57 changes: 47 additions & 10 deletions apps/sim/app/api/tools/file/manage/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,18 @@ import { type NextRequest, NextResponse } from 'next/server'
import { fileManageContract } from '@/lib/api/contracts/tools/file'
import { parseRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { splitWorkspaceFilePath } from '@/lib/copilot/tools/server/files/workspace-file'
import { acquireLock, releaseLock } from '@/lib/core/config/redis'
import { ensureAbsoluteUrl } from '@/lib/core/utils/urls'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import {
ensureWorkspaceFileFolderPath,
moveWorkspaceFileItems,
} from '@/lib/uploads/contexts/workspace/workspace-file-folder-manager'
import {
fetchWorkspaceFileBuffer,
getWorkspaceFile,
getWorkspaceFileByName,
resolveWorkspaceFileReference,
updateWorkspaceFileContent,
uploadWorkspaceFile,
} from '@/lib/uploads/contexts/workspace/workspace-file-manager'
Expand Down Expand Up @@ -49,11 +54,14 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
const selectedFileId =
fileId ||
(fileInput && typeof fileInput === 'object' && !Array.isArray(fileInput)
? typeof fileInput.id === 'string'
? fileInput.id
: typeof fileInput.fileId === 'string'
? fileInput.fileId
: ''
? (() => {
const obj = fileInput as Record<string, unknown>
return typeof obj.id === 'string'
? obj.id
: typeof obj.fileId === 'string'
? obj.fileId
: ''
})()
: '')

if (!selectedFileId) {
Expand Down Expand Up @@ -91,14 +99,21 @@ export const POST = withRouteHandler(async (request: NextRequest) => {

case 'write': {
const { fileName, content, contentType } = body
const mimeType = contentType || getMimeTypeFromExtension(getFileExtension(fileName))
const { folderSegments, leafName } = splitWorkspaceFilePath(fileName)
const folderId = await ensureWorkspaceFileFolderPath({
workspaceId,
userId,
pathSegments: folderSegments,
})
const mimeType = contentType || getMimeTypeFromExtension(getFileExtension(leafName))
const fileBuffer = Buffer.from(content ?? '', 'utf-8')
const result = await uploadWorkspaceFile(
workspaceId,
userId,
fileBuffer,
fileName,
mimeType
leafName,
mimeType,
{ folderId }
)

logger.info('File created', {
Expand All @@ -118,10 +133,32 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
})
}

case 'move': {
const { fileId, targetFolder } = body
const pathSegments = targetFolder.trim()
? targetFolder
.trim()
.split('/')
.map((s) => s.trim())
.filter(Boolean)
: []
const targetFolderId = await ensureWorkspaceFileFolderPath({
workspaceId,
userId,
pathSegments,
})
await moveWorkspaceFileItems({ workspaceId, fileIds: [fileId], targetFolderId })
logger.info('File moved', { fileId, targetFolder: targetFolder || '(root)' })
return NextResponse.json({
success: true,
data: { fileId, targetFolder: targetFolder || '(root)' },
})
}

case 'append': {
const { fileName, content } = body

const existing = await getWorkspaceFileByName(workspaceId, fileName)
const existing = await resolveWorkspaceFileReference(workspaceId, fileName)
if (!existing) {
return NextResponse.json(
{ success: false, error: `File not found: "${fileName}"` },
Expand Down
13 changes: 13 additions & 0 deletions apps/sim/app/api/workspaces/[id]/files/[fileId]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { getValidationErrorMessage, parseRequest } from '@/lib/api/server'
import { getSession } from '@/lib/auth'
import { generateRequestId } from '@/lib/core/utils/request'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { captureServerEvent } from '@/lib/posthog/server'
import {
deleteWorkspaceFile,
FileConflictError,
Expand Down Expand Up @@ -55,6 +56,12 @@ export const PATCH = withRouteHandler(

logger.info(`[${requestId}] Renamed workspace file: ${fileId} to "${updatedFile.name}"`)

captureServerEvent(
session.user.id,
'file_renamed',
{ workspace_id: workspaceId },
{ groups: { workspace: workspaceId } }
)
recordAudit({
workspaceId,
actorId: session.user.id,
Expand Down Expand Up @@ -124,6 +131,12 @@ export const DELETE = withRouteHandler(

logger.info(`[${requestId}] Archived workspace file: ${fileId}`)

captureServerEvent(
session.user.id,
'file_deleted',
{ workspace_id: workspaceId },
{ groups: { workspace: workspaceId } }
)
recordAudit({
workspaceId,
actorId: session.user.id,
Expand Down
76 changes: 76 additions & 0 deletions apps/sim/app/api/workspaces/[id]/files/bulk-archive/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit'
import { createLogger } from '@sim/logger'
import { toError } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { bulkArchiveWorkspaceFileItemsContract } from '@/lib/api/contracts/workspace-file-folders'
import { parseRequest } from '@/lib/api/server'
import { getSession } from '@/lib/auth'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { captureServerEvent } from '@/lib/posthog/server'
import { bulkArchiveWorkspaceFileItems } from '@/lib/uploads/contexts/workspace'
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'

const logger = createLogger('WorkspaceFileBulkArchiveAPI')

export const POST = withRouteHandler(
async (request: NextRequest, context: { params: Promise<{ id: string }> }) => {
const session = await getSession()
if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}

const parsed = await parseRequest(bulkArchiveWorkspaceFileItemsContract, request, context)
if (!parsed.success) return parsed.response
const { id: workspaceId } = parsed.data.params
const { fileIds, folderIds } = parsed.data.body

const permission = await getUserEntityPermissions(session.user.id, 'workspace', workspaceId)
if (permission !== 'admin' && permission !== 'write') {
return NextResponse.json({ error: 'Insufficient permissions' }, { status: 403 })
}

try {
const deletedItems = await bulkArchiveWorkspaceFileItems({ workspaceId, fileIds, folderIds })
captureServerEvent(
session.user.id,
'file_bulk_deleted',
{ workspace_id: workspaceId, file_count: fileIds.length, folder_count: folderIds.length },
{ groups: { workspace: workspaceId } }
)
if (fileIds.length > 0) {
recordAudit({
workspaceId,
actorId: session.user.id,
actorName: session.user.name,
actorEmail: session.user.email,
action: AuditAction.FILE_DELETED,
resourceType: AuditResourceType.FILE,
description: `Deleted ${fileIds.length} file${fileIds.length === 1 ? '' : 's'}`,
metadata: { fileIds },
})
}
if (folderIds.length > 0) {
recordAudit({
workspaceId,
actorId: session.user.id,
actorName: session.user.name,
actorEmail: session.user.email,
action: AuditAction.FOLDER_DELETED,
resourceType: AuditResourceType.FOLDER,
description: `Deleted ${folderIds.length} folder${folderIds.length === 1 ? '' : 's'}`,
metadata: { folderIds },
})
}
return NextResponse.json({ success: true, deletedItems })
} catch (error) {
logger.error('Failed to bulk archive workspace file items:', error)
return NextResponse.json(
{
success: false,
error: toError(error).message,
},
{ status: 400 }
)
}
}
)
Loading
Loading