Commit Graph
5041 Commits
Author SHA1 Message Date
Theodore Li 43fa5eaa19 feat(data-retention): workspace-level overrides for retention and PII (#5186)
* feat(data-retention): workspace-level overrides for retention and PII

* fix(data-retention): hide unmanageable PII rows when flag off, scope override workspace IDs to org, dedupe key type

* improvement(data-retention): unify org default and workspace overrides into one policy list

* fix(data-retention): clean up overrides for workspaces deselected during edit
2026-06-23 18:41:03 -04:00
Theodore Li c20d5fc70d fix(enrichment): stop PDL billing on no-match via required-field gating (#5184)
PDL bills per matched profile, but each cascade only counts a hit when mapOutput yields a specific field. A confident profile (likelihood >= 6) lacking that field was billed yet recorded as no_match. Pass PDL's required param so it 404s (free) when the extracted field is absent, aligning PDL's billing unit with the cascade's success unit.
2026-06-23 15:37:55 -04:00
Siddharth Ganesan bf5077bf24 fix(skills): fix skills icon showing up (#5187) 2026-06-23 12:21:32 -07:00
Theodore Li 406ae92b84 fix(trigger): mark cpu-features external to fix deploy build (#5185)
ssh2 became reachable from the trigger background bundle via the Pi
local SSH backend (#5178). esbuild then tried to bundle ssh2's optional
native dep cpu-features and failed on the missing .node binary. ssh2
requires cpu-features inside a try/catch, so externalizing it is safe.
2026-06-23 11:49:25 -07:00
Waleed 77976bcb8b feat(billing): unify upgrade routing with reason context + storage/tables limit emails (#5171)
* feat(billing): unify upgrade routing with reason context + storage/tables limit emails

* fix(billing): re-arm limit-notification dedup on usage drops (prior-usage + decrement)

* fix(billing): isolate per-admin email failures in org limit notifications

* fix(billing): re-arm limit dedup at zero usage and zero prior usage (full clear / wipe-rebuild)

* fix(billing): make storage-decrement notification re-arm only (never send on a shrink)

* fix(billing): resolve recipients before claiming so opt-outs don't burn the dedup threshold

* fix(billing): fire table limit emails on upsert inserts via shared notifyTableRowUsage

* chore(billing): only log a limit email as sent when a recipient actually received it

* chore(billing): match to_jsonb int cast between claim and re-arm for consistency

* fix(billing): notify table limits post-commit so a rolled-back insert never emails or burns the claim

* feat(pi): swap Pi Coding Agent icon to the pi glyph and use a black bgColor

* fix(billing): drop priorUsage re-arm to make dedup a single atomic claim (no duplicate-email race)

* docs(billing): move limit-notification rationale to TSDoc, correct tables warn-once behavior

* docs(db): note limit_notifications dedup is per-account, not per-table

* perf(billing): cut redundant subscription fetches and edge-gate notify to slash DB load

* docs(billing): drop self-explanatory inline comments from the notification path
2026-06-23 10:51:27 -07:00
Theodore Li 8f312d299b feat(guardrails): PII redaction via Presidio sidecar (native VIN, per-rule language) (#5174)
* fix(logs): run PII redaction over HTTP and fix Presidio provisioning

- resolve the guardrails venv via candidate paths and fail fast instead of
  silently falling back to system python3 (the misleading "Presidio not
  installed" that broke redaction and the guardrails block in deployed runtimes)
- install the en_core_web_lg spaCy model in setup.sh and app.Dockerfile
- route log redaction through an internal /api/guardrails/mask-batch endpoint
  so Presidio always runs in the app container, including async executions that
  persist inside the trigger.dev runtime

* fix(guardrails): chunk + time-bound internal PII mask requests

- chunk maskPIIBatchViaHttp by count (2000) and bytes (256KB) so large
  executions split across requests and never hit the contract's 100k cap
- add AbortSignal.timeout(45s) per request so a slow/unreachable app container
  aborts and the caller scrubs, instead of hanging the trigger.dev job
- catch maskPIIBatch failures in the route: log and return a structured 500
  (broken venv fails loudly server-side; caller still scrubs, no leak)
- add mask-client tests (order across chunks, count split, non-2xx, empty)

* fix(guardrails): mint internal token per mask request

A single token (5min TTL) could expire mid-batch when a large execution
fans out into many sequential chunk requests; mint one per request instead.

* feat(guardrails): run PII via Presidio sidecars + TS recognizer registry

- replace the per-call python3 subprocess (cold spaCy load every call) with
  two long-lived Presidio sidecars (analyzer + anonymizer) reached over HTTP;
  the app image no longer carries Python/Presidio/venv
- add PRESIDIO_ANALYZER_URL / PRESIDIO_ANONYMIZER_URL
- move VIN out of Python into a TS recognizer (check-digit validated) behind a
  CUSTOM_RECOGNIZERS registry so new custom detectors are one entry; masking is
  handled uniformly by the anonymizer
- drive the guardrails block's PII type picker from the shared pii-entities
  catalog (adds VIN, fixes drift) so block + Data Retention never diverge
- delete validate_pii.py, requirements.txt, setup.sh and the Dockerfile venv step

* fix(guardrails): bound-parallelize mask batch; refresh stale comments

- maskPIIBatch runs per-string sidecar calls with bounded concurrency (8) via
  mapWithConcurrency, so a chunk of many small leaves finishes within the 45s
  request timeout instead of aborting and scrubbing; order + fail-on-error kept
- drop stale comments referencing the deleted Python venv / 30s subprocess timeout

* refactor(guardrails): single Presidio image, native VIN, per-rule redaction language

- collapse the analyzer/anonymizer URLs into one PRESIDIO_URL (combined image
  serves /analyze + /anonymize)
- remove the TS VIN recognizer (vin.ts, recognizers.ts) — VIN is now native +
  multi-language in the image; validate_pii is a thin analyze→anonymize client
- trim KR_RRN/TH_TNIN from the catalog (no Korean/Thai model in the image)
- add per-rule redaction language: PII_LANGUAGES catalog drives the contract enum,
  the Data Retention rule modal, and the guardrails block dropdown; resolver +
  logger thread it through to maskPIIBatch (default en), so non-English entity
  rules (e.g. ES_NIF) actually fire instead of silently no-op'ing under en

* fix(guardrails): correct sidecar port (5001) + README for combined image

The combined Presidio image (docker/pii.Dockerfile) serves /analyze + /anonymize
on a single port 5001 with native VIN + multi-language recognizers. Fix the
PRESIDIO_URL default (was 5002) and rewrite the README, which still described two
stock containers and a TS VIN recognizer.

* fix(guardrails): coerce stored redaction language in the resolver

The persist-path resolver accepted any stored language string, so a stale/invalid
code (e.g. a dropped locale) would reach Presidio and scrub the log even though the
admin UI shows English. Coerce against the supported set via a shared
coercePiiLanguage helper (now reused by the data-retention route too), falling back
to en for unknown values.

* fix(guardrails): rename PRESIDIO_URL env var to PII_URL

Match the infra taskdef, which sets PII_URL on the app container for the
combined Presidio sidecar.
2026-06-23 05:29:01 -04:00
Theodore Li 4d2e7d5524 fix(pii): listen on 5001 to avoid app :3000 collision (awsvpc) (#5182)
* fix(pii): bind a configurable $PORT to avoid app :3000 collision

The pii image hardcoded uvicorn --port 3000 and ignored env. In the app ECS
task (awsvpc) all containers share one network namespace, and the app owns
3000 — so the sidecar must listen elsewhere (the stock presidio images honored
PORT and ran on 5002/5001). Bind ${PORT} (shell-form CMD), default 5001, and
update EXPOSE/HEALTHCHECK accordingly so the taskdef can set PORT=5001.

Verified: default binds 5001; PORT=5002 override binds 5002; /analyze works on
the overridden port.

* fix(pii): hardcode port 5001 (drop $PORT indirection)

EXPOSE can't be parameterized, so the configurable-PORT approach left EXPOSE
showing 5001 regardless (Greptile P2). We own both the image and the taskdef
and only ever need 5001, so hardcode it: exec-form CMD on 5001, EXPOSE 5001,
healthcheck on 5001. Runtime cmdline is identical to the verified ${PORT}
default (uvicorn ... --port 5001).
2026-06-23 04:46:21 -04:00
Theodore Li 0191a614b6 feat(pii): build & own combined PII (analyzer + anonymizer) image (#5176)
* feat(presidio): build & own combined analyzer+anonymizer image

Replace the stock mcr.microsoft.com/presidio-* sidecar images with a single
image we build and push to ECR/GHCR. A thin FastAPI service constructs one
AnalyzerEngine + one AnonymizerEngine at startup and serves both on port 3000
(/health, /supportedentities, /analyze, /anonymize) so the app needs one
PRESIDIO_URL. English only; pinned presidio 2.2.362 + en_core_web_lg 3.8.0.

Bakes in the native check-digit VIN recognizer and registers 12 English
recognizers Presidio ships but does not load by default (UK_NINO, AU_*, IN_*,
SG_*), taking the supported English set from 19 to 32.

* feat(presidio): add multi-language support (es/it/pl/fi)

Configure a multi-language spaCy NLP engine (en/es/it/pl/fi lg models) and
explicitly register the national-id recognizers Presidio ships but does not
load by default: ES_NIF/NIE, IT_FISCAL_CODE/DRIVER_LICENSE/VAT_CODE/PASSPORT/
IDENTITY_CARD, PL_PESEL, FI_PERSONAL_IDENTITY_CODE. Verified the NLP-engine +
explicit-registration path detects in-language (Finnish id, score 1.0).

* improvement(presidio): address review feedback

- Register VIN under all served languages, not just en (Bugbot: VIN missed for
  non-English language routing).
- Bump HEALTHCHECK start-period to 180s — five lg models load at import (Bugbot).
- Drop --no-cache-dir so the pip cache mount actually works (Greptile).
- Pydantic request models for /analyze + /anonymize so missing 'text' returns
  422 not 500; default operator 'type' to 'replace' instead of KeyError->500
  (Greptile).

* refactor(pii): rename presidio image artifacts to pii

Rename the image/repo/secret/files from 'presidio' to 'pii' for clarity — the
service does PII detection + anonymization (and backs the guardrails block's
block/mask), not just redaction, and 'pii' matches existing pii-* naming.

docker/presidio.Dockerfile -> docker/pii.Dockerfile
docker/presidio/ -> docker/pii/
ghcr.io/simstudioai/presidio -> .../pii
ECR_PRESIDIO secret -> ECR_PII (infra side already renamed)
No behavior change — paths/identifiers only.

* refactor(pii): move service to apps/pii, make image ECR-only

- Move server.py + requirements.txt from docker/pii/ to apps/pii/ (source belongs
  under apps/, matching app/realtime; Dockerfile stays in docker/). Add a minimal
  @sim/pii package.json so the apps/* bun workspace glob accepts the Python service.
- Repoint docker/pii.Dockerfile COPY paths to apps/pii/; rename the container user
  presidio -> pii.
- Drop GHCR for pii — it's a private ECS sidecar pulled from ECR, never published.
  Removed it from the arm64/manifest (GHCR-only) jobs and guarded the build-amd64
  tag step to skip GHCR when no ghcr_image is set.
2026-06-23 03:41:35 -04:00
Vikhyath Mondreti ccc6954257 improvement(pi): prompting to ensure harness knows push is deterministic (#5180) 2026-06-22 22:19:41 -07:00
Vikhyath Mondreti 633391903d feat(pi): add pi coding agent harness (#5178)
* feat(pi): add pi coding agent harness

* formatting

* update docs

* change version num

* guard to prevent prs on error

* update param visibility

* address security concerns

* fix tests

* reorder:
2026-06-22 21:47:53 -07:00
Theodore Li 707c3cc214 feat(trigger): add trigger-eu-region flag to switch runs to eu-central-1 (#5173)
* feat(trigger): add trigger-eu-region flag to switch runs to eu-central-1

Global on/off feature flag routing every Trigger.dev run from the default
us-east-1 to eu-central-1 via the per-trigger region option, resolved at
each dispatch site through resolveTriggerRegion.

* test(trigger): mock resolveTriggerRegion in delete-async route test

The route now pulls in feature-flags (which imports isAppConfigEnabled from
env-flags); the test's partial env-flags mock made that access throw. Stub the
region module and assert the region option on the dispatch.
2026-06-22 18:17:24 -04:00
Waleed 844733a5ea feat(providers): add Sakana AI provider with Fugu models (#5169)
* feat(providers): add Sakana AI provider with Fugu models

OpenAI-compatible provider at https://api.sakana.ai/v1 (bearer auth).
Registers fugu (fast default) and fugu-ultra (reasoning flagship), both
1M context. BYOK-only, never hosted/auto-billed. Streaming, tool loop,
and response_format supported; attachments mirror deepseek (unsupported
in the current adapter).

* fix(providers): defer Sakana structured output until after tool loop

OpenAI-compatible backends reject a request carrying both response_format
and active tools/tool_choice. Mirror the LiteLLM pattern: withhold the
JSON schema while tools are active and apply it on a final tool-free call
(tool_choice: none) for both streaming and non-streaming paths.

* fix(providers): harden Sakana tool-loop error + final-stream tool_choice

- Rethrow tool-loop failures instead of swallowing them, so a failed run
  surfaces as a ProviderError rather than a partial success (matches LiteLLM).
- Force tool_choice: 'none' on the post-tool streaming pass so the model
  cannot emit fresh tool calls that the text-only stream adapter would drop.

* fix(providers): Sakana streaming usage + filtered-tools stream guard

- Pass stream_options: { include_usage: true } on both streaming calls so
  token/cost data is captured (the shared OpenAI-compatible stream helper
  only fills usage from chunk usage, which the API omits without the flag).
- Include !hasActiveTools in the early-stream guard so requests whose tools
  are all filtered out (e.g. usageControl 'none') still take the fast
  streaming path instead of the tool-loop path. Mirrors LiteLLM.

* fix(providers): answer every Sakana tool_call to keep message history valid

An assistant message lists all tool_calls, so a call for an unconfigured
tool must still get a matching `tool` response or the next request violates
the OpenAI message contract. Emit an error tool-result for unknown tools
instead of dropping them.

* test(session): de-flake SessionProvider normal-load test

flush() only drained microtasks, so the query->render update occasionally
lost the race and ctx.data was still null after the flush budget. Yield one
macrotask tick per flush so React Query's notifyManager and deferred renders
settle deterministically. Verified across repeated local runs.
2026-06-22 13:35:11 -07:00
Waleed e96b150bc9 refactor(frontend-arch): migrate server state to React Query, collapse duplicate workflow-state cache, granular error boundaries (#5168)
* refactor(session): migrate SessionProvider to React Query useSessionQuery

Replace the hand-rolled useState/useEffect/loadSession session loading in
SessionProvider with a useSessionQuery() React Query hook. The SessionContext
shape is unchanged ({ data, isPending, error, refetch }) so no consumer changes.

The 'upgraded' path still forces a fresh DB read via
client.getSession({ query: { disableCookieCache: true } }) (refetch() cannot
pass disableCookieCache) and writes the result via queryClient.setQueryData,
then invalidates ['organizations']/['subscription'] as before.

* refactor(workflows): collapse duplicate workflow-state cache

The registry store fetched the GET /api/workflows/[id] envelope inline via
requestJson while useWorkflowState cached the same endpoint's mapped state
under workflowKeys.state(id) — two requests, two cache shapes, never
reconciled.

Collapse to one request + one cache entry keyed by workflowKeys.state(id):

- Add hooks/queries/utils/fetch-workflow-envelope.ts: a standalone
  fetchWorkflowEnvelope(id, signal) returning the full GetWorkflowResponseData.
  Standalone (not in workflows.ts) to avoid a store -> query-hook import cycle.
- useWorkflowState/useWorkflowStates now query the envelope and derive the
  mapped WorkflowState via select (mapWorkflowState), so consumers see the
  identical mapped shape from the shared entry.
- The store's loadWorkflowState reads via getQueryClient().fetchQuery({
  staleTime: 0 }) instead of raw requestJson — always-fresh (preserving the
  prior always-fetch boot/refresh semantics, incl. the socket
  handle-resource-event refresh path that has no separate state
  invalidation), in-flight deduped, writing into the same cache entry the
  hooks read.

Request-id staleness guard, deployment-cache priming, cross-store projection,
and the active-workflow-changed event are all preserved unchanged.

* fix(workspace): add granular error boundaries to logs, knowledge, and files panels

Scope a crash in one workspace panel to that panel instead of the whole
workspace shell. Each boundary reuses the shared ErrorState component and
mirrors the existing tables/settings error.tsx convention.

* refactor(unsubscribe): migrate page to React Query

Replace the hand-rolled useState+useEffect+requestJson server-state in the
unsubscribe page with React Query hooks. Add useUnsubscribe (validation/load
query, keyed by email+token, auto-runs on mount via enabled) and
useUnsubscribeMutation (unsubscribe action, reconciles cached preferences on
success) in hooks/queries/unsubscribe.ts with a hierarchical key factory.

Export UnsubscribeData/UnsubscribeActionResponse/UnsubscribeType type aliases
from the existing user contract; loading/error/success now derive from the
query and mutation objects with no local server-state mirror.

* test(frontend-arch): cover session race fix, workflow-state cache collapse, unsubscribe, error boundary

Add targeted tests for the four frontend-architecture refactors:
- session-provider: upgrade-path ordering — fresh disableCookieCache read wins
  over a late-resolving stale mount query (proves the cancelQueries guard)
- fetch-workflow-envelope + registry store: single shared state(id) cache entry,
  always-refetch (staleTime 0), request-id staleness guard
- unsubscribe: query enable-gating + mutation cache reconcile
- logs error boundary: renders ErrorState + reset wiring (also first ErrorState coverage)

* fix(session): harden upgrade path + address review feedback

- Reconcile plan surfaces after upgrade even when the fresh disableCookieCache
  read fails: invalidate ['organizations']/['subscription'] regardless of the
  bypass-read outcome (they read server truth, not the cookie cache). The valid
  cookie-cached session is still served, so a transient failure no longer signs
  the user out or leaves the just-upgraded plan looking stale. Org-activate
  fallback stays gated on having a session.
- Use a bare return in the cancelled branch of refreshAfterUpgrade (the caller
  discards the value) for clearer intent; caller coerces with ?? null.
- Make the upgrade tests deterministic: the mount mock honors the abort signal
  like the real fetch-backed client, and assertions read the query cache (the
  state cancelQueries/setQueryData/invalidation actually govern) instead of the
  async-rendered context value.

* refactor(session): break provider<->hook type cycle, fail-fast session query

Address review feedback:
- Move the AppSession type to lib/auth/session-response.ts (the module that
  produces it) so useSessionQuery and SessionProvider both import it from there,
  eliminating the provider <-> query-hook import cycle.
- Add retry: false to useSessionQuery, restoring the prior fail-fast contract
  (the global QueryClient default is retry: 1; an auth failure should surface
  immediately rather than retry a request that won't succeed).
- Return null (not the fetched value) from refreshAfterUpgrade's cancelled
  branch to make the cancellation contract explicit.
2026-06-22 12:44:07 -07:00
Waleed d8da1e2577 fix(state): align server/client state with best practices (query-key bugs, persist hygiene, useState) (#5166)
* fix(queries): close React Query key/fetch-arg drift cache collisions

Several query hooks fetched with an identifier that was absent from their
queryKey, so distinct fetch args shared one cache entry. Thread the missing
args into the key factories and update all callsites/invalidations.

- organization: useOrganization always fetched the ACTIVE org via
  getFullOrganization() while caching under detail(orgId). Pass orgId through
  to the better-auth call (query.organizationId); active-org behavior unchanged.
- logs: logKeys.detail now keys on (workspaceId, logId) to prevent cross-
  workspace collision; updated useLogDetail, useLogByExecutionId, prefetchLogDetail,
  useCancelExecution optimistic path, and external callsites.
- inbox: inboxKeys.taskList now includes cursor/limit (pagination args were sent
  but omitted from the key); keepPreviousData pagination UX preserved.
- a2a: narrow create/update byWorkflows() invalidation to byWorkflow(ws, wf)
  since their responses reliably carry both ids; delete/publish stay broad.

Not bugs (verified, left unchanged):
- kb/connectors update/delete invalidate knowledgeKeys.detail(kbId), which is a
  prefix of connectorKeys.all(kbId) — connector list/detail are invalidated
  transitively by React Query prefix matching.

Harness: add a key-fetch-arg-drift check to check-react-query-patterns.ts that
flags a camelCase identifier the queryFn forwards into the fetch but is absent
from the queryKey (excludes the requestJson contract arg, PascalCase/SCREAMING
constants, and signal/pageParam machinery). Document the rule in sim-queries.md.
tables.useTable annotated rq-lint-allow (tableId globally unique; workspaceId is
only an authz scope).

* fix(stores): whitelist durable fields in persist partialize

chat/terminal/panel persist configs leaked actions and transient state
into localStorage. Replace the chat full-state spread with an explicit
durable whitelist, and add partialize to terminal and panel (which had
none) so isResizing and _hasHydrated are no longer persisted. Panel keeps
activeTab + panelWidth because the layout.tsx blocking script reads them
from panel-state to set data-panel-active-tab before hydration (SSR
tab-flash prevention).

Harden sim-stores doctrine: persist MUST use an explicit partialize
whitelist; never persist transient flags or _hasHydrated.

* fix(state): model component useState as single source of truth

- edit-knowledge-base-modal: reset fields on closed→open via prevOpenRef
  render idiom instead of mirroring props into state through useEffect
  (a prop change while open no longer clobbers in-progress edits)
- use-verification: collapse contradictory isLoading/isVerified/isInvalidOtp
  booleans into a single status enum + errorMessage; consumer derives flags
- contact-form / demo-request-modal: derive busy/success from the mutation
  object; delete duplicated submitSuccess local state
- sim-hooks.md: add state-shape rule (no props-into-state, status enum,
  derive mutation state)

* fix(verify): clear lingering message on complete OTP (restore parity)

* docs(state): convert inline reset comment to TSDoc

* docs(state): tighten harness rules for accuracy (queryFn forwards, partialize whitelist, mutation-flag caveat)

* fix(verify): block auto-verify while a resend is in flight (restore parity)

* fix(logs): key cancel optimistic detail by route workspaceId (not the log row)
2026-06-21 22:44:14 -07:00
Vikhyath Mondreti 951ad42a23 fix(mcp): missing isDeployed in contract breaking settings, parameter overrides lack of clarity (#5164)
* fix(mcp): missing isDeployed in contract breaking settings, parameter overrides lack of clarity

* address comments

* address ux concern

* address stray 404

* address stale fallback based on live state

* fix

* fix more things

* simplify state mgmt

* add tooltip for server selection
2026-06-21 21:23:42 -07:00
Waleed f5d42ce452 feat(url-state): adopt nuqs for type-safe URL query-param state (#5163)
* feat(url-state): introduce nuqs for type-safe query-param state

Add nuqs and migrate ad-hoc URL query-param handling to typed parsers.

- Wrap the provider tree in `NuqsAdapter` (app/layout.tsx).
- Co-locate typed param modules:
  - logs/search-params.ts — timeRange/level/workflowIds/folderIds/triggers/
    search parsers (history: 'replace', clearOnDefault) preserving the exact
    prior wire encoding (kebab time-range tokens, comma-joined arrays).
  - integrations/[block]/search-params.ts — ephemeral `connect` literal param.
- Replace the logs filter store's hand-rolled URL sync (initializeFromURL /
  syncWithURL / popstate) with a URL-backed `useLogFilters` hook over
  useQueryStates; the zustand store now holds only the non-URL viewMode toggle.
- Migrate logs.tsx (executionId + search), logs-toolbar, dashboard, and the
  integration detail `connect` deep-link (read-then-strip) to nuqs.

URL keys, defaults, and history semantics are unchanged.

* feat(url-state): migrate deferred sites to nuqs + add url-state rule

Migrate the deferred query-param sites to typed nuqs parsers, each with a
co-located search-params.ts single source of truth:

- settings/[section]: mcpServerId deep-link
- files: folderId (history: push) + new compose flag
- knowledge/[id]: addConnector read-then-strip deep-link
- knowledge/[id]/[documentId]: page (int, default 1) + chunk deep-link

Workflow editor intentionally left store-backed (socket-synced / high-frequency
/ persisted-preference view-state); documented in the rule's carve-out.

Add .claude/rules/sim-url-state.md (decision framework, conventions, server
cache + debounced-input patterns, editor carve-out); cross-link from CLAUDE.md
and sim-queries.md.

* feat(url-state): migrate remaining view-state to nuqs + harness updates

Make the URL the single source of truth for shareable view-state across the
remaining sweep-confirmed sites:

- settings/mcp: replace initialServerId prop + effect-sync with a direct
  useQueryState (mcpServerId, history: push); stop prop-drilling from settings
- integrations: selectedCategory + debounced search; add Suspense boundary
- tables: debounced search + sort/dir + row-count/owner filters (activeTable
  stays route state — selecting a table navigates to tables/[tableId]); wire
  the existing loading.tsx as the Suspense fallback
- knowledge/[id]: pagination page param
- settings/recently-deleted: tab + sort/dir + debounced search
- settings/admin: committed search (q) + pagination offset
- settings/mothership: tab + environment
- skills: editingSkill object -> skillId deep-link (derive from useSkills); add
  Suspense boundary
- files: shareFileId deep-link added to files/search-params
- landing integrations + models directories: debounced search + category/
  provider filter; add Suspense boundaries

Harness: add a When-to-use decision table, the sort (sort+dir) convention, the
selected-entity deep-link pattern, and nuqs doc links to sim-url-state.md; add
the /you-might-not-need-url-state command and wire it into /cleanup.

* fix(nuqs): revert landing-page param migrations and tighten workspace URL-state

- Revert integrations/models landing pages to static SEO HTML (drop nuqs migration + their search-params files)
- MCP settings: refresh tools only for an initial deep-linked server id, not on subsequent user selections
- Add a Suspense boundary with real chrome around the nuqs-using integration detail page
- Trim inaccurate "server component reads these params" TSDoc from search-params files (createSearchParamsCache is unused)
- Export mcpServerIdUrlKeys from the settings search-params file instead of inlining the options in mcp.tsx
- Convert two new relative imports (logs use-log-filters, tables loading) to absolute
- Wrap setSelectedCategory in useCallback; clear active skillId edit param when opening the create-skill form

* fix(logs): add logs-page Suspense boundary and co-locate nuqs params

- Wrap <Logs/> in <Suspense fallback={<LogsLoading/>}> so the nuqs reads
  (useLogFilters, executionId) have a boundary ancestor like sibling pages.
- Co-locate the executionId param in logs/search-params.ts (read-only,
  intentionally not stripped) and consume it in logs.tsx.
- Migrate log-details activeTab to a deep-linkable nuqs tab param (single
  LogDetails instance; preview path uses ExecutionSnapshot, not LogDetails).
- Align cleanup.md description pass order with the numbered steps.
- Replace mothership.tsx local Tab type with exported MothershipTab.

* fix(url-state): honor deep-linked log-details tab on first mount

* improvement(nuqs): adopt limitUrlUpdates debounce + add eq to array parser

Replace the hand-rolled debounced-search pattern (local useState mirror +
useDebounce + URL write-back effect + ref-guarded reconcile effect) with
nuqs's built-in limitUrlUpdates: debounce() across logs, integrations,
tables, and recently-deleted. The input is now controlled directly by the
instant nuqs value; only the URL write is debounced. Query keys / expensive
filters still derive a debounced value off the instant value; cheap in-memory
filters read it directly. admin (commit-on-submit) intentionally left alone.

Add an eq to parseAsTriggers (TriggerType[]) so clearOnDefault can detect the
empty-array default and strip it from the URL, per nuqs createParser docs.

Update .claude/rules/sim-url-state.md to prescribe the debounce pattern and the
createParser eq requirement for array/object/Date values.

* feat(nuqs): migrate table-detail sort, KB document filters, and calendar view to URL state

- Table detail: sort+dir to nuqs; Filter stays in useState (recursive/nested, too large for URL)
- Knowledge base: search (debounced), enabled filter, sort+dir to nuqs; tagFilterEntries stays in useState (rich rule objects)
- Scheduled-tasks calendar: scope + date-only anchor (parseAsIsoDate, nullable, derive-today) to nuqs
- Add Suspense boundaries to table-detail and scheduled-tasks pages
- Document parseAsIsoDate / nullable-dynamic-default pattern in sim-url-state.md

* fix(nuqs): resolve 7 PR review findings on URL query-param state

- logs: clear the log-details `tab` param when the sidebar closes so a
  lingering `?tab=trace` no longer carries into the next opened log;
  deep-linked tabs still open on first mount.
- logs dashboard: drive the in-memory workflow filtering off the same
  debounced search value the stats query uses (passed as a prop) so the
  chart and list stay consistent while typing.
- knowledge document: make the URL `chunk` param the single source of
  truth for the open chunk (back/forward, deep links, and external
  navigation now drive the editor) instead of a one-time useState seed.
- logs: drop the redundant `setUrlSearchQuery('')` after `resetFilters()`
  (resetFilters already clears `search`).
- files: use a per-call `{ history: 'replace' }` override for the
  `shareFileId` share-modal open/close writes so toggling the modal does
  not pollute the back/forward stack; folder navigation keeps `push`.
- tables + recently-deleted: trim search input before deriving the URL
  value so whitespace-only input no longer writes `?search=%20`.

* fix(url-state): trim whitespace-only search in integrations filter

* fix(url-state): clear log tab on all close paths + trim KB search

* fix(scheduled-tasks): use local-time date parser for calendar anchor (avoid UTC day-shift)

* feat(home): migrate ?resource deep-link to nuqs (URL as source of truth)

Replace the banned window.history.replaceState effect on the home/Chat
surface with a nuqs useQueryState('resource') binding. The URL is now the
single source of truth for the selected resource.

- Add co-located home/search-params.ts (resource param, history: replace)
- useChat accepts a controlled activeResourceState binding; home passes the
  nuqs-backed tuple. The workflow editor copilot keeps internal useState so
  its resource selection stays out of the URL (editor carve-out)
- Preserve the old effect's url.hash='' fragment strip in the binding setter
  (fragment-only rewrite, not a param mutation)
- Drop initialResourceId SSR prop from both page entries (nuqs reads the URL
  on mount; no dual source) and wrap Home in Suspense for useSearchParams

* docs(home): note nuqs deferred-flush ordering in resource hash-strip

* docs(url-state): convert inline comments to TSDoc
2026-06-21 20:51:03 -07:00
Vikhyath Mondreti 9a2e06e795 fix(files): render embedded workspace images in markdown (#5162) 2026-06-20 20:18:07 -07:00
Vikhyath Mondreti cf84e051cd improvement(path): append, patch snapshot based streaming (#5161) 2026-06-20 19:09:34 -07:00
Waleed cb17207447 fix(rich-md-editor): stop the editor flashing during an agent rewrite (#5160)
* fix(rich-md-editor): stop the editor flashing during an agent rewrite

Only reveal streamed chunks that extend what's already shown. A divergent chunk
(an agent rewrite/edit, e.g. removing appended text) would collapse the document
to the partial result and flash on every chunk; now the current content is held
in place and the final result is applied once on settle. Fresh writes still
reveal live.

* fix(rich-markdown-editor): seed shown-body on settled mount and track local edits

Seed lastSyncedBodyRef from a settled (non-streaming) mount and update it on
local edits via onUpdate, so the streaming hold engages on the very first agent
rewrite chunk (no collapse/flash) and the settle still applies the rewrite that
removes a local edit.
2026-06-20 18:28:26 -07:00
Waleed 39cfae9016 improvement(scheduled-tasks): render prompt chips in task details and align weekday picker (#5159) 2026-06-20 16:34:55 -07:00
Waleed 2bbf70ec20 improvement(rich-md-editor): stabilize bubble-menu plugin key + comment cleanup (#5158)
* improvement(rich-md-editor): stabilize the bubble-menu plugin key and move inline rationale into TSDoc

* docs(rich-md-editor): preserve bubble reveal + blur rationale via a helper + TSDoc
2026-06-20 15:53:13 -07:00
Vikhyath Mondretiandwaleed 4bf791716e improvement(rich-md-editor): streaming, performance, minor bugfixes (#5148)
* fix(files): isAgentEditing flag passthrough

* use smooth streaming hook

* improve performance

* remove comments

* improvement(rich-md-editor): reveal bubble after drag-select, keep it on-screen for tall selections, restyle task-list checkbox

* improvement(share-modal): use Send icon in the share file header

* improvement(rich-md-editor): pin the formatting toolbar so it stays put while scrolling

* improvement(rich-md-editor): show the formatting toolbar in the mothership file view

* fix(sidebar): drive collapsed width from server-rendered attribute

A collapsed rail painted at the expanded width then animated to 51px on
refresh: structure came from the cookie (server) while width came from
independent cookie reads (blocking script + store), so any disagreement left
the collapsed structure at the persisted expanded width until the store
corrected it.

Unify collapse into one derivation in WorkspaceChrome and drive the collapsed
width from a server-rendered data-collapsed attribute via CSS
(.sidebar-shell-outer[data-collapsed]) — the same cookie source as the
structure, so width can never diverge from it. This is shadcn's documented
pattern (data-attribute selectors over JS ternaries for collapsed dimensions).

Also removes the redundant migratedCollapsed reconciliation (the store already
seeds from the migrated cookie and hasHydrated flips in the same pre-paint
effect) and the now-unused per-Sidebar derivation; Sidebar takes isCollapsed
as a prop.

* feat(rich-md-editor): let focused editors claim shortcuts from the global command registry

* refactor(rich-md-editor): freeze the formatting toolbar on scroll and extract the shared toolbar button

* feat(rich-md-editor): add a link hover card and claim Cmd+K for the link shortcut

* fix(rich-md-editor): portal the toolbar + link card to body so a transformed ancestor can't offset them; align fade with the tooltip

* fix(rich-md-editor): hide the code line-wrap toggle in read-only

* fix(sidebar): pass isCollapsed to Sidebar in the error fallback

The error UI renders Sidebar outside WorkspaceChrome, so it has no derived
collapse state; feed it the same source of truth via readCollapsedCookie() now
that isCollapsed is a required prop.

* address greptile comment

* docs(rich-md-editor): note why table cells escape only pipes (renderChildren pre-escapes backslashes)

* fix(rich-md-editor): lock the editor immediately when an agent edit starts, even if the body is unchanged

---------

Co-authored-by: waleed <walif6@gmail.com>
2026-06-20 15:28:55 -07:00
Waleed 55f432637f improvement(auth): make Microsoft emailVerified derivation total (#5157)
* improvement(auth): make Microsoft emailVerified derivation total

deriveMicrosoftEmailVerified cast the verified-email claims to string[]
and called .includes through optional chaining, which only guards
null/undefined. A claim arriving as a non-array, non-string value (e.g.
a number) would throw inside getUserInfo and fail the OAuth flow.
Array-check the claims with a proper type guard so any claim shape
resolves to unverified instead of throwing.

* test(auth): lock in unverified for a string verified-email claim

Add a boundary case asserting a string verified_primary_email/
verified_secondary_email equal to the email resolves to unverified —
the old string[] cast would have returned true via String.includes.
2026-06-20 15:13:48 -07:00
Waleed d643be0b93 feat(triggers): add GitLab, PagerDuty, and Zendesk webhook triggers (#5150)
* feat(triggers): add GitLab, PagerDuty, and Zendesk webhook triggers

Add webhook trigger support for three integrations that previously had
blocks but no triggers:

- GitLab: push, merge request, issue, pipeline, comment, and all-events.
  Verifies the X-Gitlab-Token secret token; filters by object_kind.
- PagerDuty: incident triggered/acknowledged/resolved/escalated/reassigned
  and all-events. Verifies X-PagerDuty-Signature (HMAC-SHA256 over raw body,
  comma-separated rotation); idempotency on event id.
- Zendesk: ticket created/status changed/comment added/priority changed and
  all-events. Verifies X-Zendesk-Webhook-Signature (base64 HMAC-SHA256 over
  timestamp+body); idempotency on event id.

Register GitLab's X-Gitlab-Event-UUID delivery header for webhook
idempotency dedup.

* fix(triggers): scope webhook secrets to owner and add Zendesk replay protection

Address review feedback:
- Add paramVisibility: 'user-only' to the webhookSecret fields for GitLab,
  PagerDuty, and Zendesk so signing secrets are scoped to the credential
  owner and not exposed to workspace collaborators (repo convention).
- Reject Zendesk deliveries whose signed timestamp is more than 5 minutes
  from now, closing a replay window once an event id ages out of the
  idempotency cache. The X-Zendesk-Webhook-Signature-Timestamp header is
  ISO-8601, so it is parsed with Date.parse (matches the Slack handler's
  skew-check convention).

* feat(triggers): auto-register GitLab, PagerDuty, and Zendesk webhooks

Replace the manual-registration model with automatic webhook creation on
deploy and cleanup on undeploy, via createSubscription/deleteSubscription
on each provider handler:

- GitLab: POST /projects/:id/hooks with a Personal Access Token; generates
  the secret token (stored for X-Gitlab-Token verification) and enables only
  the event flags for the selected trigger. Deletes the hook on undeploy.
- PagerDuty: POST /webhook_subscriptions (account-scoped) with a REST API
  key; captures delivery_method.secret (returned only on create) for
  X-PagerDuty-Signature verification. Deletes the subscription on undeploy.
- Zendesk: POST /api/v2/webhooks with native event subscriptions, then GET
  /webhooks/:id/signing_secret for X-Zendesk-Webhook-Signature verification.
  Deletes the webhook on undeploy.

Trigger config now collects the provider credentials (user-only) instead of a
pasted signing secret; the signing secret is generated or fetched and stored
in providerConfig by the orchestration layer (no route/deploy changes).

* fix(triggers): fail closed on missing webhook secret and clean up Zendesk orphans

Address review feedback on the auto-registration changes:
- verifyAuth now rejects (401) when webhookSecret is absent for GitLab,
  PagerDuty, and Zendesk. Since the secret is generated/fetched during
  auto-registration and stored before the webhook can receive deliveries, a
  missing secret indicates misconfiguration and must fail closed rather than
  skip signature verification. Adds an opt-in requireSecret flag to
  createHmacVerifier (default off, preserving behavior for other providers).
- Zendesk createSubscription now deletes the just-created webhook if the
  follow-up signing-secret fetch fails, avoiding an orphaned subscription in
  Zendesk when setup cannot complete.

* fix(triggers): clean up GitLab and PagerDuty webhooks on failed setup

Extend the orphan-prevention fix to the remaining providers. When a create
call succeeds but post-create validation fails, the created webhook is now
deleted before throwing:
- GitLab: if the create response can't be parsed for its hook id, the hook is
  located by its URL and deleted.
- PagerDuty: if the subscription response lacks an id or signing secret, the
  subscription is deleted (by id when known, otherwise located by URL).

Both cleanups are best-effort and never throw.

* docs(triggers): note GitLab tag_push only flows through the all-events trigger
2026-06-20 15:00:59 -07:00
Waleed aa57f10b44 fix(auth): close nOAuth account takeover via email-based OAuth linking (#5156)
* fix(auth): close nOAuth account takeover via email-based OAuth linking

Restrict the unauthenticated sign-in endpoints to first-party login
providers, trim trustedProviders to providers that verify email
ownership, and stop hardcoding emailVerified for multi-tenant Microsoft
and Salesforce connectors.

* test(auth): cover Microsoft id-token emailVerified derivation

Extract the Microsoft ID-token email-verification logic into a pure
deriveMicrosoftEmailVerified helper and add unit coverage for explicit,
verified-claim, partial, absent, and malformed Azure AD claim
combinations.

* fix(auth): check the provider field the sign-in handler actually uses

The allowlist guard resolved the provider with `provider ?? providerId`,
but Better Auth reads `provider` on /sign-in/social and `providerId` on
/sign-in/oauth2. A request to /sign-in/oauth2 with an allowed `provider`
and a blocked `providerId` could pass the guard while the handler started
OAuth for the blocked connector. Resolve the field per path via
getRequestedSignInProviderId so the guard checks the same field the
handler acts on.
2026-06-20 15:00:41 -07:00
Vikhyath Mondreti 82cb324638 improvement(access-controls): default workspace experience includes all members (#5153)
* improvement(access-controls): default workspace experience includes all members

* update ui

* address comments

* improve copy

* address zero-member edge case
2026-06-20 14:24:04 -07:00
Waleed 2f7d60745a fix(uploads): close multipart storage-quota bypass via quota-exempt contexts (#5155)
The multipart endpoint accepted the quota-exempt public-asset contexts
(og-images, profile-pictures, workspace-logos), which skip checkStorageQuota,
letting any authenticated writer open arbitrarily large upload sessions that
never count against their plan limit.

These contexts have no large-file flow: their client hooks hard-cap uploads at
5MB (image-only) and the direct-upload strategy only uses multipart above 50MB,
so they always route through the presigned endpoint. Remove them from
ALLOWED_UPLOAD_CONTEXTS (joining logs) so every context the multipart endpoint
serves is quota-enforced.
2026-06-20 14:07:24 -07:00
Waleed 83dc806da8 fix(file-decompress): enforce decompression caps on inflated stream, not declared zip size (#5154)
* fix(file-decompress): enforce decompression caps on inflated stream, not declared zip size

* fix(file-decompress): destroy inflate stream on error to avoid resource leak
2026-06-20 14:06:41 -07:00
Vikhyath Mondreti 35a7bf61d2 fix(executor): stop HITL error edges from firing on successful resume (#5152)
* fix(executor): stop HITL error edges from firing on successful resume

* add comments
2026-06-20 12:41:44 -07:00
Waleed 3ebb9a5029 feat(connectors): add Google Meet knowledge base connector (#5149)
* feat(connectors): add Google Meet knowledge base connector

Syncs Google Meet meeting transcripts into a knowledge base via the Meet
REST API v2. Lists conference records, fetches transcript entries lazily
per meeting (contentDeferred), resolves speaker display names, and maps
participants/duration/meeting-date tags. OAuth via the existing google-meet
provider (meetings.space.readonly).

* fix(connectors): finalize Google Meet transcripts before indexing

- Only index a meeting once every transcript is FILE_GENERATED, so a
  partial transcript is never stored under an endTime-keyed hash that
  would never refresh
- Sort merged transcript entries by start time to preserve chronology
  across multiple transcripts in one conference

* refactor(connectors): dedicated TRANSCRIPTS_PAGE_SIZE constant for Meet transcripts

* fix(connectors): only flag Meet listing capped when cap truncates source

Previously listingCapped was set whenever the fetched count reached
maxMeetings, even when the API returned every record and no next page
existed. That suppressed the sync engine's deletion reconciliation when
the cap happened to equal the true source size. Now flag only when more
pages remain or records were dropped from the page.
2026-06-19 23:35:33 -07:00
Waleed ce283fa1c9 feat(scheduled-tasks): expose Google Calendar-style recurrence options (#5146)
* feat(scheduled-tasks): expose Google Calendar-style recurrence options

Add a per-day weekly toggle (repeat on arbitrary weekdays), monthly
nth-/last-weekday anchoring, and a yearly frequency to the scheduled
task modal, closing the gap with a calendar app's recurrence picker.

The recurrence UI compiles to cron, so this is front-end only: croner
already speaks the nth/last-weekday (#/#L) syntax, and the display path
normalizes #L to cronstrue's L so labels read "last Monday" not "null".

* fix(scheduled-tasks): preserve monthly anchor when reselecting Monthly

Selecting Monthly from the frequency dropdown hard-reset monthlyMode to
day-of-month, silently dropping a previously chosen nth-/last-weekday
anchor when switching cadence away and back. Preserve the existing mode
on reselect, mirroring how the last recurring cadence is restored across
the recurring toggle.

* fix(scheduled-tasks): fold 5th-occurrence monthly into last-weekday; align weekday-digit parsing

Address review edge cases in the monthly recurrence anchors:

- The picker no longer offers a fifth weekday (a 5th occurrence is
  always the month's last), and recurrenceToCron clamps any nth-weekday
  that resolves to a 5th occurrence to #L — so a launch date drifting to
  day 29-31 can never emit #5 and silently skip months without one.
- cronToRecurrence accepts croner's alternate Sunday digit (7) for the
  #/#L monthly anchors, matching parseCronToHumanReadable's normalizer;
  externally-authored 7#L crons now round-trip (canonicalized to 0#L).
- #5 crons are left as custom pass-through so their month-skipping
  behavior is preserved verbatim rather than rewritten.
2026-06-19 23:35:02 -07:00
Theodore Li 1248f8eef4 fix(files): only show Share in context menu for files, not folders (#5147) 2026-06-19 22:35:25 -04:00
Theodore Li 3b78436ae3 feat(pii): gate data retention PII redaction behind feature flag (#5144)
* feat(pii): gate data retention PII redaction behind feature flag

* fix(pii): evaluate pii-redaction flag globally with no org/user context
2026-06-19 22:18:00 -04:00
Waleed ecbe1919d8 feat(files): inline rich markdown editor (#5133)
* feat(files): inline rich markdown editor

Replace the raw/preview split for markdown files with a Linear-style inline WYSIWYG editor (TipTap/ProseMirror): bubble + slash menus, code-block language picker with Prism highlighting and line-wrap, resizable images (HTML <img>), GFM tables, and frontmatter held byte-exact out of band.

A round-trip preflight gate (decided once per open) falls back to the raw Monaco editor for any file that can't be edited losslessly, so the rich editor never silently corrupts a file.

* fix(files): chain autosave unmount flush after in-flight save

The unmount flush no longer fires a concurrent PUT alongside an in-flight save; it awaits the in-flight save and then writes the latest content sequentially, so an out-of-order completion can't clobber newer edits with a stale snapshot (addresses Cursor Bugbot).

* fix(files): read pasted images from clipboard items, not just files

Some browsers expose a pasted or copied image only via DataTransfer.items (with an empty files list), so screenshot paste was silently ignored. extractImageFiles now falls back to items; moved to a testable module with unit tests (addresses Cursor Bugbot).

* fix(files): destroy round-trip probe editor on serialization error

Wrap the probe serialize() in try/finally so the throwaway Editor is always destroyed even if setContent/getMarkdown throws (addresses Greptile). Adds a test proving PipeSafeTable escapes only interior cell pipes, not structural delimiters.

* fix(resource): hold breadcrumb nav latch across the route swap

scheduleClose fired on the pointer/focus exit that immediately follows a click-to-navigate and was clearing the reopen latch before the route swapped, letting the popover flash back open. The latch is now released by a short timer instead (addresses Cursor Bugbot).

* chore(files): drop platform references and non-essential inline comments

* fix(files): scope inline markdown editor to the files view

The mothership preview was routing streaming markdown through the inline editor path: it showed Monaco during streaming (previewMode fell back to 'editor') and lost the streamed content on the TextEditor→MarkdownFileEditor swap (the TextEditor unmounted before it could reconcile + autosave). The inline rich editor is now opt-in via a FileViewer prop that only the files view sets, so the mothership keeps its raw/preview streaming editor and persists as before.

* fix(mothership): use the inline markdown editor in the chat resource view

Idle markdown in the chat resource view now renders the single-surface inline editor (no raw/split/preview pencil toggle), matching the files view. While the agent streams, FileViewer forces the rendered preview instead of Monaco, and the streamed file persists via the agent's server write + the existing content-query invalidation on tool completion — so the idle editor refetches the persisted content.

* refactor(files): collapse the duplicate raw-editor fallback branch in the markdown gate

* fix(mothership): swap to the inline editor once a file preview finishes streaming

The preview session keeps status='complete' and previewText after streaming ends, so streamingContent stayed defined and the file stuck on the read-only rendered preview. Treat content as streaming only while status==='streaming'; once complete the EmbeddedFile sees no streamingContent and mounts the editable inline editor (which refetches the persisted content). The synthetic streaming-file stays a pure preview.

* Revert "fix(mothership): swap to the inline editor once a file preview finishes streaming"

This reverts commit 25b12e4caa389109c2d5ed7f7d122e35d441f980.

* Revert "fix(mothership): use the inline markdown editor in the chat resource view"

This reverts commit 9430aa7fdc5050d002f2312d31dcf4a255e2de18.

* feat(files): rich markdown editor across files + chat, read-only for unsafe, robust load/save

- chat resource view streams into the rich editor (streamdown while streaming → editable on completion); agent persists server-side, editor never saves mid-stream
- round-trip-unsafe / >128KB markdown renders read-only in the rich editor (no Monaco, no corruption)
- markdown always uses the rich editor (dropped the inline-markdown opt-in flag)
- editor loads content as TipTap's initial content keyed by file id — strict-mode/SSR-safe, no content-sync effect
- fix autosave "Saving…" status suppression under React strict mode
- lock the streamed-file persistence handoff with a state-machine lifecycle test

* chore(files): remove dead code (unused FileViewer logger + EmbeddedWorkflowActions router)

* fix(files): derive markdown round-trip verdict from live content, not a locked stale snapshot

The gate locked isRoundTripSafe on the first post-stream snapshot, which is often the empty create_file buffer before the agent's server write lands — wrongly leaving an unsafe document editable. Derive the verdict from the current content (memoized on the bytes) so canEdit tracks the real payload.

* test(files): guard the rich editor dirty signal — open is never dirty, edits emit

* fix(files): lock the markdown round-trip verdict on opened content, never strand dirty edits

The round-trip-safety verdict now gates editability only at open time — computed once, on the exact
content the editor mounts with, and locked for its lifetime. A dirty document is round-trip-safe by
construction (the editor only emits safe markdown), so the verdict must never flip off mid-edit:
doing so disabled autosave, ⌘S, the toolbar Save and the unmount flush, stranding unsaved edits.
Locking on the opened (reconciled) content also fixes the stale post-stream empty-buffer snapshot,
and lets the redundant MarkdownFileEditor gate (plus its duplicate content fetch) be deleted.

* improvement(file-viewer): reuse shared copy hook, lazy frontmatter split

- code-block: replace hand-rolled copy-with-timeout with shared useCopyToClipboard
- rich-markdown-editor: compute frontmatter split once via lazy ref, drop redundant frontmatterRef
- round-trip-safety: correct stale comments (read-only, not raw editor fallback)

* feat(file-viewer): linked images, typed-link input rule, drag-to-reorder, churn fixes

- image: round-trip linked images/badges via an href attr + custom markdown tokenizer; make
  the image a drag handle so it can be grabbed and reordered
- link-input-rule: convert typed [text](url) to a link on the closing paren (normalized href)
- markdown-paste: render pasted markdown as rich content, guarded against code blocks
- round-trip-safety: behavioral link-count check replaces the static linked-image rejection
- extensions: trim the table serializer's blank lines to stop interior-table whitespace churn

* improvement(file-viewer): Backspace at start of a heading reverts it to a paragraph

Notion-style: ProseMirror's default joins or no-ops at a heading boundary, stranding the
heading style. A second Backspace then merges as usual.

* fix(file-viewer): don't upload pasted/dropped images into a read-only editor

handlePaste/handleDrop ran the workspace image upload without checking editability, so a
read-only doc (canEdit=false or a round-trip-unsafe file) could still trigger an upload.
Guard both on view.editable.

* fix(file-viewer): sanitize linked-image href; drop global leading-newline strip

- image: run the linked-image (badge) anchor target through normalizeLinkHref so a
  javascript:/data: href in a file can't execute on click; the markdown still preserves the
  raw target (file content unchanged)
- markdown-fidelity: the table serializer now trims its own surrounding blank lines, so the
  global leading-newline strip in postProcessSerializedMarkdown is redundant — removing it
  stops clobbering content that legitimately begins with whitespace

* feat(file-viewer): stream agent output directly into the rich editor; add more code languages

- rich-markdown-editor: the TipTap editor is now the only markdown surface. Agent output streams
  into it read-only (synced per chunk, autoscrolled), then the same instance hands off to an
  editable editor on settle — no separate streamdown preview, so no stream→edit flash. The
  round-trip verdict + frontmatter lock when the content settles.
- code-block/code-highlight/detect-language: register Go, Rust, Java, C, C++, C#, Ruby, PHP grammars
  and add detectors, so those blocks highlight and the picker offers them.
- css: style h5/h6 in the prose stylesheet.

* fix(sidebar): hydrate collapse state before paint to stop refresh flash

The collapsed sidebar swaps entire subtrees (collapsed flyout vs expanded
lists), but isCollapsed only resolved after the first paint via auto
rehydration, so a collapsed reload rendered the expanded tree into the 51px
rail and then reflowed — the misplaced/flashing content on refresh.

Adopt zustand's documented SSR pattern: skipHydration on the persist config
(first render keeps the default false, matching SSR HTML) and flush
persist.rehydrate() from a useLayoutEffect so the correct structure commits
in the same pre-paint frame. Removes the old race where onRehydrateStorage
lifted the data-sidebar-collapsed mask before React committed the rail.

* refactor(file-viewer): audit fixes — stale docs, DRY settle-lock, language detection

- rich-markdown-editor: rewrite the now-stale single-surface docstring (no PreviewPanel); extract a
  shared lockSettled() helper used by both the mount and stream-settle paths; guard the settle
  re-seed so it only setContent's when the body actually changed (no redundant doc rebuild)
- detect-language: stop misreading generics (List<String>) as HTML markup; detect Go type/struct
- code-block: export LANGUAGE_OPTIONS + add a test asserting every picker language has a registered
  Prism grammar (prevents picker/highlighter drift)

* refactor(file-viewer): remove dead markdown-preview renderer now superseded by the rich editor

Markdown files route exclusively to RichMarkdownEditor on both the read-only
and editable paths, so PreviewPanel's markdown branch and its Streamdown-based
renderer were unreachable. Delete MarkdownPreview and its renderers, callout/
frontmatter/checkbox machinery, and the now-unused remark/rehype/prism/streamdown
imports; drop the dead toggleMarkdownCheckbox/onCheckboxToggle plumbing in
text-editor. Keep the html/csv/svg/mermaid branches intact.

* refactor(file-viewer): drop dead streamingMode/append path, align naming, cover autosave

The streaming engine only ever runs in 'replace' mode (the only runtime callers
pass it); the 'append' branch of resolveStreamingEditorContent was unreachable.
Remove streamingMode + the StreamingMode type and thread it out of the 6
components that forwarded it — nextContent is now simply the streamed snapshot,
behavior-identical on the live path. Rename for codebase semantics: the boolean
prop streaming -> isStreaming, EditorKeymap -> RichMarkdownKeymap, the highlight
PluginKey KEY -> HIGHLIGHT_PLUGIN_KEY. Add a defensive isEditable guard to the
markdown paste handler (parity with the image handler; read-only must never
mutate). Add a dependency-free useAutosave test suite (debounce, min-display
window, no-data-loss when an edit lands mid-save, error/no-retry, Cmd+S flush,
streaming-disabled lock, unmount flush).

* fix(file-viewer): re-lock round-trip verdict + frontmatter on each stream settle

LoadedRichMarkdownEditor stays mounted across multiple agent edits to the same
file within a chat (previewContextKey is the chat id), but the settle effect only
locked settledRef when it was null — so a second stream into the same instance
kept editability and frontmatter tied to the first settled snapshot. A repeat
edit that is round-trip-unsafe would stay editable, and saves would re-attach the
stale frontmatter. Track wasStreaming and re-derive the verdict + frontmatter on
every stream->settle transition (user edits never re-derive, preserving the
don't-strand-edits rule). Verified red/green in the e2e streaming harness.

* test(file-viewer): lock link href sanitization for dangerous schemes from file content

Greptile flagged a possible javascript: link XSS. Verified TipTap 3.26.1 already
neutralizes javascript:/data:/vbscript: (and mixed-case/whitespace variants) from
file-loaded markdown to an empty href. Add a committed regression test that asserts
this against the real headless editor, so a future TipTap bump can't silently
reintroduce the issue.

* perf(file-viewer): cap the round-trip probe at 24KB and coalesce streaming syncs

@tiptap/markdown's parse is superlinear (~O(n2)) in document size — measured ~170ms
at 11KB, ~875ms at 23KB, multiple seconds past ~35KB — and it runs synchronously at
mount inside the round-trip-safety probe (twice) and the editor's own setContent. The
128KB cap allowed multi-second main-thread freezes; lower it to 24KB so the worst-case
mount stays near a second while still covering the vast majority of real markdown files
(larger files open read-only). Separately, coalesce streaming chunk-syncs to one re-parse
per animation frame so a fast-streaming agent doesn't re-parse the whole accumulating
doc per token. Typing latency was measured to be already excellent (sub-ms median, no
change needed); the only hot cost was the mount parse.

* perf(file-viewer): chunked markdown parsing to remove the O(n2) mount cost

@tiptap/markdown's whole-document setContent(md,'markdown') is superlinear in size,
freezing the main thread at mount for large files (~2.5s at 34KB, ~11s at 65KB) and
forcing a restrictive read-only cap. Parse block-by-block instead: a conservative
blank-line/fence-aware splitter (merges list/quote runs and indented continuations so
ambiguous structures stay atomic; reference-link/footnote/raw-HTML docs fall back to a
whole parse), each block parsed with the editor's own lexer via one reused headless
parser, assembled into a doc. This is linear and byte-identical to the one-shot parse —
measured ~15ms vs multiple seconds at 124KB+ — so the editor mount, streaming sync, and
round-trip probe are all linear, and the editable-size cap goes 24KB -> 256KB (covers
the p99 of real files). Fidelity + idempotency are pinned by unit tests, a 400-document
property/fuzz test, and adversarial edge cases (nested/loose lists, blockquotes, setext,
indented code, lazy continuation, HTML, reference links).

* fix(sidebar): render collapse state from a cookie so SSR matches

The server couldn't read localStorage, so a collapsed user's first paint
rendered the *expanded* tree at 51px — prefetched chat/workflow lists,
pinned-chat pin icons, and loading skeletons all crammed into the rail and
then reflowed once the store hydrated.

Mirror the collapse state into a sidebar_collapsed cookie (the shadcn/ui
sidebar pattern), read it in the workspace server layout, and seed the
sidebar's first render with it: structure is now correct on the server, so
the first paint is the real rail with no skeleton/pin/shift. The store
remains the post-hydration source of truth; the blocking script honors the
cookie for width when localStorage is absent so width and structure agree.

* refactor(sidebar): make the cookie the single source of truth for collapse

Consolidates the collapse machinery onto one source of truth instead of
layering the cookie on top of the legacy localStorage + CSS-mask system:

- Collapse persists only in the sidebar_collapsed cookie; the store seeds
  isCollapsed from it and drops it from localStorage (partialize + merge),
  removing the dual-write and the cross-tab desync it caused.
- Retire the redundant html[data-sidebar-collapsed] attribute + CSS mask now
  that the server emits the correct data-collapsed structure; also delete the
  dead sidebar-collapse-show/-remove/-btn rules.
- Blocking script reads the cookie for collapse (width stays in localStorage)
  and seeds the cookie once from the legacy flag so existing collapsed users
  keep their preference.
- Keep skipHydration + a pre-paint rehydrate for width only — the documented
  zustand SSR pattern, so _hasHydrated is deterministically false during SSR.

Width stays in localStorage; each field now has exactly one home.

* refactor(file-viewer): simplify + cleanup chunked-parse (linear merge, parse-once seed)

From the /simplify + /cleanup passes:
- splitMarkdownBlocks: build continuation runs and join each once instead of
  concatenating onto the growing previous block per group, which was O(n2) for a
  pathological single long loose list (now linear: 208KB loose list splits in ~3ms).
- rich-markdown-editor: seed the editor's initial content via a lazy useState
  initializer instead of useRef(parseMarkdownToDoc(...)), whose argument re-parsed the
  whole document on every render (i.e. every keystroke). Parses exactly once at mount.
- Document that the indent-merge rule is load-bearing for nested fenced code, and
  tighten the verbose inline comment blocks.

* refactor(sidebar): drop orphaned sidebar-collapse-btn class

Its CSS rule was removed with the data-sidebar-collapsed mask; the button's
collapse behavior is fully driven by the React isCollapsed ternary, leaving the
class name pointing at nothing.

* test(file-viewer): consolidate split test files into one per module

Match the dir's one-test-per-module convention: fold the markdown-parse property/fuzz
suite into markdown-parse.test.ts and the editability corpus into round-trip-safety.test.ts
(both already tested the same module from a separate-concern file). No coverage change —
same assertions, fewer files (12 -> 10).

* fix(file-viewer): make all editor controls respect read-only permissions

Every interactive control that calls updateAttributes/dispatches a command mutates
the doc even when read-only (ProseMirror commands run regardless of editable), so
gate them on editor.isEditable:
- bubble menu: the Cmd/Ctrl+K shortcut and shouldShow now bail when not editable, so
  a read-only doc can't open the link bar and setLink into it (Cursor finding).
- code block: the language picker renders as a static label when read-only (its
  onSelect mutates); copy + view-only wrap stay.
- image: no drag-to-reorder (draggable=false, no drag handle) and no resize handle
  when read-only; the image still renders and follows its link.
- links: a plain click now follows the link in read-only (reader) mode, while edit
  mode still requires a modifier so a plain click can place the cursor (Cursor finding).
Verified with new read-only permission e2e tests.

* fix(sidebar): honor collapsed cookie even when localStorage is corrupt

The blocking script read the collapse cookie inside the same try as
JSON.parse(localStorage); invalid persisted JSON fell through to the 248px
fallback and ignored a collapsed cookie, painting an expanded-width rail on
first load. Read collapse from the cookie first and parse the persisted width
in its own try so the two are independent.

* docs(sidebar): convert inline comments to TSDoc

* fix(file-viewer): resolve in-app workspace image URLs in the rich editor

The removed MarkdownPreview rewrote /workspace/{id}/files/{fileId} image src to the
serving endpoint /api/files/view/{fileId}; without it, in-app image URLs 404 in the
rich editor (Cursor finding). Re-add the rewrite as a display-only transform on the
rendered <img src> — the node's stored src attribute keeps the original path so markdown
round-trips unchanged. Absolute/non-workspace URLs pass through. Unit tested.

* fix(files): restore same-page anchor links in the rich markdown editor

Headings rendered by the TipTap editor had no slug ids (the old MarkdownPreview
got them from rehype-slug), so in-document table-of-contents links like
[section](#section) had no targets. Resolve the slug to its heading on click
(GitHub-style, duplicate-disambiguated) and scroll to it, with zero per-keystroke
cost.

* feat(files): render mermaid diagrams in the rich markdown editor

A code block renders as a Mermaid diagram when it is fenced ```mermaid or
auto-detected (an untagged fence whose first line opens with a diagram keyword,
the Linear/GitHub heuristic). Detection is display-only — the node stays an
ordinary code block and the markdown round-trips unchanged.

- Source while the caret is inside the block, diagram on blur; a Show source /
  Show diagram control plus copy, matching the code block's hover chrome.
- Clicking the diagram selects the node (same ring as an image), not flips source.
- Theme-aware (light/dark) via next-themes; the diagram frame shares the code
  block's chrome (one CSS source of truth).
- Extracted MermaidDiagram into a shared module so the editor reuses it without
  pulling preview-panel's heavy deps; rendered SVGs are memoized so toggling the
  source view and back is instant.

Covered by mermaid-diagram unit tests and the editor e2e harness.

* fix(files): harden the markdown editor (CRLF chunking, href allowlist, image escaping)

Final-audit follow-ups:
- splitMarkdownBlocks normalizes CRLF/CR first — a closing fence ending in \r
  no longer fails to match, which had collapsed Windows-authored files with
  fenced code into one block and defeated the linear chunker (perf regression).
- normalizeLinkHref rejects file://, blob:, and other non-network schemes
  (script/data schemes already rejected); network scheme:// (http/ftp/…) and
  bare host:port still pass.
- Image markdown serialization escapes alt/title delimiters and angle-brackets
  a src with spaces/parens, so they round-trip losslessly; linked-image anchors
  open in a new tab (target=_blank).
- Markdown paste routes through the chunker so a large pasted blob can't freeze
  the main thread.

* test(files): cover the code-highlight incremental re-tokenization gate

Export and unit-test changeTouchesCodeBlock: prose-only edits map decorations
(false), edits inside a code block or a setNodeMarkup language change re-tokenize
(true) — the perf-correctness path that keeps highlighting off the keystroke path.

* fix(files): keep relative links relative, navigate in-app links within the SPA

- normalizeLinkHref no longer prefixes `./`/`../` relative paths into `https://./…`
  (they round-trip and resolve correctly).
- Following a same-origin in-app link (e.g. /workspace/…) routes through the
  Next router (same tab) instead of always opening a new tab; modifier-click and
  external URLs still open a new tab.

* fix(files): linked images don't open a tab on a plain click in the editor

The linked-image anchor's native navigation was firing on a plain click in edit
mode (where handleClick intentionally returns false for caret placement). Prevent
the anchor's default so the editor's handleClick — gated on editable/modifier,
matching text links via openOnClick:false — is the sole navigator.

* fix(sidebar): match the collapse cookie value strictly (not a substring)

A substring search for 'sidebar_collapsed=1' also matched 'sidebar_collapsed=10',
desyncing the pre-paint sidebar rail and client store from the strict server read.
Parse the cookie value and compare it to '1' exactly, in both the pre-paint inline
script and readCollapsedCookie. Added a store test.

* fix(sidebar): reconcile migrated-legacy collapse before paint

A user whose collapse lived only in localStorage has no sidebar_collapsed cookie
at SSR (initialCollapsed=false), but the pre-paint script migrates them to a
cookie. The store's persist.rehydrate() is async (flips _hasHydrated after paint),
so the first paint showed expanded labels in the collapsed 51px rail. Reconcile to
the cookie synchronously in a useLayoutEffect (first render still matches the
server, so no hydration mismatch) — no narrow-rail flash.
2026-06-19 18:32:42 -07:00
Theodore Li 7349bf403f feat(files): password, email-OTP, and SSO auth for public file shares (#5140)
* feat(files): password, email-OTP, and SSO auth for public file shares

* fix(files): suppress filename in share previews for email/sso, not just password

* fix(files): normalize allow-list emails to lowercase; genericize shared SSO denial message

* fix(security): make isEmailAllowed case-insensitive; normalize email at client gates

* test(security): cover isEmailAllowed case-insensitive matching

* fix(security): bind auth cookie to auth type; password endpoint rejects non-password shares

* chore(db): format generated migration meta

* fix(files): share upsert validation returns 400 not 500; disabling always succeeds

* feat(access-control): org admins can restrict allowed file-share auth types
2026-06-19 18:53:12 -04:00
Siddharth Ganesan 5925651cbc feat(vfs): add lazy vfs + remove dynamic fields for prompt caching hits (#5138)
* feat(vfs): add lazy vfs + remove dynamic fields for prompt caching hits

* feat(vfs): send typed workspace snapshot for append-only deltas

Build the workspace inventory from the primary db (fixes replica-lag staleness)
and emit it as a typed VfsSnapshotV1 `vfs` payload alongside the markdown, so the
mothership can diff it into append-only baseline/delta messages. Generate the TS
contract mirror from the Go-owned JSON schema (sync-vfs-snapshot-contract) and
sort connector types so diffs stay byte-stable.

* fix(lint): fix lint

* fix(vfs): forward the typed snapshot through the branch payload builder

The branch buildPayload implementations hand-list the params they pass to
buildCopilotRequestPayload and forwarded workspaceContext but dropped vfs, so the
typed snapshot never reached the Go request (req.Vfs was always nil and the
append-only delta path never engaged). Forward vfs in both the workflow and
workspace branches, and add a regression guard asserting the branch threads it
through (the bug slipped past tests because post.test mocked the payload builder
and payload.test called it directly, bypassing the branch).

* improvement(contracts): update vfs contracts
2026-06-19 15:12:42 -07:00
Theodore Li 208d135dac feat(enrichment): add enrichment details sidebar with cost + provider cascade (#5139)
* feat(enrichment): add enrichment details sidebar with cost + provider cascade

* fix(enrichment): address review — persist detail on cancel/skip, exclude not_run from ran count, refetch on panel open

* fix(enrichment): keep cascade detail sticky on upsert; mark unattempted providers not_run on abort

* fix(enrichment): show Cancelled in details panel for aborted runs
2026-06-19 17:19:13 -04:00
Theodore Li 9d2a6ef043 feat(logs): redact PII from workflow logs via configurable rules (#5136)
* feat(logs): redact PII from workflow logs via configurable rules

Enterprise PII redaction for workflow execution logs, configured under
Data Retention as org-scoped rules (each rule picks entity types + which
workspaces it applies to). Reuses the guardrails Presidio engine in mask
mode at the log-persist choke point, with a check-digit-validated VIN
recognizer. Also adds per-workspace data-retention-hours overrides.

* fix(logs): widen PII entity visibleValues to string[] for strict build typecheck

* fix(logs): redact error/trigger/executionState; keep guardrails import lazy

- Extend PII redaction to span error/errorMessage/toolCalls and top-level
  error/completionFailure/trigger/executionState (Bugbot: PII in execution
  metadata). executionState is safe to redact — resume reads from the separate
  pausedExecutions table, not the log copy.
- Lazy-import validate_pii in pii-redaction so the Python/child_process
  guardrails module stays out of the static middleware/RSC graph.
- Type the org retention mutation to the contract body (optional, non-null).

* refactor(logs): drop per-workspace retention override; PII redaction stays org-scoped

- Remove the unused per-workspace data-retention-hours override (no UI; superseded
  by workspace-scoped PII rules). Reverts cleanup-dispatcher to org-only retention,
  drops resolveEffectiveRetentionHours, the workspace.dataRetentionSettings column +
  migration, and the workspace data-retention route/contract/hooks. Fixes Bugbot's
  null-as-unset finding by removing the buggy path entirely; org retention behavior
  is unchanged.
- Stop re-checking isWorkspaceOnEnterprisePlan at persist time (it returns false on
  transient errors, which would fail-open and leak PII). Enabled rules already imply
  entitlement; redact whenever rules apply (fail-safe).

* fix(logs): redact oversized strings and executionData.environment

- Drop the per-string size cap in PII redaction: oversized strings were left
  unmasked (leak). Nothing is skipped now; large payloads still fail-safe via the
  total-bytes ceiling + per-chunk timeout (scrub, never leak).
- Add executionData.environment (incl. variables) to the redaction set.

* refactor(logs): single-scope PII rules with most-specific-wins resolution

Each rule now targets one scope — all workspaces (workspaceId: null) or a single
workspace — with workspaceId unique across rules. Resolution is most-specific-wins
(a workspace's own rule overrides the all rule), not union; an empty specific rule
exempts that workspace. Matches Access Control's resolveWorkspaceGroup precedence.
UI 'Applies to' becomes a single-select; Add rule disables when all scopes are taken.

* feat(logs): default + workspace-overrides UI for PII redaction

Reshape the PII redaction settings into a 'Default (all workspaces)' block plus a
'Workspace overrides' list, making the most-specific-wins precedence explicit
(overrides replace the default; unlisted workspaces use it). Same data model
(workspaceId null = default), UI only.

* improvement(logs): clearer default/overrides PII UI

Drop the uppercase section labels and the overrides description; gate the
Workspace overrides section behind a configured default; use a single Delete
action; 'Add redaction' creates the all-workspaces default and disappears once set.

* fix(guardrails): handle stdin EPIPE in PII python spawns

Attach an 'error' listener to the child's stdin in both runPythonScript (the
batch masking hot path) and executePythonPIIDetection. A 256KB chunk can exceed
the OS pipe buffer, so if the Python process exits mid-read (OOM/kill) the EPIPE
emitted on stdin was unhandled and would crash the Node process. Funnel it into
the promise rejection so the fail-safe scrub path handles it gracefully.

* fix(logs): redact executionData.correlation

The top-level correlation field is copied from pre-redaction trigger data, so
webhook/schedule correlation values could persist unredacted. Add it to the
redaction set alongside trigger/environment.

* fix(logs): enforce unique PII rule scope server-side

The contract accepted multiple rules with the same workspaceId (or several
null all-rules); resolution is first-match, so duplicates could disagree with
the UI. Add a schema refine rejecting duplicate scopes.

* fix(logs): re-hydrate data-retention form on org switch

The form hydrated once via a boolean ref, so switching the active org left stale
retention days + PII rules and saves targeted the new org with old config. Key
hydration on orgId so it re-loads per org.
2026-06-19 17:15:46 -04:00
Vikhyath Mondreti 13b5d215e9 improvement(access-controls): docs, terminology, fix delete bug (#5141)
* improvement(access-controls): dedup independent block names

* improvement(access-controls): fix delete button, naming in perm group modal
2026-06-19 13:48:07 -07:00
Vikhyath Mondreti 91f9dfdaec improvement(governance): derived access (#5134)
* improvement(governance): org-ws-credential roles clarity

* revert isHosted

* improvement(credentials): code cleanup

* address comments

* make kb cascade delete on user hard delete

* revert env flags

* chore(db): drop local 0242 migration to regenerate after merging staging

Our 0242 collides with staging's 0242. Remove it (and its snapshot +
journal entry) so the KB-cascade migration can be regenerated with the
correct number on top of the merged staging migrations.

* chore(db): regenerate kb→workspace cascade migration as 0243

Regenerated via drizzle-kit generate on top of the merged staging
migrations (staging took 0242). Re-applied the safety edits: NOT VALID
+ separate VALIDATE on the FK re-add, and the -- migration-safe note on
the DROP. check:migrations passes.

* improve copy

* update docs
2026-06-19 12:47:09 -07:00
Theodore LiandClaude Opus 4.8 c419a34317 feat(tables): raise per-plan table limits (free 5/50k, pro 100/100k, max 1k/500k) (#5135)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 22:05:19 -04:00
Theodore Li f0b3550729 feat(files): public share links for workspace files (#5130)
* feat(files): public share links for workspace files

* improvement(files): drop reserved public_share columns until used; sync audit mock

* fix(files): share modal tracks authoritative saved state until toggled

* feat(files): per-IP rate limit on public share endpoints

* fix(files): address PR review — public CSV OOM, content cache, share FK, soft-delete filter, download anchor

* fix(files): disable CSV import action in read-only preview (public share)

* refactor(files): drive CSV preview import affordance off readOnly, not disableImport

* fix(files): version public viewer caches by file updatedAt so edits aren't stale

* fix(files): 409 (not corrupt source) when a shared generated doc has no compiled artifact

* feat(files): gate public sharing behind an access-control permission
2026-06-18 21:26:49 -04:00
Vikhyath Mondreti 267e49c69c improvement(workspaces): auto-add without invite if part of organization (#5132)
* feat(workspaces): auto-add without invite if part of organization

* reverse feature flag hardcoding

* address comments

* improve ux for org invite modal
2026-06-18 15:48:02 -07:00
Theodore LiandClaude Fable 5 63fdc472c1 improvement(block): table empty-state filter/sort builders + upsert conflict-column selection (#5123)
* ci(migrations): fail dev schema push with an actionable error on rename/drop prompt

`drizzle-kit push --force` only suppresses the data-loss confirm, not the
rename-vs-drop disambiguation prompt. That prompt fires whenever a diff both
adds and drops tables/columns at once (e.g. migration 0231 created
sim_trigger_state while dropping the workspace_notification_* tables), and in
CI it crashes with a bare "Interactive prompts require a TTY" stack trace.

Catch that specific failure in the dev push step and emit a GitHub error
annotation explaining the cause and the fix (drop the stale objects on the dev
DB to match schema.ts — the same DROPs the versioned migration already applied
to staging/prod), instead of leaving an opaque trace. Exit status is preserved
either way.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* improvement(tables): empty-state filter/sort builders + upsert conflict-column selection

* improvement(tables): throw on ambiguous upsert instead of guessing the conflict column

* Revert "ci(migrations): fail dev schema push with an actionable error on rename/drop prompt"

This reverts commit 2626482269.

* improvement(tables): unique-column picker for upsert + richer get-schema (counts, ids, live plan row limit)

* fix(tables): honor OR boundary when skipping incomplete filter rows

* fix(tables): source workspaceId for column selector from route context

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-18 18:15:12 -04:00
Vikhyath Mondreti 58312a10e5 improvement(misc): add more sportmonks tools, improvestreaming ux (#5129) 2026-06-18 12:31:54 -07:00
Siddharth Ganesan ea83be3bbe fix(mship): add folder rename tools and locked workflow status (#5126)
* fix(mship): add folder rename tools and locked workflow status

* fix(mship): manage_folder bug fixes

* improvement(mship): clean up deprecated fields from contracts

* test(mship): update tool-call display title tests for client-derived titles

Display titles now come from the Sim-side name resolver, not the stream's
ui.title/phaseLabel. Update the read lifecycle test to expect the
name-derived title and drop the obsolete phaseLabel-fallback test.

* fix(contracts): lint
2026-06-18 11:58:38 -07:00
Siddharth GanesanandVikhyath Mondreti e5f3965ed1 feat(mship): add parallel subagents, improve streaming performance (#5122)
* feat(subagents): add support for parallel subagents

* fix(subagents): address parallel-subagent bugs

* progress on streaming refactor

* improvement(subagents): update comment to reflect new go feature flag

* debug mode progress

* remove debug logs

* fix(validation): add escape annotation

* improvement(code): remove dead fallbacks

* fix subagent lane fallback issue

* fix(mothership): increase default redis event limit to 100k from 5k

* fix(mothership): streaming invariant projection enforcement

---------

Co-authored-by: Vikhyath Mondreti <vikhyath@simstudio.ai>
2026-06-18 11:27:49 -07:00
Theodore Li 597d7eafb5 fix(tables): enforce row limits against the current plan, not a frozen per-table cap (#5120)
* fix(tables): enforce row limits against the current plan, not a frozen per-table cap

* fix(tables): gate multi-batch CSV create + initial rows against the plan, harden limits cache bound

* fix(tables): thread running row count through copilot batchInsertAll capacity check

* chore(tables): align tx-variant capacity docstrings

* fix(tables): map row-limit errors to 400 in create-from-CSV import

* feat(tables): add Upgrade action to the row-limit toast

* fix(tables): keep CreateTableData.maxRows so staging callers type-check after merge

* improvement(tables): route row-limit Upgrade action to the explore-plans page
2026-06-17 22:38:55 -04:00
Theodore Li badfbc3bdf fix(resource): left-align table filter/sort when there's no search (#5128)
The unconditional ml-auto from #5117 right-aligned the embedded table
editor's filter/sort cluster, which has no search bar. Only push the
aside + filter/sort group right when a search occupies the left; without
a search it stays left-aligned as before.
2026-06-17 22:26:37 -04:00
Theodore Li 63a3e6d2cb feat(files): stream large CSV previews and add import-as-table (#5125)
* feat(files): stream large CSV previews and add import-as-table

* fix(files): validate fileId in csv-preview route, guard double-import, fix sniff perf and toggle flash

* fix(files): scope mothership preview-toggle loading guard to CSV files only
2026-06-17 21:54:18 -04:00