Theodore Li 985f9e6172 feat(tables): saved views with filter, sort, and column presets (#5961)
* feat(tables): saved views with filter, sort, and column presets

* fix(tables): views own column layout, preserve deep-linked sort, seed update cache

* fix(tables): merge view layout writes server-side, keep layout on save-from-All

* fix(tables): send view saves as a merge patch so concurrent writes can't clobber

* fix(tables): persist explicit All selection, prune view state for deleted columns

* fix(tables): key-order-stable dirty check, reset layout when switching to All

* fix(tables): return 404 when patching a missing view

* improvement(tables): order the bar filter, sort, columns and drop the hidden count

* fix(tables): record newly created columns in the active view's order

* fix(tables): don't write layout as a reader, clear dead sort, reset dead view id

* feat(tables): add New view to the views menu, starting from All

* chore(api): bump route-count baseline to 981 after staging merge

* fix(tables): guard default demotion, use the applied filter for dirty/save

* fix(tables): clear state when the active view is deleted externally

* revert(tables): drop the ineffective rows-query gate for view resolution

* feat(tables): enable saved views in the embedded mothership table

* fix(tables): resolve the views flag on the chat route too

* improvement(tables): right-align the embedded run/stop control

* fix(tables): live layout on new views, prune stored config, order cache writes

* fix(tables): live layout on save-as-view, reset it per view, ignore inherited param when embedded

* fix(tables): route undo/redo column-layout writes to the active view

* fix(tables): scope layout undo to the view that recorded it

Layout is view-owned, but UndoEntry carried no owner, so the persistence
sink — rebound every render to the active view — decided the target at undo
time rather than at record time. Reordering in view A then undoing from view
B wrote A's column order into B and left A unchanged.

Entries are now stamped with the active view id, and the three layout-bearing
action types (create-column, delete-column, reorder-columns) are pruned from
both stacks when the active view changes. Row and schema actions are
table-scoped and survive the switch untouched. Inert when views are disabled:
the id is always null, so nothing is ever pruned.

* fix(tables): send only the layout keys an action changes

Pin, reorder, and insert-column each shipped a full columnWidths snapshot
they never modified. Both sinks merge at top-level key granularity, so an
earlier-issued patch landing after a concurrent resize replaced the newer
width map with its stale copy.

Resize, auto-resize, and delete-column still send columnWidths — those
genuinely change it.

* fix(tables): don't strand layout writes while views load, keep schema undos across views

Two more instances of layout being written without a known owner.

The sink was left unbound until a view resolved, so a resize/reorder/pin (or
the column-append effect) during the views fetch fell through to the table's
shared metadata — corrupting All for a table about to adopt a view, and losing
the edit to the re-seed. The sink is now bound while the query is in flight and
suppresses the write. An error counts as settled, so a failed views fetch falls
back to All instead of suppressing layout writes for the session.

Pruning was also too broad: create-column and delete-column are table-scoped
schema ops that merely have a layout side-effect, so dropping them on a view
switch made a deleted column unrecoverable. Only reorder-columns is purely
layout and still prunes; the other two survive and have just their layout half
suppressed at replay when the recorded view isn't active.

* fix(tables): flush layout buffered during load when the owner settles to All

Suppressing the write while the views query was in flight stopped All from
being corrupted, but nothing resolved the buffer afterwards, so a resize during
load looked applied and vanished on refresh.

Settling on All re-seeds nothing (viewLayoutKey never changed), so the gesture
is still on screen and is now persisted to shared metadata. Adopting a view
re-seeds the grid from that view and has already replaced the gesture on
screen, so the buffer is dropped to match.

* fix(tables): resolve undo layout ownership at write time, not dispatch time

The layout writes for column create/delete happen in mutation success
callbacks, and persistLayoutRef is rebound every render. Resolving
entryOwnsLayout up front meant the guard could still hold from before a view
switch while the sink it guarded already pointed at the destination, writing
the recorded view's layout into whichever view was now active.

Now a function, so the guard and the sink are read at the same moment: switch
away mid-mutation and the schema half still lands while the layout half is
dropped, matching the rule that undo only ever affects the view on screen.

* fix(tables): read layout from the grid instead of mirroring it

The wrapper kept two shadow copies of the grid's column layout — liveLayoutRef
and pendingLayoutRef — and all three of this round's findings were that mirror
going stale:

- liveLayoutRef was only cleared on activeView.id change, so widths buffered
  before the views query settled survived into All, where layout writes bypass
  the mirror entirely. Both create paths spread it last, so a saved view stored
  those snapped-back widths.
- currentViewConfig memoized a spread of that ref, and mutating a ref doesn't
  re-run a memo, so Save as view sent the pre-gesture layout.
- The flush effect keyed on activeView, but adoption writes the view id through
  the URL, so for one render the query had settled while activeView was still
  null — flushing to All in exactly the case that had to drop.

The grid owns this state, so it now publishes a reader through a sink ref and
the wrapper asks at the moment it needs a value. Nothing to keep in sync, so
nothing to go stale. Only whether an unowned change happened is tracked, and
the resolve effect — which is what actually picks the owner — decides to
persist or drop.

* fix(tables): flush unowned layout when the views fetch fails

The resolve effect is the only caller of resolvePendingLayout and gated on
isSuccess, which never becomes true on a query error — so layout touched
during the load window was never persisted on the error path, even though
the table had already settled to All and later writes worked.

The error branch now flushes to shared metadata, matching the rest of the
error path's fall-back-to-All behavior.

* fix(tables): gate the on-screen layout restore by ownership, not just the persist

The undo success callbacks applied the recorded view's order/widths/pinning to
the grid unconditionally and only gated the PATCH, so switching views before a
column create/delete mutation resolved left the destination displaying the
origin view's layout until switched away and back.

Each callback now checks ownership where its layout work begins: three are
purely layout and return at the top; delete-column undo restores cell data
first (row data, runs everywhere) and gates only the layout block below it.
In a non-owning view the restored column still appears via the grid's append
effect — at the end, leaving that view's layout untouched.

* fix(tables): route all layout writes through one owner-aware sink, reconcile order at seed

Two holes closed:

The sink binding toggled on activeView, leaving a render-frame gap after the
views query settled but before the resolve effect adopted a default — writes in
that gap fell through to shared metadata. The sink is now always bound while
views are enabled and handlePersistLayout is the single router, reading the
owner at call time: unresolved buffers, a view patches the view, All writes
metadata. The error branch stamps the owner so post-error writes stop
buffering.

The append effect only fires when the schema changes, so a column that arrived
while another source owned the layout rendered via the displayColumns fallback
but was never written into the adopted owner's stored order until a drag
happened to heal it. Seeding now reconciles the incoming order against the
schema and persists the appended tail through the current sink.

* fix(tables): capture the layout owner when a schema action is dispatched

Insert-column and the delete chain persist layout from mutation callbacks
through updateMetadataRef, which always targets the current sink — so a view
switch mid-flight wrote the origin view's order/widths/pins into the
destination. Undo got this guard already; the live paths never did.

Both now capture viewLayoutKey at dispatch and compare at the callback. On a
mismatch the layout work is skipped: the destination re-seeded its own layout,
the new column lands there via the append effect when the refetch arrives, and
the deleted column's dangling keys are pruned on read.

pushUndo also takes the captured owner as an override — stamped at callback
time it would record the destination, letting a later undo apply the origin's
layout to it.

* fix(tables): resolve views on list availability, not query success

A failed background refetch flips isError while the cached list stays usable,
and every view mutation invalidates the views query — so one blip made the
resolve effect treat views as terminally failed and stop applying switches
until the next successful refetch.

The axis is now whether a list exists: error with no list ever fetched settles
to All; error with a cached list resolves normally against the cache; owner is
unknown only while the initial fetch is in flight.

* ci: raise Build App timeout to 25 minutes

The build outgrew the 15-minute cap over the last two days of merges:
10m02 (c6acc62a07), then 14m44 after the folders/desktop/library batch
(adc557a2f2, 16s under the limit), then two consecutive timeouts on
7b1af4121b after the outlook merge. Staging's own latest runs show the same
signature (one cancelled). GitHub labels a job timeout "cancelled", which is
why these read as cancellations.
2026-07-29 14:57:58 -04:00

Sim.ai Documentation Slack X

Ask DeepWiki Set Up with Cursor

Sim — Integrate, Context, Build, and Monitor AI agents

A workspace to build, deploy and manage AI agents and workflows.

Quickstart

Cloud-hosted: sim.ai

Open sim.ai

Self-hosted

git clone https://github.com/simstudioai/sim.git && cd sim
bun run setup

Open http://localhost:3000

The Sim platform — chat on the left, the visual workflow builder on the right

Capabilities

  • Connect 1,000+ integrations and every major LLM
  • Add Slack, Notion, HubSpot, Salesforce, databases, and more
  • Build agents visually, conversationally, or with code
  • Ingest files, knowledge bases, and structured table data
  • Monitor runs, logs, schedules, and workflow activity

One workspace, every surface

Chat and workflows are just the start — tables, files, knowledge, and scheduled tasks all live in the same workspace.

Tables in Sim — structured data your agents can query

Tables — a database, built in

Files in Sim — documents for your team and every agent

Files — one store for your team and every agent

Knowledge bases in Sim — synced docs your agents can search

Knowledge — your agents' memory

Scheduled tasks in Sim — recurring agent runs on a calendar

Scheduled tasks — runs on your schedule

Self-hosting

Requirements: Bun and Docker.

bun run setup is an interactive wizard: it provisions the database, generates secrets, writes your .env files, connects a Chat API key, and starts Sim the way you choose:

  • Local dev — run from source to contribute or hack on Sim
  • Docker Compose — a self-contained instance for testing self-hosting
  • Kubernetes (Helm) — deploy to a local cluster

When it finishes, open http://localhost:3000.

Manage your install with bun run sim:

bun run sim start | stop | restart   # bring your install up / down / cycle
bun run sim status                    # what's installed and healthy
bun run sim logs                      # follow logs
bun run sim doctor                    # diagnose configuration problems
bun run sim down                      # remove containers (data kept)
bun run sim reset                     # archive .env and wipe managed data

sim detects how you're running (Docker Compose, local dev, or Kubernetes) and acts accordingly.

Prefer a bare sim? Run bun link once — but note sim lands in ~/.bun/bin, which Homebrew's bun doesn't add to your PATH, so you may need export PATH="$HOME/.bun/bin:$PATH" in your shell profile.

Sim also supports local models via Ollama and vLLM. See the self-hosting docs for details.

Chat API Keys

Chat is a Sim-managed service. bun run setup connects a Chat API key for you — sign in when it opens your browser and the key is stored automatically. To view, create, or revoke keys later, go to sim.ai/selfhost/settings/chat-keys.

Environment Variables

See the environment variables reference for the full list, or apps/sim/.env.example for defaults.

Tech Stack

Next.js · Bun · PostgreSQL · Drizzle · Better Auth · Tailwind — and the rest of the stack

Contributing

We welcome contributions! Please see our Contributing Guide for details.

License

This project is licensed under the Apache License 2.0 - see the LICENSE file for details.

Built by the Sim team in San Francisco

Languages
TypeScript 77%
MDX 20.8%
JavaScript 1.9%
CSS 0.1%