feat(tables): folder tools for the Table block, and folders as picker scope - #7391
Open
mzxchandra wants to merge 3 commits into
Open
feat(tables): folder tools for the Table block, and folders as picker scope#7391mzxchandra wants to merge 3 commits into
mzxchandra wants to merge 3 commits into
Conversation
… scope Tables already had the folder machinery - the five folder operations, their application use cases, and a public v2 route - but no agent-callable tools, so a run could query a table's rows and could not file the table anywhere. Six tools, mirroring the File block's set: `table_list_folders`, `table_create_folder`, `table_update_folder`, `table_delete_folder`, `table_restore_folder`, and `table_move`. Restore is addressed by the path the folder held when it was deleted rather than by id, because that is what `restoreTableFolderUseCase` takes; the response reports where the folder actually landed, which differs when its old parent is still archived or a live sibling has taken its name. Executor admission. The folder operations delegated to Copilot only, so every one of these tools would have 403'd. Widened to `toolReadOperation` / `toolWriteOperation(id, 'tables.use')`: tables.folders.list <- table_list_folders tables.folders.create <- table_create_folder tables.folders.update <- table_update_folder tables.folders.delete <- table_delete_folder tables.folders.restore <- table_restore_folder tables.update <- table_move `tables.use` rather than `tables.create` throughout: a folder organizes, it does not add a table, so a workspace with table creation withheld can still file what it has. `table_move` reuses `updateTableUseCase` with only `folderPath` set rather than growing a second way to move a table. Both `check:actorless-executor-operations` and `check:capability-subject` pass with the widening - neither the folder use cases nor `updateTableUseCase` requires a human subject, so an actorless run (schedule, webhook, deployed API) can reach them without a 500. Dispatch is in-process, as new cases in `executeTableTool`. The dispatcher keyed every case to a routed contract, which these have no business inventing - there is no HTTP route for them - so it now carries a response schema and the folder cases pass standalone schemas through `parseInternalOperationInput`, the path the Knowledge handler already takes. Reusing the public v2 folder contracts was the alternative and was rejected: `recursive` there is a `z.stringbool()` shaped by URL query encoding, and this flag is the guard between deleting one empty folder and deleting a subtree. None of the tools sends a `workspaceId`. The executor mints a delegated principal bound to the run's workspace and the operations read it from there, so accepting one would declare a field the server ignores. `table_move` is still scoped to its table, but through its own schema: `getTableContract`, which the older scoped tools use, requires the `workspaceId` these deliberately omit, and reading it that way rejected every move as malformed. On `table_v2`, each folder path is a tree selector paired with a manual text entry so a `<reference>` can be typed on a text surface. The one exception is the Folder that narrows the table picker: it never travels, because a folder cannot stand in for a table the way it stands in for a file, so it is basic-mode only rather than a pair whose advanced half would resolve a reference and have it discarded. It renders above the picker it narrows - below reads as a second choice rather than a filter. The scope comparison is by decoded segment, never by string prefix: `/a/bc` starts with `/a/b` and is not inside it, and a folder genuinely named `Q3/Q4` is one level. `isFileInFolderScope` now delegates to the same predicate instead of keeping a second copy.
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
Contributor
Greptile SummaryThe PR adds agent-callable table-folder operations and folder-scoped table selection, including follow-up fixes that preserve model-supplied operation parameters and clear loaded selections excluded by a new scope.
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains.
|
| Filename | Overview |
|---|---|
| apps/sim/blocks/blocks/table_v2.ts | Adds the six folder-oriented block operations and preserves model-supplied paths while giving explicit authored canvas values precedence. |
| apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/table-selector/table-selector.tsx | Filters table choices by folder scope and persistently clears a loaded selection when the selected scope excludes it. |
| apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/table-selector/scope.ts | Implements segment-aware folder containment with fail-open handling for malformed paths and incomplete folder caches. |
| apps/sim/lib/internal/table/execute-tool.ts | Dispatches the new table-folder tools through internal operation schemas and handlers. |
| apps/sim/lib/internal/tool-operations/registry.server.ts | Expands executor admission for the table read and write operations required by the new tools. |
| apps/sim/tools/table/folders.ts | Defines the agent-facing table-folder tools and their structured operation results. |
Sequence Diagram
sequenceDiagram
participant Agent
participant Block as Table block transformer
participant Dispatcher as Internal table dispatcher
participant UseCase as Table/folder use case
Agent->>Block: Folder operation parameters
Block->>Dispatcher: Validated canonical paths and options
Dispatcher->>UseCase: Execute workspace-scoped operation
UseCase-->>Dispatcher: Updated folder/table result
Dispatcher-->>Agent: Structured tool response
Reviews (2): Last reviewed commit: "fix(tables): keep a model's folder desti..." | Re-trigger Greptile
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
…ble selection Two review findings, both on the block surface. A block used as an Agent tool is handed the TOOL's schema, not this block's subblock ids - `createLLMToolSchema(toolConfig, ...)` in `providers/utils.ts` - so a model answers with `path` / `folderPath` while the canvas stores `folderRef` / `moveTargetRef`. Every folder transformer read only the canvas id and dropped the model's answer. Five of them then failed schema validation, which is loud and recoverable. `move` did not: the tool substitutes the workspace root for an absent destination, so "move into /Reports" became "move to the root" with a success result. The canvas value still wins where the author set one; the model's answer is the fallback, matching what `columns` already does in this file. `update_folder` branches on the canvas source instead, because its destination is composed rather than picked and an unset parent is a real destination (the root), not a missing one. The delete cascade deliberately gets no model fallback - it is `user-only` on both the tool param and the subblock. Narrowing the folder scope filtered the selected table out of the picker without touching the stored value, so the combobox showed its placeholder while the block kept executing against the hidden table. The selection is now cleared when it falls out of scope - but only for a table that is actually loaded and actually out of scope, since a table missing from the list is still-loading or deleted, and clearing there would destroy a valid config over a transient cache state.
Contributor
Author
|
Skipping Bugbot: Bugbot is disabled for this repository. Visit the Bugbot dashboard to update your settings. |
Contributor
Author
|
@cubic review |
Contributor
@mzxchandra I have started the AI code review. It will take a few minutes to complete. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds the six folder tools to the Table block, so an agent can organize tables and not just read them. Stacked on #7388 — please merge that first.
Tables already had the folder machinery (the five folder operations, their application use cases, and a public v2 route) but no agent-callable tools, so a run could query a table's rows and could not file the table anywhere.
Six tools:
table_list_folders,table_create_folder,table_update_folder,table_delete_folder,table_restore_folder,table_move.Restore is addressed by the path the folder held when it was deleted rather than by id, because that is what
restoreTableFolderUseCasetakes. The response reports where the folder actually landed, which differs when its old parent is still archived or a live sibling has taken its name.Executor admission
The folder operations delegated to Copilot only, so every one of these tools would have 403'd. Widened to
toolReadOperation/toolWriteOperation(id, 'tables.use'):tables.folders.listtable_list_folderstables.folders.createtable_create_foldertables.folders.updatetable_update_foldertables.folders.deletetable_delete_foldertables.folders.restoretable_restore_foldertables.updatetable_movetable_movereusesupdateTableUseCasewith onlyfolderPathset rather than growing a second way to move a table. It never setsnameordescription, so no executor path gains rename powers.check:actorless-executor-operationsandcheck:capability-subjectboth pass: neither the folder use cases norupdateTableUseCaserequires a human subject, so an actorless run (schedule, webhook, deployed API) reaches them without an opaque 500.Dispatch
In-process, as new cases in
executeTableTool. The dispatcher keyed every case to a routed contract, which these have no business inventing — there is no HTTP route for them — so it now carries a response schema, and the folder cases pass standalone schemas throughparseInternalOperationInput, the path the Knowledge handler already takes.Reusing the public v2 folder contracts was the alternative and was rejected:
recursivethere is az.stringbool()shaped by URL query encoding, and this flag is the guard between deleting one empty folder and deleting a subtree.None of the tools sends a
workspaceId. The executor mints a delegated principal bound to the run's workspace and the operations read it from there, so accepting one would declare a field the server ignores.table_moveis still scoped to its table, but through its own schema:getTableContract, which the older scoped tools use, requires theworkspaceIdthese deliberately omit, and reading it that way rejected every move as malformed.Block surface
Each folder path is a tree selector paired with a manual text entry so a
<reference>can be typed on a text surface. The one exception is the Folder that narrows the table picker: it never travels, because a folder cannot stand in for a table the way it stands in for a file, so it is basic-mode only rather than a pair whose advanced half would resolve a reference and have it discarded. It renders above the picker it narrows — below reads as a second choice rather than a filter.The scope comparison is by decoded segment, never by string prefix:
/a/bcstarts with/a/band is not inside it, and a folder genuinely namedQ3/Q4is one level.isFileInFolderScopenow delegates to the same predicate instead of keeping a second copy.Type of Change
Testing
Unit: 553 test files / 9800 tests pass across the touched areas. 8 new test files. All 45 audits pass, plus
check:api-validation,check:client-boundary, biome.Tests deliberately cover the seam between "the schema accepted the field" and "the use case received it" — a tool option that parses and is then dropped looks identical to one that works from both ends.
End-to-end, driven through the real UI as real workflow runs and verified in Postgres:
create folderCreated table folder "/Q3%20Results"— the canonical percent-encoded path composed from the typed namemove tableQ3_Pipelinemoved fromReportsintoQ3 Resultsdelete folder, cascade off, non-emptyFolder is not empty; both folders intactdelete folder, cascade onrestore folder["leads","q3_pipeline"]; Folder =Reports→["q3_pipeline"]Reviewers should focus on: the
tables.updatewidening fortable_move, and the delete cascade guard (default off,visibility: 'user-only'so a model asked to "clean up" cannot set it on a guess).Known issue, pre-existing and not from this PR
table_restore_folderthrows in local dev only.restoreFolder(lib/folders/orchestration.ts) wraps its body inwithFolderTreeLockbut drops thetx, so the body queries the globaldbpool inside the transaction and tripspackages/db/tx-tripwire.ts. That tripwire isthrowwhenNODE_ENV !== 'production'andwarnin production — re-running the same restore withDB_TX_TRIPWIRE=warnreturns{"success":true,"restoredItems":{"folders":1,"tables":1}}.Not introduced here: the untouched
POST /api/v2/tables/folders/restorefails identically, as does the Recently-deleted restore path inlib/resources/orchestration/restore-resource.tsfor workflow and knowledge folders. The sibling transitions (create at:397, relocate at:798) both threadtx; restore is the odd one out. The fix means threading a tx throughrestoreFolderWithoutTreeLockand its per-resourcerestoreChildrenhooks across four resource types, so it belongs in its own PR rather than as a rider on this one.Checklist