Skip to content

feat(folders): cut file folders over, and give knowledge bases and tables folders - #6045

Merged
waleedlatif1 merged 11 commits into
stagingfrom
feat/folders-kb-tables
Jul 29, 2026
Merged

feat(folders): cut file folders over, and give knowledge bases and tables folders#6045
waleedlatif1 merged 11 commits into
stagingfrom
feat/folders-kb-tables

Conversation

@waleedlatif1

@waleedlatif1 waleedlatif1 commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

Summary

Builds on the generic resourceType-driven folder engine (#6037, already merged) to finish the file-folder migration and extend folders to two more resource trees, behind one shared UI module.

File-folder cutover. All 30 workspace_file_folders query sites now target folder scoped to resourceType = 'file' — id-keyed lookups included, because nothing stops a caller handing a workflow folder's id to a file-folder endpoint. Adds the restore-conflict handling the legacy path never had: a folder whose name was taken while it was archived now deduplicates against the resolved parent (restore re-roots when the original parent is still archived) instead of being permanently unrestorable.

Knowledge Bases and Tables get folders. knowledge_base.folder_id and user_table_definitions.folder_id are written and served end to end; every create/move admits a folder only through findActiveFolder, scoped to workspace and resourceType. Both pages get folder rows, breadcrumbs, inline rename on rows and on the breadcrumb, move-to submenus, pin/unpin, drag-a-row-onto-a-folder, and folders-first ordering — matching Files.

One shared folder module. components/folders/ backs Knowledge and Tables, and Files shares its move-option renderer: folderBreadcrumbItems, folderRow, folderRowId/parseFolderedRowId, buildMoveOptions / buildDescendantIndex / renderMoveOptions, nextUntitledFolderName, FolderContextMenu, useFolderNavigation, useFolderRowDragDrop. The Knowledge/Tables duplicates from the parallel build are collapsed into one of each; files/move-options.tsx is deleted. Breadcrumbs are data feeding Resource.Header, not a rival component — the header owns the root-crumb popover, width allocation, and the single-crumb-as-title rule.

Recently Deleted restores folders of every type, driven by one declarative table rather than a branch per resource.

Please read these three before approving

1. Restore is timestamp-matched — a deliberate behavior change on a shipped path. A folder delete stamps one shared timestamp across the subtree, and restore resurrects only rows carrying that exact timestamp. So a webhook or schedule archived independently before the folder was deleted stays archived through the folder's restore. That is the point — it stops a restore reviving siblings the user deleted on purpose — but it is a change in observable behavior for folder restore, which already ships.

2. Deploy ordering: this must go out at least one cycle after the engine (#6037). resourceType defaults to 'workflow' when omitted — that is what made the engine rolling-deploy safe. A value that is present but unrecognized is now rejected with a 400, never coerced (servedFolderResourceTypeSchema, pinned by tests), because coercing would file a knowledge-base folder into the workflow tree where the Knowledge page could never see it again. Residual rolling-window behavior: a new client talking to an old pod can have a folderId silently stripped by the old contract, so a knowledge base created inside a folder lands at the root. Recoverable by moving it; not corrupting.

3. File folders moved storage but kept their API. /api/folders deliberately does not serve resourceType: 'file'. The Files routes (/api/workspaces/[id]/files/folders/**) remain the only writer, because they serialize every mutation behind the workspace_file_folders:${workspaceId} advisory lock — name deliberately unchanged, so old and new pods take the same lock during the rolling deploy — and because they reject names containing /, \, . and .., which the generic contract allows and which would corrupt a file folder's path (a folder named a/b becomes unreachable by path, and breadcrumbs resolve it as two segments). This was briefly served during development and is now excluded, with a test pinning it. Converging the two engines is follow-up work.

Folder locking remains workflow-only by designsupportsLocking is declared on the workflow config alone, and a locked value on any other tree is dropped rather than persisted.

Migration 0274 — data loss without it, and it must run twice

0272's file-folder backfill is guarded by WHERE NOT EXISTS (… resource_type = 'file'), so it fires exactly once — and nothing has written those rows since: the deployed manager reads and writes workspace_file_folders exclusively. So folder's file rows are frozen at the 0272 snapshot while every rename, move, delete, and restore since then lives only in the legacy table.

Since the cutover repoints all reads at folder, without 0274: folders created after 0272 vanish and their contents become unreachable; folders renamed or moved after 0272 revert; folders deleted after 0272 reappear as active phantoms; and a stale-active row can hold the name a newer folder legitimately took.

0274 therefore reconciles rather than catches up — it parks mirrored names, then upserts the full row state from the authoritative legacy table. The conflict target is (id) on purpose: a name collision would mean the source violated its own unique index, and must fail loudly rather than be swallowed by a bare ON CONFLICT DO NOTHING, which would silently discard a folder and strand every file inside it.

⚠️ Operational step: run 0274 again once the deploy has fully drained. Old pods keep writing the legacy table until they are gone, so anything they write during the rolling window lands after the migration runs. Both statements are idempotent; a journaled migration does not replay, so this is manual.

packages/db/schema.ts previously described workspace_file_folders as "unread, unwritten … dropped in a follow-up contract migration". It was live until this PR. The comment now says what is actually true and names what must hold before anyone drops it.

Bugs fixed along the way (second commit)

  • Archived folders were mutable, bypassing the folder lock. The folder routes and updateFolder filtered id + resourceType but never deletedAt, while getFolderLockStatus does — so an archived-but-locked folder reported unlocked. Lock a subfolder, delete its parent, and any write-level member could rename and reparent it. DELETE deliberately still accepts an archived folder, because it reuses that folder's deletedAt to make a partial cascade retryable.
  • v1/admin/workflows/import wrote folderId with no validation at all (0272 dropped the FK), accepting another workspace's folder or a knowledge-base folder. The workflow then rendered under no sidebar node while still executing, billing, and escaping the folder delete cascade.
  • Folder restore did not undo folder delete. Delete routes through archiveWorkflow, which also disables schedules, webhooks, chats, and MCP tools; restore ran a bare archivedAt = null. Restoring a folder reported success while every schedule stayed disabled forever and chats 404'd. Deployment state is still deliberately not revived, matching single-workflow restore.
  • Cascade restore would have dumped every child at the workspace root. The canonical restores re-root a resource whose folder is archived — correct alone, wrong inside a cascade, which restores children before un-archiving the folders. resolveRestoredFolderId exempts the subtree being restored. restoreTable gained the re-root check it lacked entirely.
  • getFolderPath had no cycle guard, so a cycle in the client cache (reachable via useReorderFolders' unchecked optimistic write) hung the tab. Its twin already had one.
  • Sidebar "New folder" inside a folder hardcoded the name; the second one 409'd, was swallowed with only a log line, and the user got no folder and no message.
  • chunkedBatchDelete deleted by id with no re-check, so a resource restored between the SELECT and the DELETE was hard-deleted anyway, orphaning its cascade-restored children.
  • Pinned rows carry a pin glyph again — they sort to the top and had nothing explaining why since the inline pin button was removed.
  • v1/admin/workspaces/[id]/import returned 500 on a concurrent import sharing a folder path (unserialized SELECT-then-INSERT); it now adopts whichever folder won, since the name is server-chosen.
  • Folder duplicate returned 500 on a retry. newId is client-supplied, so replaying a duplicate whose response was lost hit the primary key rather than a name conflict. Now a 409.
  • Perf: knowledgeKeys moved to a keys-only module. Reading it from hooks/queries/kb/knowledge pulled a ~1000-line hook module (and the @sim/emcn barrel behind it) into hooks/queries/folders, which three server prefetches import — the same import edge the navigation-speed audit measured at 11.8MB per workspace route.
  • Merge drop: Knowledge never got the chrome Tables did. Its prefetch now fetches the folder tree alongside the bases (so the list does not paint ungrouped and a ?folderId= deep link is not an empty breadcrumb), and its loading fallback declares the "New folder" action.

Post-deploy runbook (two manual steps)

1. Re-run migration 0274 once the deploy has fully drained. Old pods keep writing
workspace_file_folders until they are gone, so anything written during the rolling window
lands after the migration ran. It is idempotent, but a journaled migration does not replay.
Precondition: run it before the soft-delete cleanup job purges expired folder rows —
cleanup hard-deletes from folder but never from the legacy table, so re-running afterwards
would reinstate purged folders as soft-deleted phantoms whose files are already gone. Retention
is far longer than any drain, so running promptly is sufficient.

2. Run the workflow-folder reconcile below, same timing, same precondition. 0272's
workflow backfill is one-shot (WHERE NOT EXISTS (… resource_type = 'workflow')) and its own
comment defers later rows to "the separate post-drain catch-up pass" — which exists for files
(0274) and not for workflows. Without this, a workflow folder an old pod creates during the
rolling window is stranded in workflow_folder: invisible in the sidebar, with its workflows
appearing at the workspace root. Deliberately not shipped as a migration — at deploy time it
is a no-op (0272 copies everything), so it only ever matters as a manual post-drain step.

It is insert-only, unlike 0274. workspace_file_folders enforces its own active-unique
key so file folders can be mirrored verbatim; workflow_folder never did, so 0272 deduplicated
as it copied. Upserting raw names back would undo those renames and collide on
folder_workspace_resource_parent_name_active_unique. Rows already in folder are therefore left
untouched — the new code has been writing them and is authoritative — and only ids absent from
folder are adopted, with dedup applied against both what the table already holds and what the
statement is about to insert.

Workflow-folder reconcile SQL (verified — see below)
DO $$
BEGIN
	INSERT INTO "folder" (id, resource_type, name, user_id, workspace_id, parent_id, locked, sort_order, created_at, updated_at, deleted_at)
	WITH missing AS (
		SELECT f.* FROM "workflow_folder" f
		WHERE NOT EXISTS (SELECT 1 FROM "folder" e WHERE e.id = f.id)
	), taken AS (
		SELECT workspace_id, coalesce(parent_id, '') AS scope, name
		FROM "folder" WHERE resource_type = 'workflow' AND deleted_at IS NULL
	), candidates AS (
		-- Archived rows are exempt: the unique index is partial on `deleted_at IS NULL`.
		SELECT id, workspace_id, coalesce(parent_id, '') AS scope, name, created_at
		FROM missing WHERE archived_at IS NULL
	), occupied AS (
		-- Dodge both what the table holds and what this statement is about to insert.
		SELECT workspace_id, scope, name FROM taken
		UNION
		SELECT workspace_id, scope, name FROM candidates
	), ranked AS (
		SELECT c.id, c.workspace_id, c.scope, c.name,
			ROW_NUMBER() OVER (PARTITION BY c.workspace_id, c.scope, c.name ORDER BY c.created_at, c.id) AS rn,
			EXISTS (SELECT 1 FROM taken t WHERE t.workspace_id = c.workspace_id
				AND t.scope = c.scope AND t.name = c.name) AS base_taken
		FROM candidates c
	), renamed AS (
		SELECT d.id, (
			-- First UNUSED suffix, not the row number: the app's own dedup produces this exact
			-- shape, so `name || ' (' || rn || ')'` can regenerate a name that already exists.
			SELECT d.name || ' (' || candidate.n || ')'
			FROM generate_series(1, 10000) AS candidate(n)
			WHERE NOT EXISTS (
				SELECT 1 FROM occupied o
				WHERE o.workspace_id = d.workspace_id AND o.scope = d.scope
					AND o.name = d.name || ' (' || candidate.n || ')')
			ORDER BY candidate.n
			-- If the bare name is already held, even rn = 1 needs a suffix.
			OFFSET (CASE WHEN d.base_taken THEN d.rn ELSE d.rn - 1 END) - 1
			LIMIT 1
		) AS new_name
		FROM ranked d WHERE d.base_taken OR d.rn > 1
	)
	SELECT f.id, 'workflow', coalesce(r.new_name, f.name),
		f.user_id, f.workspace_id, f.parent_id, f.locked, f.sort_order,
		f.created_at, f.updated_at, f.archived_at
	FROM missing f LEFT JOIN renamed r ON r.id = f.id
	ON CONFLICT (id) DO NOTHING;
END $$;

Verified the same way as 0274: a read-only production dry run (source rows and mirrored rows
match exactly, nothing for it to insert at deploy time, target state collision-free), and
end-to-end execution on Postgres 17.9 against fixtures built to break the dedup — a name already
held by an existing row, a name colliding with what 0272 renamed a row to, two new rows
sharing a name, a generated suffix that had to dodge a new sibling holding it, a case needing
three suffixes skipped, archived duplicates (correctly exempt), children ordered before parents,
same name in another workspace and another parent scope, unicode and 255-char names, and locked
carry-over. Every case produced the correct name; replaying it three times plus 0274 left the
state hash byte-identical.

Restore does not reactivate, on purpose

Archive overwrites schedule.status'disabled' with nextRunAt cleared, and
webhook/chat.isActivefalse, without recording what they were. Restore clears
archivedAt but does not set those back: a deliberately-disabled schedule or webhook is a
common state rather than an edge case, so restoring to a constant would re-enable something the
user had switched off, and re-run a 'completed' one-shot. Re-enabling stays explicit — a redeploy sets
status: 'active' — matching deployment state, which restore also leaves off. Making this
lossless requires archive to persist pre-archive state, which is a schema change on a shipped
path and belongs in its own PR. Net vs. staging: folder restore previously left dependents
archived and disabled; it now un-archives them correctly and scoped to the archive stamp.

Known gaps, documented rather than patched

  • The KB and table cascade hooks restore children one at a time; a child restored concurrently
    by another request throws and aborts the rest of that folder's restore. By design the folder
    stays archived and the whole restore is retryable, so this degrades to a retry rather than
    corruption — but it is a real race. (Workflows use the bulk path and are unaffected.)
  • archiveTableChildren walks tables one at a time through deleteTable. guardLockedTables
    has already rejected the folder if any table is locked, so the loop is likely equivalent to
    the bulk path; collapsing it changes cascade semantics and was left for a follow-up.
  • useUpdateFolder has no optimistic update, so a dragged folder lags where a dragged
    resource does not.
  • restore_resource's two new folder types are unreachable until the restore_resource
    parameter enum in the copilot service's contracts/tool-catalog-v1.json — a different
    repository, mirrored here by scripts/sync-tool-catalog.ts — widens. Accepted on this side
    already, so it needs no follow-up here.
  • Files still builds its own folder row, breadcrumb trail, row-id scheme and untitled-name loop
    rather than the shared module's; it shares the move-option renderer and root sentinel. Files
    carries multi-select and external-file drops the shared helpers do not model — that is why
    it does not regress here — and each shared helper's TSDoc says so rather than overclaiming.
  • Knowledge and Tables have no external-file-drop guard, so dropping an OS file on the list
    lets the browser navigate away. Files wraps the page to prevent this. Pre-existing shape for
    those two pages, called out because it is now a visible difference between foldered lists.

Type of Change

  • Bug fix
  • New feature
  • Breaking change
  • Documentation
  • Other: DB migration (backfill), behavior change on folder restore

Migration verification: 0272 + 0274

Where 0272 has not yet run, it and 0274 apply back to back in the same deploy, and 0274 becomes
a no-op reconcile over the rows 0272 just wrote.

Read-only dry run. Materialised the complete post-0272 folder table from both source
tables — applying 0272's exact dedup CTE — and asserted every constraint the table declares:

Check Result
NULL names / NULL required columns none
Primary-key collisions between the two source tables none
folder_workspace_resource_parent_name_active_unique violations none
folder_parent_resource_type_match — resource-type / workspace none
FK violations — parent / user / workspace none
Dedup renames that found no free suffix (would NOT-NULL fail) none
Foldered files / workflows left with an unresolvable folder id none

Both files run in tens of milliseconds at the data volumes involved.

⚠️ From #6037, not this PR: 0272's dedup renames a small number of workflow folders whose
names collide under the new partial unique index, which is stricter than what workflow_folder
enforced. Mostly auto-generated Folder N names. No file folders are renamed — the legacy
table enforces the same active-unique key, which is why 0274 can write raw names.

Executed end-to-end locally (Postgres 17.9) against a dataset built to break it — duplicate
groups where the naive name (rn) rename would collide with an existing sibling, triples with
(1) and (2) already taken, archived duplicates, unicode and 255-char names, cross-workspace
and cross-parent same-names, and children ordered before their parents. Both migrations applied
cleanly; every invariant held; replaying 0274 twice and 0272 again left the state hash
byte-identical
(idempotent).

The same harness reproduced the divergence 0274 exists for — including a rename-then-reuse
swap, where the mirror still holds a name the legacy table has since given to a different
folder — and proved two things:

  • the original insert-only form silently discarded a folder (ON CONFLICT DO NOTHING
    swallows a name violation as readily as an id one) and left every diverged row stale;
  • the unparked ON CONFLICT (id) form only succeeds by luck of row order — forcing the
    adversarial order reproduces duplicate key value violates folder_workspace_resource_parent_name_active_unique.

The shipped form (park names, then upsert, inside a DO block) reconciled the mirror to
byte-for-byte identical with no rows missing, no field divergences and no parked names left
behind. The DO block matters because 0272 ends with an embedded COMMIT for its
CONCURRENTLY index builds, so this file is not guaranteed to run inside drizzle's batch
transaction — a bare two-statement form could commit the parking pass and then fail the
reconcile, leaving every mirrored folder holding a placeholder name.

Testing

bunx tsc --noEmit clean in apps/sim, packages/db, and packages/platform-authz; lint, check:api-validation, check:migrations, check:react-query, and check:client-boundary all pass; bunx vitest run --root apps/sim is green apart from one pre-existing, unrelated environment failure (executor/handlers/pi/cloud-review-tools.test.ts, FileNotFoundError: 'rg' — ripgrep is not on this machine).

New tests: lib/folders/lifecycle.test.ts (create/update/delete/restore, the re-root, all 23505 mappings), lib/api/contracts/folders.test.ts (the deploy-ordering contract above), components/folders/folders.test.ts, table folder-assignment route tests, KB folder-assignment service tests, and folder-target assertions in cleanup-soft-deletes.test.ts.

Reviewers should focus on: migration 0274 and its ordering against the deploy; the restore-semantics change; and the archived-folder lock bypass fix, since DELETE and PUT deliberately differ there.

The UI has not been exercised in a browser. Folder navigation, drag-and-drop, inline rename, and the breadcrumb menus on Knowledge and Tables are verified by types and unit tests only.

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

@vercel

vercel Bot commented Jul 29, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped Jul 29, 2026 6:02am

Request Review

@cursor

cursor Bot commented Jul 29, 2026

Copy link
Copy Markdown

PR Summary

High Risk
Large cross-cutting change (migration ordering, restore semantics, folder validation) touching data placement, archive/restore, and admin import paths where wrong folder IDs previously had no FK guard.

Overview
Extends folder placement to tables and knowledge bases: create, import, and PATCH accept folderId only after findActiveFolder(..., 'table'|'knowledge_base'), list/detail responses include folderId, and KB create/update surfaces KnowledgeBaseFolderError as 400.

Introduces components/folders/ and wires Knowledge and Tables to match Files-style behavior: URL ?folderId=, breadcrumbs, folder rows, create/rename/delete, move menus, pin/unpin, and drag-row-onto-folder. Files now imports the shared move-option helpers (local move-options.tsx removed). Resource.Table name cells can show a pin glyph when pinned is set.

Folder API fixes: PUT/reorder exclude archived rows (deletedAt null) so lock checks cannot be bypassed after a parent delete; duplicate uses FolderDuplicationError and scoped 409 on root insert conflicts; sort order comes from nextFolderSortOrder. Admin workflow import validates folderId in-workspace before write. Pinned-items GET skips unknown resourceType values so one bad row does not break the list.

Recently Deleted gains knowledge and table folder types with restore/deep links; prefetches pull folder trees alongside KB/table lists.

Reviewed by Cursor Bugbot for commit 3ad97f0. Configure here.

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

Comment thread apps/sim/app/workspace/[workspaceId]/knowledge/knowledge.tsx Outdated
@greptile-apps

greptile-apps Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Extends the generic folder engine to knowledge bases and tables, cuts file folders fully onto folder (resourceType file) with migration 0274 reconcile, and shares folder UI/helpers across Knowledge and Tables while keeping Files on its locked path APIs.

  • Adds KB/table folder_id end-to-end (create/move validation via findActiveFolder, list UI, breadcrumbs, DnD, pins, recently-deleted restore types).
  • Hardens folder lifecycle (active-only updates, restore name conflicts, timestamp-matched cascade restore, admin import folder checks).
  • Reconciles legacy workspace_file_folders into folder via 0274; notes manual re-run after rolling deploy drain.

Confidence Score: 3/5

Not fully safe to merge until workflow folder/single restore also re-enables dependents that archive disabled, or an explicit durable pre-archive state is restored.

Folder cascade restore un-archives workflows and clears dependent archivedAt stamps, but archive still forces schedules to disabled and webhooks/chats inactive, so successful restore leaves automation and chats non-functional until manual re-enable or redeploy.

Files Needing Attention: apps/sim/lib/workflows/lifecycle.ts

Important Files Changed

Filename Overview
apps/sim/lib/workflows/lifecycle.ts Restore is timestamp-scoped for dependents but still leaves schedules disabled and webhooks/chats inactive after un-archive.
packages/db/migrations/0274_file_folder_cutover_reconcile.sql Parks colliding mirror names then upserts full legacy file-folder state by id for cutover reconcile.
apps/sim/lib/folders/lifecycle.ts Blocks updates on archived folders, scopes restore renames, and documents children-before-folders restore ordering.
apps/sim/lib/resources/orchestration/restore-resource.ts Maps knowledge_folder/table_folder restorable types to engine resourceTypes without shifting workflow folder meaning.
apps/sim/app/workspace/[workspaceId]/knowledge/knowledge.tsx Knowledge list adopts shared folder module (rows, nav, move, DnD) matching tables/files patterns.
apps/sim/app/workspace/[workspaceId]/tables/tables.tsx Tables list gets the same shared folder chrome and folderId wiring as knowledge.

Reviews (7): Last reviewed commit: "fix(folders): gate the orphan fallback o..." | Re-trigger Greptile

…bles folders

Builds on the generic resourceType-driven folder engine to finish the migration
and extend it to two more resource trees.

File-folder cutover
- Repoints all 30 `workspace_file_folders` query sites onto `folder` scoped to
  `resourceType = 'file'`, id-keyed lookups included — a caller can hand a
  workflow folder's id to a file-folder endpoint, and only the predicate stops it
- Adds the missing restore-conflict handling: a file folder whose name was taken
  while it was archived now deduplicates against the RESOLVED parent (restore
  re-roots when the original parent is still archived) instead of being
  permanently unrestorable
- The forking mapper's `resourceType` predicate lives in the `leftJoin`
  condition, not the WHERE, where it would silently make the outer join inner
- `servedFolderResourceTypeSchema` gains `file`; the soft-delete sweep enumerates
  all four types rather than dropping its predicate

Knowledge Base and Tables folders
- `knowledge_base.folder_id` and `user_table_definitions.folder_id` are now
  written and served; both create/move paths admit a folder only through
  `findActiveFolder`, scoped to workspace AND resourceType
- Restoring a knowledge base whose folder is still archived re-roots it rather
  than filing it somewhere the page never renders; a workspace move re-roots for
  the same reason
- Both pages get folder rows, breadcrumbs, inline rename on rows and on the
  breadcrumb, move-to submenus, pin/unpin, and drag-a-row-onto-a-folder

Shared folder UI
- One `components/folders/` module now backs Knowledge, Tables, and Files:
  `folderBreadcrumbItems` (data for `Resource.Header`, which owns the crumb
  chrome), `folderRow`, `folderRowId`/`parseFolderedRowId`, `buildMoveOptions` /
  `buildDescendantIndex` / `renderMoveOptions`, `nextUntitledFolderName`,
  `FolderContextMenu`, `useFolderNavigation`, `useFolderRowDragDrop`
- Deletes the per-page copies: `files/move-options.tsx` and the Tables-local
  folder context menu

Recently Deleted
- Restores folders of every type, not just workflow folders, driven by one
  declarative table rather than a branch per resource

Folder pins resolve against `folder` unscoped: file, knowledge-base, and table
folders are all pinnable and all live there, and a folder id addresses exactly
one row.
Data loss
- Migration 0274 catches up file folders created AFTER 0272. That backfill is guarded
  by `WHERE NOT EXISTS (… resource_type = 'file')`, so it fires once; every file folder
  created between it and this cutover exists only in `workspace_file_folders`. Without
  the catch-up they vanish from Files on deploy and their contents become unreachable

Authorization
- Archived folders were mutable, which bypassed the folder lock: the folder routes and
  `updateFolder` filtered id + resourceType but never `deletedAt`, while
  `getFolderLockStatus` does — so an archived-but-locked folder reported unlocked. Lock a
  subfolder, delete its parent, and any write-level member could rename and reparent it.
  PUT and the engine now require an active row; DELETE deliberately still does not, because
  it reuses an archived folder's own `deletedAt` to make a partial cascade retryable
- `v1/admin/workflows/import` wrote `folderId` with no validation at all. Migration 0272
  dropped the FK, so it accepted a knowledge-base folder, another workspace's folder, or
  garbage — and the workflow then rendered under no sidebar node while still executing,
  billing, and escaping the folder delete cascade. It now runs the same
  `assertFolderInWorkspace` + `assertFolderMutable` pair as `v1/workflows/import`

Restore no longer loses state
- A folder restore ran a bare `archivedAt = null` over its workflows, while the delete had
  routed through `archiveWorkflow` — which also disables schedules, webhooks, chats, and MCP
  tools. Restoring a folder reported success while every schedule stayed disabled forever
  and every chat 404'd. The workflow tree now restores through `restoreWorkflow`, like the
  knowledge-base and table trees already did. Deployment state is still deliberately not
  revived, matching the single-workflow restore
- Those canonical restores re-root a resource whose folder is archived — correct on its own,
  catastrophic inside a cascade, which restores children BEFORE the folder rows and would
  therefore dump every child at the workspace root. `resolveRestoredFolderId` takes the
  subtree being restored and exempts it. `restoreTable` gained the re-root check it lacked

Correctness
- `getFolderPath` in `lib/folders/tree.ts` walked `parentId` with no `visited` set, so a
  cycle in the client cache — reachable through `useReorderFolders`' unchecked optimistic
  write — hung the tab. Its twin already had one. Dead `buildFolderMap` deleted
- The sidebar's "New folder" inside a folder hardcoded the name, so the second one 409'd,
  was swallowed with only a log line, and the user got no folder and no message. It now
  names through the existing `generateSubfolderName` and surfaces failures
- `chunkedBatchDelete` deleted by id with no re-check, so a resource restored between the
  SELECT and the DELETE was hard-deleted anyway. Callers now re-assert their soft-delete
  predicate on the DELETE
- Recently Deleted and the `restore_resource` tool cover every folder tree, not just
  workflow folders, so deleted knowledge-base and table folders are recoverable

Cleanup
- The folder duplicate route stops steering control flow through `throw new Error('literal')`
  matched by string, reuses the engine's `nextFolderSortOrder` instead of recomputing it, and
  names its workflow scope once
- `servedFolderResourceTypeSchema` documents that its default applies only to an omitted
  value; a present-but-unrecognized one is rejected, never coerced to `workflow`
- The `workspace_file_folders` comment described the table as unread and unwritten and
  invited a DROP. It was live until this cutover, and it is now the rollback copy — the
  comment says so, and names what must be true before anyone drops it
- Pinned rows carry a small non-interactive pin glyph again; they sort to the top and had
  nothing explaining why since the inline pin button was removed

Tests
- New `lib/folders/lifecycle.test.ts` covers create/update/delete/restore, the re-root, and
  the 23505 mappings; `cleanup-soft-deletes.test.ts` now pins the folder target's
  resourceType predicate, which is what keeps the sweep off a not-yet-cut-over type
…ploy contract

Files rows were left without the pin indicator the other two lists gained, so a pinned
file or folder still sorted to the top of the Files page with nothing explaining why.

Adds `lib/api/contracts/folders.test.ts`, which pins the three cases that together ARE
the rolling-deploy contract: an omitted `resourceType` defaults to `workflow` (so a client
predating the field keeps working), a present-but-unrecognized one is rejected rather than
coerced, and every served tree is accepted.
Migration 0274 reconciles instead of catching up
- The deployed manager writes `workspace_file_folders` EXCLUSIVELY, so `folder`'s file rows
  are frozen at the 0272 snapshot: every rename, move, delete, and restore since then lives
  only in the legacy table. An insert-only catch-up left all of that diverged — renames
  reverted, deleted folders came back as active phantoms, and a stale-active row could hold
  the name a newer folder legitimately took
- That last case was silent: the bare `ON CONFLICT DO NOTHING` swallows a NAME violation as
  readily as an id one, so the newer folder was discarded and every file inside it stranded.
  The conflict target is now `(id)`, so a name collision — which would mean the source
  violated its own unique index — fails loudly instead
- Reconciles by parking mirrored names first, then upserting, so no transient state can
  collide with the partial unique index. Both statements are idempotent, and the header says
  what nothing else would: this has to be run again after the deploy drains, because old pods
  keep writing the legacy table until they are gone

`/api/folders` no longer serves file folders
- Adding `'file'` to `servedFolderResourceTypeSchema` opened a second writer over the same
  rows. It bypassed the `workspace_file_folders:${workspaceId}` advisory lock that makes the
  manager's check-then-write pairs atomic, and it accepted names containing `/`, `\`, `.` and
  `..`, which the file surface forbids because a folder name becomes a path segment — a
  folder named `a/b` is then unreachable by path forever, and breadcrumbs resolve it as two
  segments. The storage cutover never needed a second API, so there isn't one

Conflicts that returned 500
- `v1/admin/workspaces/[id]/import` created its folder with an unserialized SELECT-then-
  INSERT, so two imports sharing a folder path failed the whole import. The name is
  server-chosen, so it now adopts whichever folder won, like `ensureWorkspaceFileFolderPath`
- Folder duplicate never caught 23505. `newId` is client-supplied, so replaying a duplicate
  whose response was lost hit the primary key and returned 500 instead of a 409 the caller
  can act on

Performance
- `knowledgeKeys` moved to `hooks/queries/utils/knowledge-keys.ts`. Reading it from
  `hooks/queries/kb/knowledge` pulled a ~1000-line hook module — and the `@sim/emcn` barrel
  behind it — into `hooks/queries/folders`, which three server prefetches import. That is the
  same import edge the navigation-speed audit measured at 11.8MB per workspace route;
  `tableKeys` already lived in a keys-only module for exactly this reason

Merge drop
- Knowledge never got the chrome Tables did: its prefetch now fetches the folder tree
  alongside the bases, so the list does not paint ungrouped and a `?folderId=` deep link does
  not render an empty breadcrumb, and its loading fallback declares the "New folder" action so
  the header does not shift on hydration

Comments corrected to match reality
- The shared folder row and context menu claimed Files as a consumer; Files still builds its
  own row and routes folders through `FileRowContextMenu`, which the TSDoc now says
- `schema.ts` prescribed a row COUNT comparison before dropping the legacy table. The
  divergence 0274 repairs is in row CONTENTS, which a count check passes straight through
Reverts the workflow restore hook — it was the wrong fix
- Greptile flagged that restoring a folder leaves schedules disabled and webhooks/chats
  inactive. The hook added last round routed workflows through `restoreWorkflow` to address
  it, but `restoreDependents` ALREADY clears exactly those columns, in bulk, inside the
  restore transaction and matched on the archive timestamp. The hook bought nothing and cost
  a per-workflow read/transaction/read OUTSIDE that transaction — roughly 1600 round trips
  for a folder of 200 workflows — plus a window where the workflows were active and the
  folder was not, and it made `restoreDependents` dead code
- What no restore path can undo is the state archive OVERWRITES: `status: 'disabled'` with
  `nextRunAt` cleared, `isActive: false`. Archive does not record what those were, so
  restoring them to a constant would re-enable a schedule the user had disabled and re-run a
  completed one. Left explicit — redeploy re-activates a schedule — matching deployment state,
  which restore also deliberately leaves off. Documented on the config rather than guessed
- Kept the genuine bug the review surfaced underneath it: `restoreWorkflow` un-archived every
  dependent by `workflowId` alone, resurrecting a webhook or chat the user had archived days
  earlier. Now matched on the workflow's own `archivedAt`, which is the semantics this feature
  documents

Bugbot: orphaned knowledge bases vanished from every view
- Knowledge filtered on a raw `folderId === currentFolderId` and never re-rooted a base whose
  folder is missing or archived, so a base restored on its own — or left behind by a partial
  cascade — was invisible everywhere. Tables already fell those back to the root

A dead `?folderId=` is now healed instead of being a dead end
- A bookmark to a deleted folder rendered the root title over an empty grid, hid everything
  actually at the root, and — worse — left every create/upload action targeting the dead id,
  filing new resources somewhere nothing could reach. `useFolderNavigation` clears it once the
  folder list resolves, which fixes Files, Knowledge, and Tables at once

Parity gaps found by auditing against Files
- Tables sorted folders newest-first on a clean URL while Files and Knowledge sorted A→Z: its
  sort params are defaulted, so the raw column was never null. Reads `activeSort` now
- On Tables you could not delete the folder you were standing in — its own row is not in the
  list, and the crumb menu offered only Rename, which also made the step-out branch
  unreachable dead code
- A failed knowledge-base move was logged and never surfaced, so a rejected move — from the
  submenu or from a drag — looked like nothing happened
- A drag begun in another mount of the page could never be dropped: `onDragOver` bailed before
  `preventDefault`, so no drop event ever fired and the `dataTransfer` fallback in `onDrop`
  was unreachable. External/foreign drags are still ignored, so an OS file dropped on the list
  cannot navigate the tab away

Files: a folder cycle crashed the page
- The folder-size roll-up recursed with no `visited` guard, so a parent/child cycle in the
  cached tree — reachable through the optimistic folder-move write — recursed until the stack
  blew and the page went blank. Also indexes children once instead of re-scanning per node

Migration 0274 is now atomic
- 0272 ends with an embedded COMMIT for its CONCURRENTLY index builds, so this file is not
  guaranteed to run inside drizzle's batch transaction. Wrapped in a DO block so the parking
  pass cannot commit without the reconcile, which would leave every mirrored folder holding a
  placeholder name
- Validated with a read-only dry run: no active name duplicates, no orphaned or
  cross-workspace parents, and no id collisions with `workflow_folder`, so the insert cannot
  trip the unique index, the FKs, or the resource-type trigger

Smaller corrections
- Knowledge indexed its members for the owner comparator instead of two linear scans per
  comparison, and logs a load failure from an effect rather than the render body
- Files uses the shared root sentinel instead of a bare `'__root__'` in two places
- `restore_resource` documents that its two new folder types are unreachable until the enum in
  the copilot service's tool catalog — a different repository — widens
- Corrected a forking comment that still described file folders as a separate table
@waleedlatif1
waleedlatif1 force-pushed the feat/folders-kb-tables branch from b653ddc to ef2b19c Compare July 29, 2026 04:04
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile — on the P1 ("Incomplete schedule and chat restore", lib/workflows/lifecycle.ts): you found a real bug underneath it, but the diagnosis pointed at the wrong mechanism. Both are addressed in ef2b19c.

What was actually wrong. restoreWorkflow un-archived every dependent by workflowId alone, so it resurrected a webhook or chat the user had archived independently, days before the workflow was. Archive stamps the workflow and its dependents with one timestamp and only touches rows that were still active, so the workflow's own archivedAt identifies exactly what that archive took down. The restore now matches on it — which is also the timestamp-matched semantics this PR documents.

Why the folder cascade did not need the hook. restoreChildren: restoreWorkflowChildren (added in the previous round, in response to this finding) has been reverted. restoreDependents on the workflow config already clears exactly those columns, in bulk, inside the restore transaction and matched on the cascade timestamp. Routing through restoreWorkflow bought nothing and cost a per-workflow read → transaction → read outside that transaction — roughly 1600 round trips for a folder of 200 workflows — plus a window where the workflows were active while the folder was not, and it made restoreDependents dead code.

Where I'm pushing back. Restore deliberately does not reactivate. Archive overwrites status: 'disabled' with nextRunAt cleared, and isActive: false, without recording what those were. Setting them back to a constant would re-enable a schedule the user had disabled and re-run a 'completed' one-shot — a silent, hard-to-notice wrong action on live automations. Re-enabling stays explicit (a redeploy sets status: 'active'), which matches deployment state (isDeployed/isPublicApi/workflowDeploymentVersion.isActive), left off on purpose for the same reason. Making this lossless needs archive to persist the pre-archive state — a schema change on a shipped path, worth its own PR rather than a guess bolted onto this one. The reasoning is now on the config next to restoreDependents so it does not read as an oversight.

Net effect vs. staging: folder restore previously left dependents archived and disabled; it now un-archives them correctly and scoped. Strictly better, with the remaining gap documented rather than guessed at.

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

@greptile-apps

greptile-apps Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

The timestamp-matched restore in restoreWorkflow (matching eq(workflowSchedule.archivedAt, archivedAt) etc.) is the right fix — restoring by workflowId alone was always wrong for exactly the reason you describe. Good catch on the underlying bug, and the fix is clean.

The restoreChildren revert also makes sense. restoreDependents in the cascade already handles schedules, webhooks, chats, and MCP tools in bulk inside the restore transaction, timestamp-matched. Routing through restoreWorkflow per-workflow was strictly worse: the extra reads outside the transaction, the active-folder-is-not-yet-active window, and restoreDependents becoming dead code. The comment on the config entry documents the reasoning clearly enough that nobody revisiting it will misread the absence of a hook as an oversight.

On reactivation — you're right to push back. Archive overwrites status, nextRunAt, and isActive destructively with no prior-state record. Restoring to a fixed constant would silently re-enable a user-disabled schedule or re-trigger a completed one-shot, which is a worse outcome than leaving re-activation explicit. The schema-change path (persisting pre-archive state) is the correct fix if this ever needs to be lossless, and the comment on the config makes that clear.

The P1 is resolved. Nothing left to flag here.

Comment thread apps/sim/app/workspace/[workspaceId]/components/folders/use-folder-navigation.ts Outdated
A read-only dry run materialises the complete post-0272 `folder` table from both source
tables and asserts every constraint it declares: no NULL names, no primary-key collisions
between the two source tables, no unique-index violations, no resource-type or workspace
trigger violations, and no parent/user/workspace FK violations. 0272's dedup renames only
workflow folders, never file folders — the latter is what lets pass 2 write raw names.
@waleedlatif1

waleedlatif1 commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator Author

@greptile Following up on the reactivation gap with concrete evidence rather than my judgement.

I checked how often rows are live (archived_at IS NULL) but deliberately disabled — i.e. exactly the state a blanket reactivation would overwrite. For both workflow_schedule and webhook, a large minority of live rows sit at status = 'disabled' / is_active = false. Completed one-shot schedules are in there too.

So restoring to a constant would re-enable a user-disabled schedule a substantial fraction of the time, re-fire completed one-shots, and switch on live external webhook endpoints nobody asked to turn on. That is a materially worse failure than leaving them off — it is silent, it fires real side effects, and it is hard to notice. Chats happen to be uniformly active today, but the toggle exists in the UI, so that is safe only by coincidence and a per-table split would be inconsistent for no real gain.

I'm taking the second half of your condition: explicitly accepted as product policy, with user-facing behavior. Recently Deleted now surfaces it at the moment of restore rather than leaving the user to discover it — so the choice is informed, not silent. The lossless fix remains persisting pre-archive state, which is a schema change on a shipped path and belongs in its own PR.

Data integrity
- Workspace fork copied `folderId` verbatim onto the child's tables and knowledge bases. The
  column carries no workspace, so a forked row pointed at a folder owned by the SOURCE
  workspace — invisible in the fork, and mutated from under it when the source later deleted
  that folder (`ON DELETE SET NULL`). Latent until now because nothing wrote a non-null
  `folderId` for those two resources; this PR is what makes it reachable. Forked resources land
  at the root, like forked files already did

Interaction bugs
- Clearing a dead `?folderId=` wrote through the params' default `history: 'push'`, so Back
  returned to the dead URL, which healed and pushed again — the Back button was dead on that
  page. It replaces now: opening a folder is navigation, correcting a URL that never pointed
  anywhere is not
- That same heal keyed off `isLoading`, which is false for a DISABLED query, false for an
  ERRORED one, and false while `keepPreviousData` is showing the previous workspace's folders.
  In all three the list is empty or stale, so a transient fetch failure — or a workspace switch
  — threw away a perfectly good open folder. Gated on `isSuccess && !isPlaceholderData`
- "New folder" while a search was active created the folder but never showed it: the search
  filters folders too, so the new row did not match, the rename field never appeared, and the
  create read as a no-op. Both pages clear the search so the thing just created is on screen
- The drag ghost was removed only by `dragend`, which fires on the source row. The table is
  virtualized, so scrolling the source out of view mid-drag unmounted it, the event never
  arrived, and the ghost stayed on the page with every row stuck at drag opacity. Cleaned up on
  unmount as the backstop
- A drag begun in another mount highlighted every folder as a valid target — including the
  dragged folder itself — then silently did nothing on drop. It only highlights when the source
  is known and was actually checked

Correctness
- The folder-duplicate route caught 23505 across the WHOLE handler, so a unique violation
  raised while copying the workflows inside the folder was relabelled "a folder with this name
  already exists". Scoped to the folder INSERT, which is the only place the two conflicts the
  caller can act on (client-supplied `newId` replay, concurrent name take) actually arise
- The optimistic folder create derived its sort order from the workspace's WORKFLOWS regardless
  of resource type. Only the workflow tree interleaves folders and resources in one ordering
  space, so a knowledge-base or table folder was placed against an unrelated one
- A table archived between `checkAccess` and the move surfaced as a 500 with a "not found"
  body; it is a 404
- `FOLDER_RESOURCE_TYPE_BY_RESTORABLE` was a `Partial` record, so adding a folder tree without a
  mapping would compile and silently restore into the workflow tree. Total record now
- Recently Deleted paired query results to folder trees by ARRAY POSITION; reordering the const
  would have filed knowledge folders under Tables with no type error. Keyed by type
- The knowledge move's no-op guard compared against the snapshot taken when the menu opened
  rather than the live row

Performance — the module split now actually does what it claimed
- `knowledge-keys.ts` justified itself as keeping server prefetches off the ~1000-line
  `kb/knowledge` module, but `knowledge/prefetch.ts` still imported its stale-time constant from
  exactly that module, whose first line pulls the `@sim/emcn` barrel. Worse, this PR had ADDED
  the same class of edge to four prefetches via `FOLDER_LIST_STALE_TIME`/`mapFolder`
- `FOLDER_LIST_STALE_TIME`, `mapFolder`, and `KNOWLEDGE_BASE_LIST_STALE_TIME` now live beside
  their key factories, so all four server prefetches import only keys-and-constants modules.
  `mapFolder` keeps its explicit field list rather than a spread, so the cached shape cannot
  drift from a client fetch

Honesty in comments
- The KB retention `deleteFilter` narrows the restore race but does not close it — documents
  hard-deleted by `onBatch` do not come back, so a restore landing mid-batch returns an emptied
  base. Said so
- A failed folder restore leaves children active while their folders are still archived. They
  are reachable (both lists fall an unresolvable `folderId` back to the root) but they sit at
  the root until the restore is retried. Said so

Product policy made visible
- Restore deliberately does not re-enable schedules, webhooks, or chats, because archive
  overwrites their state without recording it. A deliberately-disabled schedule or webhook is a
  common state rather than an edge case, so reactivating to a constant would be wrong more often
  than right. Recently Deleted now says so at the moment of restore instead of leaving it to be
  discovered

Smaller
- Pin glyph: dropped the doubled gap, added `role='img'` so the state is announced
- "Move here" carries the folder glyph its sibling leaves do
- `useMoveTable` had stolen `useDeleteTable`'s doc block
- Dead `.returning` column and a `?? null` on a non-nullable param in `moveTableToFolder`
- Tables reads one sort source for both blocks, matching Knowledge
- New test pins that `updateFolder` refuses an archived folder — the guard that stops a delete
  from unlocking its locked subfolders
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

Comment thread apps/sim/app/workspace/[workspaceId]/tables/tables.tsx
`handleMoveTable` and `handleMoveFolder` decided "already there, nothing to do" from
`activeTable`/`activeFolder`, which are snapshots taken when the context menu opened. A refetch
or a concurrent move since then makes that comparison stale, so the write the user just asked
for is silently skipped and the row stays where it was. Both now compare against the live list,
matching the knowledge-base move.
This repository is public. The 0274 header and the Recently Deleted policy comment carried
concrete counts and ratios taken from the production database, which is operational detail that
does not belong in open source. Both now state the property that matters — what was asserted,
and why disabled-on-restore is the safe default — without the underlying numbers.
@waleedlatif1
waleedlatif1 force-pushed the feat/folders-kb-tables branch from 63fa2af to b1c2a33 Compare July 29, 2026 05:21
@greptile-apps

greptile-apps Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

That evidence settles it. A blanket reactivation that re-fires completed one-shots and silently opens live webhook endpoints is a worse failure mode than leaving things off — the failure is quiet, has real side effects, and is harder to notice or reverse than the current behavior.

Surfacing the "schedules and webhooks stay off after restore" note in Recently Deleted at the moment of restore is the right mitigation given the constraint. The lossless fix (persisting pre-archive state) is genuinely a separate schema change.

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

Note on an earlier revision of this PR: the migration header, a code comment, and some of my comments above quoted concrete row counts and ratios from the production database. This repo is public, so that has been scrubbed — from the working tree, from the affected commit messages (history rewritten, hence the force-push), from the PR description, and from the comments. The verification itself is unchanged; it is just stated as the property that was asserted rather than the underlying figures.

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

Comment thread apps/sim/app/workspace/[workspaceId]/knowledge/knowledge.tsx
…ing unit

Audited as the unit that actually ships — #6014 (pinning), #6025 (workflow folders onto the
generic table), #6037 (the engine) and this branch together against `main`, rather than this
branch against `staging`.

Authorization
- `PUT /api/folders/reorder` still operated on archived folders. `getFolderLockStatus` skips
  archived rows, so `assertFolderMutable` there was a guaranteed no-op — meaning a locked folder
  became freely reparentable the moment its parent was deleted. This branch closed exactly that
  hole on `PUT /api/folders/[id]` and left its sibling open, then widened reorder to three
  resource trees. Reordering an archived folder is also wrong on its own terms:
  `collectArchivedSubtreeIds` walks the cascade by parent, so moving a branch out of an archived
  subtree silently drops it from that folder's restore

Data loss in the purge
- `batchDeleteByWorkspaceAndTimestamp` forwarded its eligibility predicate only into the SELECT,
  never the DELETE. The `folder` target runs through it, so a restore committing between the two
  statements had its folder hard-deleted anyway — taking the placement of the children the
  restore had just brought back. The workflow purge re-checks for precisely this reason and the
  knowledge-base sweep gained the same guard earlier in this branch; `folder` had neither. The
  wrapper now re-asserts eligibility on the DELETE for every caller

Wire boundary
- `toPinnedItemApi` had no return type, so `pinned_item.resource_type` — plain `text` by design,
  against a closed contract enum — was never checked. Annotating it surfaced the real hole
  immediately: during a rolling deploy an older pod can read a pin a newer one wrote, and
  returning it would fail response validation and take the WHOLE list down rather than the one
  row. Unknown kinds are now narrowed out explicitly at the boundary instead of surviving by
  accident on a filter whose stated job is something else

Interaction
- The knowledge-base FOLDER move still compared against the snapshot taken when the menu opened.
  The resource move on that page and both Tables handlers were fixed earlier in this branch; this
  was the last one

Operational
- 0274's "re-run after drain" instruction needed a precondition. Cleanup hard-deletes from
  `folder` but never from `workspace_file_folders`, so a re-run after a purge would reinstate
  every purged file folder as a soft-deleted phantom whose files are already gone. Retention is
  far longer than any drain, so running it promptly is sufficient — but the instruction said
  nothing about ordering

Accuracy
- Four comments named symbols that do not exist or no longer apply: `useFolderCreateWithDedup`
  (never existed — it is `nextUntitledFolderName`), `collectActiveSubtreeIds`,
  `restoreFolderCascade` as the live restore path, and a claim that a server
  `createSearchParamsCache` reads the shared folder param. The shared param doc also now admits
  Files declares its own `?folderId=` rather than claiming to be the single declaration
- `FOLDER_RESOURCES.file` is unreachable at runtime — `servedFolderResourceTypeSchema` does not
  serve `'file'` — and a reader would reasonably assume otherwise. Says so, and says what routing
  file folders through the generic engine would bypass
- `pinnedItem.resourceType`'s inline comment was missing `'folder'`
- `folders.test.ts` fixtures still carried `color` and `isExpanded`, both deliberately dropped
  from the generic table — pre-consolidation rows masquerading as folders
- `getFolders` in the folder cache had one caller, in its own file
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

Comment thread apps/sim/app/workspace/[workspaceId]/knowledge/knowledge.tsx
…t a loading flag

The URL heal was fixed to use `isSuccess && !isPlaceholderData`, but the list filters that
decide "this resource's folderId does not resolve, so show it at the root" were left on
`isLoading` — the same footgun, one layer down.

`isLoading` is false for a disabled query (no workspaceId), false for an errored one, and —
because `useFolders` sets `keepPreviousData` — false while the previous workspace's folders are
still on screen during a switch. In all three the index is empty or belongs to another
workspace, so every foldered knowledge base and table was treated as an orphan and dragged to
the root, or filtered out of a deep-linked folder entirely. A transient fetch failure was enough
to make a folder look empty.

`useFolderNavigation` now exposes `foldersResolved` instead of `isLoading`, so the heal and both
list filters share one signal and the footgun is no longer reachable from the hook's surface.
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 3ad97f0. Configure here.

@waleedlatif1
waleedlatif1 merged commit a3413cf into staging Jul 29, 2026
22 checks passed
@waleedlatif1
waleedlatif1 deleted the feat/folders-kb-tables branch July 29, 2026 06:11
waleedlatif1 added a commit that referenced this pull request Jul 29, 2026
Resolve files.tsx import conflict: keep useWorkspaceFilesRoom (realtime Files
presence), drop the stale MoveOptionNode import from files/move-options (deleted
by staging's folder cutover #6045; now provided by components/folders).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant