mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
cb63ecaefbfd14c7fc4daa26cb7c8cd9db5f4d8f
5976
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
cb63ecaefb |
fix(inbox): disable the inbox atomically and simplify its webhook tests (#6441)
disableInbox deleted the webhook row and cleared the workspace columns as two independent statements. Now that inbox_provider_id is uniquely indexed, a half-applied disable strands the id of an AgentMail inbox that no longer exists, and the next workspace to claim that address cannot enable at all. Wrap both writes in one transaction. The receiver's tests also hand-rolled a table fixture that schemaMock already provides and queued rows the shared mock returns by default. Drop both, and assert the routed inbox id on the unknown-inbox case so it fails against a revert. |
||
|
|
77bc8badf0 |
test(triggers): guard the provider granularity the path route depends on (#6437)
* test(triggers): guard the provider granularity the path route depends on The public trigger route classifies delivery by provider, not by trigger id, so a provider owning both a polling and an HTTP trigger would have its HTTP deliveries rejected. Sim already models dual-delivery services as two providers (slack / slack_app), but nothing asserted it: the existing POLLING_PROVIDERS sync assertions all still pass for a mixed provider. * test(triggers): exempt provider-gated triggers from the granularity check Classifying every non-polling trigger as path-delivered was too broad: internal and app-level-ingress providers never serve the public path route either, so mixing polling with those would have failed the invariant despite there being no routing conflict to fix. |
||
|
|
0b016c26dd |
fix(webhooks): drop the AgentMail-specific webhook body cap (#6436)
The receiver had its own 4 MB cap on the grounds that it is the only one that parses an unverified body before authenticating. It is not — the generic path receiver parses at the shared cap well before verifying auth, so the tighter limit was inconsistent rather than principled, and it could reject a large-but-legitimate email outright. The one-signature-check-per -request property comes from the tenant lookup and its unique index, not from the cap. Also records why the routing id is bounded at 320 characters. |
||
|
|
a7b016b655 |
fix(files): make the rich-markdown-field container a positioning context (#6435)
rich-markdown-field also mounts EditorBubbleMenu with its own container ref, so the same 'relative' the editor container needs applies here — otherwise the absolutely-positioned toolbar resolves against the wrong offset parent and scroll tracking breaks in the field. Safe in both modes: 'relative' adds no offset and no clipping to the uncapped (page-scrolls) box. |
||
|
|
33fe043b6b |
fix(cli): generate per-install secrets instead of using fixed values (#6433)
* fix(cli): generate per-install secrets instead of using fixed values The launcher passed the same built-in BETTER_AUTH_SECRET and ENCRYPTION_KEY to every install. Generate them once per install, persist them 0600 at ~/.simstudio/secrets.env, and reuse them on later runs so data already in the Postgres volume stays readable. Also passes INTERNAL_API_SECRET, which the realtime container requires and never received. * fix(cli): reassert owner-only permissions on the secrets file writeFileSync's `mode` applies only when it creates the file, and the write is skipped entirely when the stored values are already valid — so a secrets file left with permissive permissions kept them. chmod it on every run. * fix(cli): write the secrets file atomically Regenerating any one key rewrites all of them, and a plain write truncates first — a crash mid-rewrite would strand a still-valid ENCRYPTION_KEY and orphan the data it protects. Write to a temp file and rename into place. |
||
|
|
9f50508248 |
fix(files): pin editor bubble menus to the cursor during scroll (#6434)
The text and table toolbars were positioned `fixed` and portaled to `<body>`, so TipTap repositioned them on a debounced scroll listener — the toolbar visibly lagged and "chased" the cursor as the pane scrolled. Leave the menus at TipTap's default `absolute` strategy and append them into the editor's scroll container (now a positioning context) so they become absolutely-positioned children of the scrolled content and track the selection through native scrolling — no scroll listener, so no lag. TipTap's default `flip`/`shift` keep the toolbar above/below the selection at the pane edges, and the container's overflow clips it once the selection scrolls out of view. Verified in a harness: 0px menu-to-cell drift during scroll (was 100s of px on fast scroll), and no clipping at the top, bottom, or table edges. |
||
|
|
ea5df41a57 |
fix(webhooks): resolve the AgentMail tenant before verifying the signature (#6431)
* fix(webhooks): resolve the AgentMail tenant before verifying the signature The receiver identified the delivering workspace by trying every stored webhook secret in turn, so each request cost one HMAC over the full body per tenant. Resolve the workspace from the envelope's inbox id first and check a single secret, and make that id uniquely indexed so the one-check property holds by construction. Also gates the route through the shared admission gate, like the other public receivers, and gives it a body cap sized for an email envelope. * fix(db): declare the inbox provider index partial in the schema too The migration built a partial unique index but the Drizzle definition and snapshot omitted the predicate, so a regenerate would have diverged. Also records at the registration site why only message-bearing event types can be subscribed. * fix(db): drop the duplicate 0285 journal entry The regenerate that added the partial-index predicate appended a second entry for the same migration, so drizzle would have replayed it — dropping and rebuilding the index, leaving a window with no unique constraint. |
||
|
|
5a9f64a273 |
fix(files): bound HTML parser input before building the DOM (#6423)
* fix(files): bound HTML parser input before building the DOM Rejects HTML documents above a byte and markup-token budget before cheerio builds the document tree, and wires the rejection into the parse route's fail-closed path alongside the existing YAML one. * fix(files): classify HTML extraction RangeErrors as complexity rejections * fix(files): scope the parseFile try to the read so typed rejections propagate * fix(desktop): mock electron in tests that transitively import it url-guard, csp, and telemetry-policy all reach electron through @/main/navigation but never mocked it, so they depend on a working Electron binary download and fail when that install is incomplete. |
||
|
|
87b7b4b685 |
improvement(files): harden untrusted document preview and parsing (#6420)
* improvement(files): harden the docx preview renderer * fix(files): tighten the OOXML archive size guard against parser OOM * fix(files): apply the per-entry OOXML cap to every part, not just XML names |
||
|
|
a2ad4b693a |
fix(files): escape user filenames in the Content-Disposition header (#6430)
The download name is the user's originalName, which only rejects path separators, so a quote reached the quoted filename parameter unescaped and could close it and append parameters of its own. An injected filename* is the one that matters: RFC 6266 tells clients to prefer it, so it decides the name the file lands under on disk regardless of what the UI showed. - neutralize the quote, backslash and non-printable characters in the quoted parameter, and neutralize the semicolon there too since that fallback exists for clients liable to split parameters without honouring the quoting - percent-encode the filename* ext-value fully, including the characters encodeURIComponent leaves raw — the apostrophe is the ext-value delimiter - names that are already safe printable ASCII keep their exact previous header Also stops a control character in a name from producing an invalid header value, which previously made the download 500. |
||
|
|
05aabc3be1 | fix(mcp): apply the workspace personal API key setting to MCP serve auth (#6429) | ||
|
|
1d67e01ee4 |
fix(webhooks): centralize the path-delivery rule for trigger providers (#6428)
Fold the internal- and polling-provider exclusions into acceptsPathWebhookDelivery so the generic per-webhook path route has one predicate deciding which providers it serves, instead of the route body re-deriving it. Widen isPollingWebhookProvider to accept a nullable provider, matching isInternalTriggerProvider, and drop the resulting `?? ''` at both call sites. Regression coverage is sourced from the trigger registries so a newly added internal or polling trigger is covered automatically. |
||
|
|
441004ad24 |
improvement(file-parsers): bound PDF text extraction (#6425)
* improvement(file-parsers): bound PDF text extraction Extract page text through pdf.js's streaming API with page, character, and wall-clock budgets instead of buffering the whole document, so extraction memory stays bounded regardless of input. Release the document proxy when done, and route output through sanitizeTextForUTF8 like the other parsers. * fix(file-parsers): only flag truncation when PDF text is actually dropped |
||
|
|
52be28eb3a |
improvement(execute): enforce workspace permissions on function file exports (#6427)
Check the acting user's workspace access before a function execution uses a request-supplied workspaceId, and gate workspace file writes in the shared VFS writer so every caller is covered by default. Access is resolved once per request and threaded through the export path so the added check does not re-query per output file. |
||
|
|
3726bd22ca |
fix(knowledge): apply knowledge-base access checks consistently across auth types (#6426)
* fix(knowledge): apply knowledge-base access checks consistently across auth types The tag-definitions route only ran its knowledge-base access check for browser sessions, skipping it for internal JWT callers. Authorize on the acting user for every auth type instead — read access for GET, write access for POST — and require an acting user to be present, matching the sibling knowledge routes. Thread the acting user through the KB tag schema enrichers so their request carries the identity the route now authorizes. * chore(tests): mark tag-definition test fixtures as const |
||
|
|
9f8368e9f4 |
fix(files): anchor the editor bubble menus to the selection on scroll (#6419)
The text and table bubble menus stayed pinned to a viewport position when the file scrolled — clicking a table cell then scrolling left the toolbar floating over unrelated content. TipTap v3's BubbleMenu reposition listener defaults to `window`, but the editor scrolls inside an inner overflow container, so it never fired; the menu only moved when the selection itself changed. Pass the editor's scroll container as the BubbleMenu `scrollTarget` (a first-class TipTap option) so it repositions with the selection, and enable Floating UI's `hide` middleware so the menu hides once its anchored cell scrolls out of view. Share the anchor + options through one `floating-anchor` helper so the two menus can't drift. Removes the prior workarounds that fought this: the `strategy: 'fixed'` viewport-pin, the resolveAnchor viewport-clamp branches, and the bubble menu's selection-keyed rect cache (which froze the menu in place on scroll). Verified in a harness: on scroll the menu delta matches the cell delta (follows), and it hides once the cell leaves view. |
||
|
|
533afaaedc |
fix(docs): stop the pinned sidebar running under the site footer (#6422)
The sidebar and its divider are fixed to the viewport, so at the end of the page the footer was drawn over them and the lower part of the nav list became unreachable. FooterOverlapProbe publishes how far the footer reaches into the viewport as `--docs-footer-overlap`. The sidebar reads it as `bottom`, so it keeps its full height and slides up out of view as the footer arrives; the divider reads it too but is shortened rather than slid, so it terminates on the footer's top border instead of stopping short. Measured against the viewport rather than the document on purpose: the value is a constant 0 while the footer is off screen, so a content-height change higher up the page cannot move the sidebar. Verified with Playwright at 1280x800 and 2000x1100 — expanding/collapsing an FAQ with the footer off screen moves the sidebar 0px/0px and leaves the content column unchanged, and at the page bottom the sidebar's bottom edge lands within ~1px of the footer's top. |
||
|
|
0aae736b07 | fix(provenance): stop short secret values from rewriting unrelated log text (#6416) | ||
|
|
4f5ad2011f |
fix(tables): keep row context menu labels on one line (#6418)
* fix(tables): keep row context menu labels on one line * improvement(emcn): ellipsize menu row labels instead of clipping them |
||
|
|
6c6d8a5756 | improvement(chat): rename "Branch in new chat" to "Fork in new chat" (#6417) | ||
|
|
8a224be397 |
fix(files): read the Files prefetch from the data layer and bound invitation previews (#6415)
Two unrelated load-time fixes. On a hard refresh of /files the folders painted first and the files a beat later. Both are prefetched and hydrated together, so the files entry was not reaching the client. Each read went to its own route over an internal HTTP request; prefetchQuery swallows a rejection and shouldDehydrateQuery drops the errored entry, so a failure there silently shipped a page with that list missing, and the files read is the heavier of the two. Those two reads now call the data layer. Note the staging logs show no errors from that route, so this removes the failure mode without proving it was the one firing — the request it drops from the render path, and the shape fix below, stand on their own. listWorkspaceFilesWithShares is shared by the route and the prefetch and shapes its result through the route contract's response schema. listWorkspaceFiles returns contentUpdatedAt, which the schema neither declares nor passes through, so the prefetch was caching a field a client fetch never has and that vanished on the next refetch. The reads carry no authorization of their own now that they bypass the route, so the prefetch proves the viewer first. It reuses the layout's cached host-context lookup rather than re-deriving the permission, so the gate costs no extra queries. Separately, GET /api/invitations computed join previews in a serial loop and each preview issues up to three queries of its own, putting all of them on the critical path of the workspace switcher opening. Bounded with mapWithConcurrency; the mapper was already total, which is what that helper requires. |
||
|
|
3b0651ed9c | feat(library): Best AI Agents for Customer Support Ticket Triage and Routing (#6408) | ||
|
|
8c6166e149 |
fix(mship): return the chat connect flow to the tab that started it (#6403)
* fix(mship): return the chat connect flow to the tab that started it Connecting an integration from a chat credential chip opened OAuth in a new tab and returned there, so the user landed on a second copy of the app while the conversation they started from sat stale behind it. The flow now runs in a popup and returns through a new self-closing page at /oauth/chat-complete, which publishes its verdict to the shared attempt record and closes. The chat tab picks that up over its storage listener and updates in place, so it never navigates. A blocked popup takes the same route in a new tab and still lands on the completion page, so both paths share one verdict source. That verdict is now the server's: reaching the completion page means Better Auth routed the flow to its success callback. The previous check diffed the workspace credential list, which reported failure whenever a user re-authorized an account they had already linked -- that path updates the account row and creates no new credential. The lock is stricter than the label. A failure to create the credential from its draft is swallowed server-side, so a flow can report success with nothing in the workspace; the row stays retryable unless the credential actually appears. Also: the popup is named per attempt so sibling rows cannot renavigate each other's window; a cross-origin connect URL keeps the anchor's noopener instead of taking the popup path; the focus verifier reads the attempt after its refetch so a verdict published mid-flight is not overwritten; and the verifier treats a popup parked on a terminal page (/oauth-error, the workspace error exit) as finished rather than waiting on it forever. * fix(mship): settle the connect row from the popup, not from focus alone Addresses the review findings on the chat OAuth return leg. - Watch the popup on an interval. A provider interstitial bouncing to the workspace root, a denied consent on /oauth-error, or a closed window all end the flow without publishing a verdict or firing any event in this tab, so the row waited forever. The focus handler also no longer consumes the away flag when it defers to a live popup. - Focus an already-running popup on a repeat click instead of starting a rival attempt, which orphaned the first flow's verdict on an attempt id the row had stopped reading. - Settle from the refetched credentials on the popup success path, so the row's lock is corroborated and a connected row stops being clickable. Extracts the shared refetch-then-decide step into settleFromCredentials, used by the focus handler, the popup watcher, and the success path. * fix(mship): never read a disowned popup handle as a finished flow A provider page with COOP same-origin disowns the popup, and the disowned handle reports closed for a consent screen still running. The watcher took that as an ending and published 'failed' against a live flow. - Replace the boolean with a three-state observation. Only a same-origin terminal page counts as 'ended'; a closed-or-disowned handle is 'unobservable' and publishes no verdict. Closing a popup hands focus back to this tab anyway, so the focus verification settles that case. - Stop the interval before settling. The refetch leaves the status pending for its duration, so a running interval could fire again and resolve an attempt a retry had since replaced. - Gate the success toast on a launched-attempt ref rather than the window handle, which the watcher clears before React applies the verdict. * fix(oauth): keep the chat connect return leg in its opener's browsing context /oauth/chat-complete runs as a popup but fell into the strict COOP rule, so same-origin moved it into its own browsing-context group the moment it loaded — disowning it from the tab that opened it. That is the documented cause of a popup that is not reliably script-closable and whose opener sees window.closed report true for a live window. Matches it to its opener's same-origin-allow-popups instead, which is the directive the platform provides for exactly this case. * fix(oauth): keep every page an OAuth popup lands on observable to its opener The popup watcher settles on a same-origin terminal page, but both entries in OAUTH_POPUP_TERMINAL_PATHS were served strict same-origin COOP, which moves the popup into its own browsing-context group. The opener could then neither read its location nor trust window.closed, so the terminal-page branch could never fire in production and a flow exiting through one of those pages left the row waiting until the user happened to refocus the tab. Serves /oauth-error and the /workspace root the same same-origin-allow-popups their opener uses. The workspace root previously fell under the strict rule while every /workspace/... route already got the permissive one. * test(mship): cover the announcement surviving an early popup release The success toast is gated on the launched-attempt ref rather than the window handle; nothing pinned that. Adds the regression test, and trims the comment duplication the fix left behind. * fix(mship): bound the wait on a popup whose outcome became unobservable A closed handle and a COOP-disowned one are indistinguishable, so the watcher published no verdict for either and relied on the focus verification to settle it. That recovers the normal case — closing a popup hands focus back — but not one where the opener was never blurred, leaving the row waiting indefinitely. Arms the same safety timeout the MCP OAuth popup uses for the same reason: past it, the row decides from the credential list rather than waiting on a verdict that is never going to arrive. Cleared as soon as a real verdict lands. * fix(mship): survive a remount while an attempt is still pending The unobservable deadline lived in the watcher effect's closure, so it was armed only by the mount that launched the popup. The transcript virtualizes: a row scrolled away mid-connect came back with no window handle and no blur behind it, and nothing re-armed the bound. Derives the deadline from the attempt's own requestedAt and arms it for any pending attempt, so a remount inherits the time remaining rather than restarting the clock or losing it. A demonstrably live popup still owns the flow and is left to the watcher. * fix(mship): bind a settle to its own attempt and keep the deadline armed Two races the previous rounds left behind. A settle read the attempt only after its refetch, so a retry landing during that window was resolved by a run it never triggered — failing a replacement whose popup was still going. The attempt id is now captured before the await and the verdict only lands if it still matches; the status is still re-read after, so a verdict published mid-refetch is not overwritten. The safety deadline was one-shot. A consent screen that outlived it consumed the timeout while still live, leaving nothing to catch the popup dying unobservably afterwards. It now re-checks at the poll interval instead of expiring against a live window. * chore(mship): tighten the comments on the OAuth popup flow Trims the COOP rationale in next.config.ts to the point, and condenses the longest blocks in the connect hook without dropping the reasoning a reader needs to keep the invariants. --------- Co-authored-by: Waleed Latif <walif6@gmail.com> |
||
|
|
b5e5ca535a | improvement(ci): run the CodeQL cron weekly and cancel superseded scans (#6406) | ||
|
|
cb8338c424 |
fix(workspaces): give the pin and options button one shared slot (#6402)
The pin sat inline before an always-reserved 18px options button, so a pinned row's name lost ~18px of truncation budget — pinning visibly re-truncated the name at the moment of the click, and hovering showed pin and options together. Match the chat rows: one fixed 18px slot with both absolutely positioned, the pin fading out as the button fades in. The trailing width is now constant, so pinning cannot reflow the name. The options glyph moves to --text-icon, the canonical icon token its new sibling already uses. |
||
|
|
457170b7bf |
fix(agent): overly broad check for secrets protection (#6399)
* fix(agent): overly broad check for secrets protection * remove opaque input processing * fix * address comments * fix |
||
|
|
08af7db910 |
improvement(ui): unbold the app, align the folder chevron, tidy the feedback modal (#6400)
#6241 deleted the --font-weight-* scale from globals.css and its fontWeight mapping from tailwind.config.ts. font-medium jumped 440/480 -> 500, font-semibold 500/550 -> 600, and body dropped 420 -> 400, so every existing call site snapped a full step above a body that got lighter. #6291 fixed packages/emcn only; the product call sites were left behind. Strips the weight class from body, label, row, and heading text across app/workspace, ee, workflow-renderer, the non-workspace route groups, and components/ui/button.tsx, whose buttonVariants injected font-medium into every consumer. Keeps it only where it steps up: markdown/prose bold and micro avatar initials. Also aligns the workflow-tree folder chevron to the sidebar section header (14px, 150ms), and on the feedback modal drops the prompt line above the Feedback field and re-homes Copy ID as a footer secondary action. |
||
|
|
3b4d587b55 | feat(demo): fire the X conversion event when a demo is booked (#6401) | ||
|
|
1c5393ad6d |
feat(workspaces): pin workspaces and widen the switcher to six rows (#6397)
* feat(workspaces): pin workspaces and widen the switcher to six rows Show up to six workspaces in the switcher instead of three, keeping the search input from six onward so it appears exactly when the list fills. Pin workspaces to the top of the switcher via the existing row context menu. Pins are per-user and global, so they live on the user's settings row rather than in `pinned_item`, which scopes every row to one workspace. They ride along on the /api/workspaces payload the switcher already loads, so the server prefetch hydrates them and pinned-first ordering never re-sorts after hydration. Drop the seat/workspace-migration disclosure copy from both invitation accept surfaces. The accept-time disclosure tokens are unchanged, so the server still verifies the outcome hasn't shifted since the page loaded. * fix(workspaces): serialize pin writes so a rapid toggle cannot be undone Each write carries the whole pin list, so two overlapping requests that the network delivered out of order left the earlier click as the stored state. Chain them instead, and hold reconciliation until the last queued write settles — refetching between two writes rendered the server's intermediate state and bounced the row out of the pinned group and back. * refactor(workspaces): store workspace pins in pinned_item, not user settings Workspace pins were a jsonb array on the settings row, replaced wholesale on every toggle. That shape is what forced the write serialization in |
||
|
|
d317607d2c |
feat(dynatrace): add the write and configuration surfaces (#6398)
* feat(dynatrace): add the write and configuration surfaces Takes the block from 22 operations to 47. The original PR shipped the read paths plus a few ingests; this closes the gaps that made those reads dead-end. The one that was a real defect: security was read-only. The audit-vulnerabilities skill promised "a remediation queue" and then gave you no way to act on it, even though muting is the single most common triage action. Adds mute and unmute, singly and in bulk, plus the remediation items behind a third-party finding, plus the Attacks API so an exploited vulnerability can be traced to the request that exploited it. The rest, by how much they unblock: - Custom tags (read/add/delete). Entity tags already drive every selector in the block; being able to write them closes a loop that was half open. - Settings objects (schemas, list, get, create, update, delete). This is how maintenance windows, alerting profiles, and management zones are configured in modern Dynatrace, so "open a maintenance window before the deploy" was simply unreachable before. The value is a schema-defined blob, so the tool is honestly opaque rather than falsely typed; the docs tell you to mirror an existing object. Update and delete carry the updateToken so a concurrent change fails instead of being overwritten. - Synthetic monitors and on-demand batch execution, which pairs with the deploy-marker tool to gate a release on a smoke test. - Problem comment get/update/delete, and SLO create/update/delete, completing CRUD that was previously half-built. Two structural notes. Synthetic monitors are the only endpoints still on Environment API v1, so `buildDynatraceUrl` grew a v1 sibling and the shared base-URL normalizer now strips either version; the query builder also learned to repeat a param per value, which Synthetic's `tag` needs. And creating an SLO returns 201 with an empty body and the new ID in the Location header, so that tool reads the header rather than parsing nothing. Deliberately excluded: the Grail/DQL query API. It is the long-term successor to the deprecated logs/search endpoint, but it authenticates with a platform token rather than an Api-Token, so it is a second auth path and belongs in its own change. * fix(dynatrace): drill the documented JSON shapes, and require the tag selector Two problems, one found in review and one worth more than it was given. The tag operations could run without an entity selector. All three tag tools declare `entitySelector` required, but the shared block field was only marked required for List Entities, so the block let a workflow reach those tools with an invalid configuration and let Dynatrace do the rejecting. My own structural auditor missed it because it only checked that *some* visible subBlock existed for a required param, not that the specific one was required — that check is now precise, and it confirms these three were the only instances across all 47 operations. The larger one: outputs were declaring `type: 'json'` for shapes the API reference documents in full. Thirty-five of them. The top-level entities were mapped properly, but nested payloads — a problem's evidence and impact analysis, a vulnerability's risk assessment and global counts, an attack's attacker, request, entry point and exploited vulnerability, a remediation item's assessment and mute state, the synthetic execution and failure records, the metric ingest error envelope, the DQL translation — were passed through as anonymous blobs. A downstream block could not reference `attacker.sourceIp` without knowing to guess it. All of those now carry their fields. What stays opaque is now only what genuinely is, and each says why in its description: a settings object's schema-defined value, an entity's type-dependent property bag and relationship keys, caller-supplied synthetic metadata, an audit log's JSON patch, the undocumented partial-success body of log ingestion, and the handful of security-detail shapes the reference names without expanding. * fix(dynatrace): make the synthetic enabled filter tri-state Review catch. `enabled` on List Synthetic Monitors is a three-way filter — enabled, disabled, or either — and I had it as a switch. The URL builder deliberately serializes `false` (there is a test pinning that `evaluate=false` survives), so leaving "Enabled Only" unchecked sent `enabled=false` and returned only the disabled monitors: exactly backwards. Made it a dropdown with Any / Enabled only / Disabled only, matching the monitorType field directly above it, which had the same shape and already used an empty-id "Any" option. The params mapper sends nothing for "Any". Checked the other nine switches rather than assuming. None share the bug: for each of them off genuinely means false, and false is Dynatrace's own default, so serializing it is correct. A test now pins that list so the trap cannot be re-introduced by converting one of them, alongside a test covering all three states of the filter. |
||
|
|
2e7e5ae804 |
feat(dynatrace): add the Dynatrace integration (#6393)
* feat(dynatrace): add the Dynatrace integration
Adds a Dynatrace block backed by 22 Environment API v2 tools, covering the
surfaces an observability workflow actually reaches for:
- Problems: list, get, close, list comments, add comment
- Metrics: query data points, list and get descriptors, ingest line protocol
- Entities: list, get, list entity types
- Events: list, get, ingest
- Logs: search, ingest
- SLOs: list, get
- Application Security: list and get security problems
- Audit log: read
Every request path, query parameter, and response mapping is taken from the
published Dynatrace API reference — no inferred fields. Auth is an access
token sent as `Authorization: Api-Token ...` against a user-supplied
environment URL, so SaaS, Managed, and environment ActiveGate all work.
Two details worth knowing:
`ingest_event` exposes Dynatrace's event timeout as `eventTimeout`, not
`timeout`. The tool transport reserves `params.timeout` for the HTTP request
deadline, so the obvious name would have silently retargeted the wrong knob.
`get_metric` encodes its path segment with `encodeDynatracePathSegment`
rather than `encodeURIComponent`, which leaves the `:` separators in metric
keys and transformation operators intact, matching the docs' own examples.
* fix(dynatrace): close the gaps a validation pass turned up
Three real defects and one usability gap, all found by auditing the tools
against the Dynatrace API reference a second time.
`ingest_logs` double-encoded its payload. `logs` is a `json` param, and a
`json` param arrives as a *string* whenever it comes from a long-input field
or an LLM tool call — only a block-to-block reference hands over a parsed
value. `JSON.stringify` on that string produced `"[{...}]"`, so Dynatrace
received a quoted string where it expected an array. The block hid this in
the UI path by pre-parsing, but the parse lived in `tools.config.params` and
*threw* on malformed input, and it never covered the direct tool-call path at
all. Both tools now normalize through the shared `parseJsonParam`, so the
tool is correct regardless of who calls it, and the block just forwards the
raw value. `ingest_event.properties` had the identical bug.
Path identifiers were not trimmed. A problem or entity ID pasted with a
trailing newline became `%0A` in the URL and 404'd with nothing to suggest
whitespace was the cause.
Errors dropped the part that matters. Dynatrace's ErrorEnvelope carries
`constraintViolations[]`, which names the offending selector or parameter;
the generic `nested-error-object` extractor returns only `error.message`
("Constraints violated."), and which extractor won was left to fallback
order. Adds a `dynatrace-errors` extractor that folds the violations into the
message and pins it on all 22 tools. It sits after `nested-error-object` in
the chain, which already matches this shape, so no other service's error
handling changes.
Adds 21 tests covering URL construction for SaaS/Managed/ActiveGate, cursor
pagination dropping sibling filters, identifier trimming, metric-key colon
preservation, both JSON-param paths, the `eventTimeout` -> `timeout` mapping,
EntityStub flattening, the audit log's dotted `dt.settings.*` keys, and the
204/200 split on log ingestion.
* docs(dynatrace): add the page intro, and pin every response key in tests
Adds a MANUAL-CONTENT:intro block to the generated integration page covering
what the block reaches, how to get an environment URL and a scoped token for
SaaS vs Managed, how selectors work, and how cursor pagination behaves.
Verified it survives `generate-docs.ts` byte-identically.
Also closes the last silent-failure gap the validation pass left open. A
wrong top-level response key does not throw — it maps to an empty array and
reads as "no results", which is indistinguishable from a genuinely empty
environment. Dynatrace is unusually easy to get wrong here: the SLO list
returns `slo` (singular) and the metric query returns `result` (singular).
Adds a table-driven test asserting the documented key for all ten list
endpoints plus the scalar keys of the ingest and single-entity responses.
Confirmed it bites by flipping `data.slo` to `data.slos` and watching only
that row fail.
* chore(dynatrace): type the shared param map as unknown
Review follow-up. `Record<string, any>` in the block's params builder dropped
compile-time checking from every operation's shared params; `unknown` is
enough here since the values flow straight into the tool param maps. Matches
.claude/rules/sim-typescript.md, which sibling blocks (Datadog, Grafana)
still violate.
* fix(dynatrace): stop three silent failures found in a final read-through
All three turn a failed call into something that looks like a successful
empty one, which is the worst shape for an observability integration — you
cannot tell "nothing is wrong" from "the call did not work".
`readJsonBody` swallowed any unparseable body and returned `{}`. A gateway
HTML page, a captive-portal interstitial, or a truncated payload therefore
mapped every field to null and read as "no problems found". Only genuinely
empty bodies are tolerated now (201 from add-comment, 204 from log ingest);
anything else that will not parse raises with a truncated preview.
`ingest_logs` sent `[]` when the payload was missing or empty. Dynatrace
answers 204 to that, so the tool reported `accepted: true` for a call that
shipped no logs. It now fails loudly instead.
`encodeDynatracePathSegment` percent-encoded the whole metric key and then
regex-unescaped `%3A` back to `:`. Same output, but it undoes the encoder's
work and hides the intent. Colons are structural in a metric key, so it now
splits on them, encodes each part, and rejoins — which says that directly.
Each fix has a test, and each test was confirmed to fail in isolation with
only its own fix reverted.
|
||
|
|
5cf1f9be84 |
improvement(provenance): cleanup secrets boundary (#6374)
* fix(secrets): preserve raw outputs with durable provenance * improvement(provenance): cleanup boundary * fix copy resources * fix fork copies to work with provenance * address comments * fix |
||
|
|
40c0a571fd |
fix(files): stop the file-viewer image reshift on open (#6390)
* fix(files): reserve embedded-image space on direct file-view loads An embedded image in a markdown file reshifted on every open: it loaded ~2.3s in with no reserved box and shoved everything below it down (CLS ~0.17). The intrinsic dimensions ARE stored server-side, but the image node view never read them at render. useWorkspaceImageDimensionsAdapter read the active files list via queryClient.getQueryData — non-reactively — so on a cold file-view load it returned null at first render and, because the adapter identity was stable, never re-checked when the list later resolved. Read the list via a reactive useWorkspaceFiles subscription instead: the adapter re-runs the image node view's memoized dimension read when the list resolves, so it reserves the box from the stored dimensions before the (slower) image download finishes. The query key is shared, so it dedupes with surrounding views. Gate it behind `enabled` (driven by the absence of a caller-supplied contentSource) so the public share page — which passes a share token as workspaceId — doesn't fire a 404. Verified in a CLS harness: dims present -> 0 shift; dims absent -> 0.20. * fix(files): stop the duplicate 'mention' extension-name warning The @-mention menu extension and the mention node were both named 'mention', so TipTap logged "Duplicate extension names found: ['mention']" on every editor (twice under the collaborative placeholder + live pair). Rename the menu extension to 'mentionMenu' (the node keeps 'mention', its persisted doc-node type) and move its editor.storage.mention -> editor.storage.mentionMenu. |
||
|
|
9b80fddd00 |
fix(chunkers): preserve FAQ prose in docs chunks (#6383)
* fix(chunkers): preserve FAQ prose in docs chunks
cleanContent deleted every FAQ section from the embedding index: the
multiline tag strip swallows an entire <FAQ items={[...]}/> block (it
matches from <FAQ to the first ">", often inside an answer string), and
the brace strip eats any surviving { question, answer } items — 637
Q&As across the docs never reached search, with mangled JSX fragments
embedded in their place. Consume FAQ blocks whole before the tag strip
and emit their question/answer text as plain prose, escape-aware so
braces and quotes inside answers survive. Tag and brace stripping are
otherwise unchanged — a corpus survey showed FAQ props are the only
place real page prose lives inside JSX syntax on searchable pages.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(chunkers): accept single-quoted FAQ items with trailing commas
session-policies.mdx and verified-domains.mdx write FAQ items with
single-quoted multiline values and trailing commas; the double-quote-only
item pattern matched nothing there, so the component consumer replaced
those whole FAQ blocks with a space. Capture either quote style
escape-aware (quotes of the other style inside a value are fine) and
allow the trailing comma; captured values keep their quotes and are
unquoted before unescaping.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* style(chunkers): wrap the FAQ replace call for biome
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
73bac1adc2 |
improvement(admin): move user row actions into an overflow menu with confirm modals (#6384)
* improvement(admin): move user row actions into an overflow menu with confirm modals * fix(admin): surface password reset status outside the actions menu * fix(admin): surface ban and role change errors inside the confirm modal * fix(admin): reset the ban mutation when opening the confirm modal * improvement(admin): simplify the user row actions after review passes * fix(admin): show password reset progress while the request is in flight * fix(admin): confirm the role change the admin chose, not the live row's inverse * improvement(admin): align the role confirm and reset feedback with house patterns |
||
|
|
a84260fc56 |
improvement(mship): questions improvement (#6385)
* credentials continue * fixes |
||
|
|
de5fcf9731 |
fix(providers): stop reporting an absent Ollama as an error (#6387)
* fix(providers): stop reporting an absent Ollama as an error Ollama is optional and its URL falls back to a loopback default, so a deployment that runs none refuses the probe on every poll — 10,068 of these in 14 days, the single largest error stream in the app, all of them the same expected condition. Report it the way the vLLM and LiteLLM routes already report an unconfigured base URL, and skip the probe entirely on the hosted platform, which has no local runtime to reach. An explicit OLLAMA_URL is still honoured everywhere, so a self-hosted deployment behaves exactly as before — including the localhost default that requires no configuration. * fix(providers): keep an unreadable Ollama response out of the not-reachable path The single catch covered the connection, the JSON read, and the schema parse, so a server that answered but answered wrongly was filed as 'no Ollama here'. Scope the quiet path to the connection itself and report an unusable response as the fault it is. |
||
|
|
c9aed7af99 |
fix(jsm): accept the numeric pagination the JSM tools actually send (#6386)
* fix(jsm): accept the numeric pagination the JSM tools actually send The JSM tools declare start/limit as type: 'number' and the block coerces Max Results with Number.parseInt, but every /api/tools/jsm/* contract typed them as z.string(). Any JSM read with pagination filled in 400'd before reaching Atlassian, and get_queues 400'd unconditionally because the block always sends includeCount as a boolean. Normalize both shapes at the contract boundary, add the missing Start Index block input, and route Max Results through the existing toOptionalInt helper so a non-numeric entry no longer sends NaN. * improvement(forking): widen the fork mapping target picker further 320px still clipped the longest secret keys the picker shows. * fix(jsm): cap pagination at the documented int32 maximum Addresses review: the schema claimed the int32 range but only floored at 0, so values above 2147483647 were forwarded to Atlassian instead of being rejected at Sim's boundary. Also restores the const tuple for the paginated operation list and drops the widened ToolConfig from the test table. |
||
|
|
d2964af7d2 |
fix(chat): re-measure the prompt editor when its width changes (#6380)
* fix(chat): re-measure the prompt editor when its width changes The chat input's textarea grows to its full content height under a mirror overlay, but it only re-measured on text change. A width change after typing (window resize, sidebar toggle, resource panel opening) left the textarea at a stale inline height while the overlay rewrapped taller. The spilled lines still painted and scrolled but had no textarea beneath them, so clicks landed on the scroller and never placed a caret. Re-measure on width change only — the measure writes the textarea's height, so reacting to height would feed itself. * fix(chat): measure the observer's first delivery like any other The width can change between the mount-time measure and observe(), so treating the first notification as confirmation of the mount width dropped that change and left the stale height in place. * chore(chat): trim duplicated comments on the prompt editor autosize The failure mode was documented in four places. Keeps one canonical explanation next to the guard and leaves only the per-test whys the test names do not already carry. |
||
|
|
a7080d5fda |
improvement(forking): widen the fork mapping target picker and make it searchable (#6381)
* improvement(forking): widen the fork mapping target picker and make it searchable * fix(forking): stop the truncated-candidates hint promising a search that cannot reach past the cap |
||
|
|
eec3c352f3 |
fix(files): render the file-viewer placeholder through the live node views (#6379)
* fix(files): render the file-viewer placeholder through the live node views
The collaborative markdown viewer painted a static generateHTML placeholder while
the Yjs doc seeded, then swapped to the live editor. generateHTML runs only schema
renderHTML — never the React node views or the ProseMirror decoration plugins — so
every node whose live appearance comes from a node view or a decoration rendered
differently in the placeholder and visibly repainted on the swap: syntax highlighting
popped in, mention-chip icons shifted their labels, mermaid blocks jumped from source
to diagram, and media embeds appeared out of nowhere.
Render the placeholder through a read-only editor that shares the live editor's
extension set instead. It uses the same node views and decoration plugins, so the
placeholder is pixel-identical to the live editor and the swap neither repaints nor
reflows — highlighting, mention icons, images, mermaid (via its existing SVG cache),
and embeds (which already reserve their aspect-ratio box) all render up front. The
placeholder editor carries no Collaboration extension, Y.Doc, or awareness, so it
structurally cannot write to the shared document, preserving the seed-only-on-server
invariant; editable={false} disables every editing affordance.
* fix(files): address review on the placeholder editor
- Give ReadOnlyPlaceholder a named props interface (repo component convention).
- Render the placeholder synchronously (immediatelyRender: true) so it paints
instantly like the static HTML it replaced instead of blanking for a frame
while the editor mounts — safe because this surface is client-only, never SSR'd.
- Hoist the editor reading-column classes into a shared EDITOR_SURFACE_CLASS so
the placeholder and live editor stay geometrically identical (drop the now
redundant placeholderContent term from the live editor's hidden class).
|
||
|
|
3f743d4b78 |
fix(uploads): treat a missing storage object as absent metadata, not a failure (#6378)
* fix(uploads): treat a missing storage object as absent metadata, not a failure A workspace file is rewritten under a new key on every content update and the superseded object is deleted, so any reader holding the previous key finds nothing. getFileMetadata's provider lookups let that not-found propagate, so authorization's catch-all logged it at ERROR and never reached the branch already written for it. Return the function's established empty value instead, and collapse the three divergent per-provider not-found predicates onto one. * fix(uploads): read the not-found label from code as well as name Azure raises a RestError whose name carries the class and whose code carries the reason, so testing name first and falling back to code only when name was absent missed BlobNotFound outright — narrower than the per-provider check it replaced. * fix(uploads): keep a missing bucket or container out of the not-found path NoSuchBucket and ContainerNotFound also answer 404, so the status-only match read a total storage misconfiguration as an absent object — every file read would fail closed with nothing left to alert on. * fix(uploads): require an object-level label before treating a lookup as absent GCS answers a missing object and a missing bucket identically, so a bare 404 cannot be attributed to the object by a dispatcher that does not know what was requested. getFileMetadata now takes the labelled check and leaves an unlabelled 404 propagating as before; the provider clients keep the lenient form, which is what each already used. * refactor(uploads): let getFileMetadata delegate to the provider head helpers getFileMetadata re-implemented the S3 and Blob HEAD calls inline, so it had to inspect provider errors itself and needed a second, stricter predicate to do it safely. headS3Object and headBlobObject already perform exactly those calls and already report absence as null, so delegating removes the duplication, the error inspection, and the extra predicate at once. GCS keeps raising, as before. Covers the real provider path in the S3 client's own suite, where mocking the seam had been hiding whether the two layers agree. * fix(files): log a missing file at info rather than error when serving Each serve handler rethrows into the outer one, so a superseded key produced two ERROR lines for what is an ordinary 404 — two thirds of this module's error volume. Route all five catch sites through one helper that reserves error for failures that are actually the server's fault, matching how DocCompileUserError is already handled a few lines above. * test(uploads): cover the Blob not-found paths the shared predicate now governs S3 and GCS already asserted absence and non-404 rethrow; Blob asserted neither, so the container-level exclusion went unverified on the one provider whose error puts the reason in code rather than name. |
||
|
|
77649d3982 |
fix(sidebar): keep the pin visible on the chat you're viewing (#6377)
The pin glyph carried a stale `!isCurrentRoute` guard copy-pasted from the status dot back when the dot was also hidden on the current route. #4354 later relaxed the dot's guard but left the pin's untouched, so opening a pinned chat made its pin vanish. Derive `showStatusDot` once and express the pin as its negation so the two conditions can no longer drift apart. Also align the collapsed rail, which never forwarded `isCurrentRoute` and so showed an unread dot on the chat you were already reading, and hide the pin by the same opacity mechanism the dot uses instead of a display toggle plus a mount guard. |
||
|
|
1305e9d723 | chore(deps): bump mermaid to 11.16.1 and js-yaml to 4.3.1 to clear open Dependabot alerts (#6375) | ||
|
|
e6a17b03e9 |
improvement(tables): show a tooltip on truncated column headers (#6371)
* improvement(tables): show a tooltip on truncated column headers * chore(tables): use absolute import for HeaderLabel |
||
|
|
69e3e177ac |
feat(library): Best AI Agent Builder in 2026: Sim Leads for Open-Source, Self-Hostable Teams (#6369)
Co-authored-by: Sim Pi Agent <pi@sim.ai> |
||
|
|
6599b4c428 |
fix(chat): keep the composer remove badge anchored and align chip tokens (#6365)
* fix(chat): keep the remove badge anchored to the file card
The card wrapper had no width cap, so it sized to the filename's max-content
width while the card itself capped at 220px. The remove badge is positioned
against that wrapper, so a long filename stranded it far to the right of the
card it belongs to.
Moves the cap onto the wrapper and lets the card fill it.
* fix(chat): make attachment tiles read on every surface they render on
The sent-message tile went icon-only, which made its fill the whole
affordance — and against the workflow chat panel's --surface-1 that fill is
~8/255 away in light mode. Adds the border the user message bubble already
pairs with --surface-5 for the same reason.
- Restore an accessible name to the sent tiles: an icon-only div with a title
attribute announces as nothing.
- Step the composer icon badge on hover; the chip's hover fill closed to
within 7/255 of it in light mode.
- Extension label moves to --text-icon/text-caption; --text-muted was 2.4:1
on this fill in dark mode, well under AA.
- Tooltip.Content no longer re-declares the width and truncation it owns —
it was truncating the very name it exists to reveal.
* improvement(chat): restore sent-attachment filenames and align chip tokens
- Revert the sent-message attachments to main's styling: the icon-only tile
dropped the filename, leaving no way to tell what was sent.
- Radii onto the scale: --radius is 8px, so rounded-[10px] was off-system in
both files. Outer surfaces use rounded-lg, the nested icon badge rounded-md.
- Pill filename uses the named text-xs rather than an arbitrary text-[11px].
- The remove badge is opaque instead of a translucent scrim, so it reads the
same over a light card and over a photo rather than compositing with each.
* improvement(chat): tighten composer chip markup and tokens
- Remove the chip tooltips: the document card already shows its filename, so
the tooltip mostly restated it.
- Collapse the single-use height constant and use size-[48px] on the media
branch, which was h-[48px] + w-[48px] split across two class strings.
- Remove badge moves to --surface-2; --surface-1 sat 8/255 from the chip fill
in light mode, reachable on a coarse pointer where the chip's hover-hover
fill never applies. Its hover gating now matches the chip's.
- py-[7px] so the 32px icon badge fits the 48px box instead of overflowing it.
- Trim comments to TSDoc or one-line rationale per the repo rule.
* fix(chat): keep the remove badge reachable on coarse pointers
Gating the reveal on hover-hover alone would hide it from touch entirely,
since that variant is fine-pointer only. Instead it is visible by default and
only fine pointers get reveal-on-hover, so the badge never depends on an
emulated hover.
* fix(chat): reveal the remove control on keyboard focus
On a fine pointer the badge is transparent until hover, so tabbing to it left
a sighted keyboard user unable to see which attachment Enter would remove.
The focus-visible chain carries higher specificity than the hide rule, so it
wins regardless of source order.
* improvement(chat): drop the icon badge's own hover step
The chip's hover is the only hover affordance needed. The badge fill is now
constant, sitting one step below --surface-6 in light mode so the chip's
hover fill cannot close on it — which is what the per-badge step was
compensating for.
* fix(chat): stop gating the remove badge on a variant that cannot express it
hover-hover expands to '@media (hover:hover) and (pointer:fine) { &:hover }',
so it binds to the element carrying the class. On the badge that meant every
rule required hovering the badge itself, making the whole chain dead CSS —
the badge was simply always visible.
Rather than rebuild the gating, drop it: an always-visible control is
reachable on touch and stays visible while holding keyboard focus, which the
reveal-on-hover form could not manage without special cases for both.
|
||
|
|
0601fcda5f |
fix(files): render HTML files shared via a public file link (#6363)
The public share page rooted on `.desktop-title-bar-page`, which sets only `min-height: 100vh`. Without a definite height, `h-full` on every descendant of `<main>` resolved to `auto` -> 0. `HtmlPreview` gates its sandboxed iframe on a measured non-zero container, so it silently never mounted and the page rendered a blank area under the header. Other read-only branches survived because their content has intrinsic height. Give the public page root a definite height, and make the read-only preview chain flex-based so it fills its parent instead of depending on an ancestor's definite height. `text-editor`'s preview pane becomes a flex column for the same reason -- it is `HtmlPreview`'s other parent. |
||
|
|
1c0e82a4e2 |
perf(ci): parallelize repo audits, guard env-dependent tests, and fix the docs generator (#6358)
* perf(ci): parallelize the repo audits and guard env-dependent tests
The 21 independent audits ran as 21 sequential CI steps, each a single-threaded
read-only walk of the tree. scripts/run-audits.ts runs them concurrently:
28s serial -> 5.0s wall locally at 13-way. It buffers each audit's output and
replays only failures, so a green run stays quiet and a red one still names the
audit and shows why. Audits needing a git base ref (block registry, migration
safety) or that write files (drizzle generate) stay as their own steps.
Also fixes 5 tests that fail for every macOS dev and are invisible in CI. They
shell out to python3 using `match` statements and 3.12 f-string nesting, which
need >= 3.10; stock macOS ships 3.9.6, so `bun run test` produced raw Python
SyntaxErrors with no guard and nothing tying them to a missing tool. One also
needs ripgrep, which CI installs and a Mac usually does not.
@sim/testing/environment detects both and the tests skip with a reason via
vitest's ctx.skip(). Under CI it throws instead: these suites deliberately run
the real helper rather than a mock -- the cloud-review path/read-size bounds and
the placeholder compiler's generated Python are only observable that way -- so a
missing tool in CI means a security boundary silently stopped being covered,
which is worse than a red build.
Drops the Codecov upload. The workflow already documented it as a dead path:
nothing generates apps/sim/coverage, vitest runs without --coverage, and
fail_ci_if_error hides it, so it reported green having uploaded nothing.
* fix(ci): raise the python floor to 3.12 and stop the bridge audit serializing the batch
Two review findings, both real.
MIN_PYTHON was 3.10, chosen for the `match` statements the compiler suite
generates. But two of the three guarded tests also use PEP 701 f-strings --
reusing the outer quote, and embedding `#` -- which are 3.12. Verified on a real
3.11 interpreter: the match-guard test passes, the other two fail with
`f-string: unmatched '('` and `f-string expression part cannot include '#'`,
which is exactly the raw SyntaxError the guard exists to prevent. A 3.10 floor
let them through and failed anyway.
The audit parallelization did not speed CI up -- it slowed it down. Serially the
21 audits took ~31s; concurrently the batch took 39.2s wall, because
check:desktop-bridge went from 1s to 39.2s and became the entire wall clock while
the other 20 finished in 9s. It is the only audit that shells out through `bunx`,
which re-resolves the package against the shared install cache -- a network-backed
sticky-disk mount on CI. Cheap when it runs alone, serialized behind the others
when they run together. Spawning the resolved compiler entry point directly
removes that layer.
Verified the audit still fails on a breaking bridge change rather than passing
faster by doing less.
* fix(docs): unbreak the MDX build and read trigger config from the registry
The docs build has been failing on staging since the Smartlead merge:
./apps/docs/content/docs/en/integrations/smartlead.mdx
Expected a closing tag for `<original>` before the end of `paragraph`
Tool descriptions are emitted as prose, and that path escaped only braces --
every table-cell path already escaped angle brackets. MDX reads `<` as the start
of a JSX tag, so a description like 'The copy is named "<original> - copy"' fails
the build outright. escapeMdxProse handles the MDX-hostile characters and leaves
pipes, parens and brackets alone, which are legal in prose and whose escaping
would mangle markdown links.
Trigger configuration now comes from the evaluated registry instead of regex over
source. Static parsing silently dropped every field whose builder assembled its
array imperatively or took a description as a parameter -- all ten Jira triggers
lost `webhookSecret` and `jqlFilter` that way, and Monday lost its config too, so
regenerating the docs was destructive. Reading real objects also deletes 232 lines
of parsing. Note `required` may be a condition object rather than `true`; only an
unconditional `true` renders as Required, matching the previous behavior.
Tool headings now show the tool's name ("A2A Send Message") rather than its id
(`a2a_send_message`), unformatted, across 241 generated pages. Names come from
tools/generated/tool-metadata.ts, which CI keeps in sync. These headings feed each
page's table of contents. a2a.mdx is hand-written, so its headings were updated
directly.
Also consolidates five hand-inlined copies of the escape chain into the
escapeMdxCell that already existed, and drops 44 comments that restated the line
below them. Generator: 4306 -> 4069 lines.
Every refactor step was verified against a golden manifest of all 289 generated
files -- proven deterministic across runs and proven to catch a one-character
change -- so the only output differences are the intended ones.
KNOWN GAP: extractTriggerOutputs still parses source and has the same blind spot;
it already drops one Jira output section on main. Regenerating is now safe for
trigger config but still lossy for trigger outputs.
* refactor(ci): derive the audit list and stop shelling out through bunx
Review pass over the audit runner and the tool guards.
The audit list was hand-maintained alongside package.json with nothing linking
them, and it had already drifted: check:cron-parity exists, passes, and ran in no
CI step at all. The list is now derived from the check:* scripts with an explicit
exclusion map, so a new audit is opted out deliberately rather than forgotten.
That picks up cron-parity — 22 audits now, not 21.
check-realtime-prune-graph.ts still shelled out through `bunx turbo`, the same
pattern that took the bridge audit from 1s to 39s once the audits ran
concurrently. Both now go through scripts/local-bin.ts, which resolves
node_modules/.bin — the same path check:native-typecheck asserts is the native
TypeScript 7 compiler, so the one guarded path is the one that runs.
Audits are spawned as their script rather than `bun run <name>`, which started a
bun process only to read package.json and start a second one.
Tool detection is memoized per process; it was re-spawning python3 on each of the
5 call sites, in every vitest worker. The CI throw is deliberately NOT memoized —
memoizing it would turn every call after the first into a silent skip, which is
the failure mode the guard exists to prevent. Verified it still throws for all
three guarded tests, not just the first.
Also: dropped the environment module from the @sim/testing barrel so
node:child_process stays out of unrelated consumers' module graphs, restored the
per-audit reporting the 21 separate steps used to give (collapsible groups, error
annotations, and a timing table they never had), and trimmed comments that
restated their code or duplicated the runner's own docs.
* fix(devin): give the 11 Devin tools real display names
Every Devin tool had its id as its `name` (`list_session_messages`), so the
generated docs rendered `### list_session_messages` where every other integration
renders a human name. It was the only integration doing this -- 11 of 4427 tools.
Names take the service prefix, matching the majority convention (3200 of 4416
names start with their service).
Also points the ship skill at check:audits instead of hand-listing the audits.
That copy had drifted five behind package.json: cron-parity, import-specifiers,
sql-date-binding, trigger-block-cycle and native-typecheck were all missing, so
shipping never ran them. It was the third copy of that list; there is now one.
* fix(docs): read trigger outputs from the registry too
Closes the gap left by the config fix: extractTriggerOutputs still parsed source,
so triggers whose outputs come from a builder call lost their tables. jira_webhook
had no output section at all.
The registry was not a drop-in, which is why the naive swap deleted 10,298 lines
earlier. The two sides encode nesting differently. A TriggerOutput marks a group
by OMITTING type and holding children as sibling keys:
issue: { id: { type: 'number' }, title: { type: 'string' } }
while the renderer walks the JSON-Schema-ish shape the parser used to synthesize:
issue: { type: 'object', properties: { id: …, title: … } }
formatOutputStructure only descends into .properties, so handing it the raw
registry value collapsed every nested group to one untyped row and dropped its
children. normalizeTriggerOutputs converts between the two, preserving leaves
that already declare properties/items and merging the 13 hybrid nodes that carry
both a type and inline children.
Measured across all 368 triggers before changing anything: 155 identical, 213
divergent, and the divergence was purely the nesting encoding — no node has a
non-string type, and a group never carries its own string description, so
leaf-vs-group classification is unambiguous. That is what makes a nested property
literally named 'description' (42 of them) survive.
Deletes the static path: extractTriggerOutputs, resolveTriggerBuilderFunction,
resolveTriggerOutputsConstant, readTriggerSiblingModules,
getWebhookProviderConstants, plus resolveConstStringValue and matchQuotedProperty
which the config fix had already stranded.
20 output sections recovered (linear 79->93, tiktok 6->11, jira 44->45) and 1698
rows. Verified independently: zero sections lost across all 289 generated files,
no file lost rows, output deterministic across regeneration.
The 96 deletions are all corrections, not losses. 70 are confluence fields the
parser flattened out of `comment: { ...buildContentEntityFields(), parent: {…} }`
and rendered as top-level trigger outputs; they reappear nested under their
parent in the same hunk. 8 are greenhouse key ordering, 6 are intercom
descriptions the parser had dropped, 1 is a vercel row moving position.
Generator: 4069 -> 3903 lines.
* chore(test): silence vite 8 deprecation warnings in the sim vitest config
@vitejs/plugin-react v4 targets pre-rolldown Vite: it sets `esbuild.jsx`
and `optimizeDeps.rollupOptions`, both deprecated under Vite 8's oxc
pipeline, and self-reports that plugin-react-oxc should be used instead.
v6 is that plugin merged back under the original name — it requires Vite
^8, drops Babel entirely, and emits none of those options.
Vite 8 also resolves tsconfig paths natively, so vite-tsconfig-paths is
replaced by `resolve.tsconfigPaths`.
Full apps/sim suite unchanged: 1483 passed / 2 skipped files,
20415 passed / 30 skipped tests.
* refactor(docs): drop 33 more comments that restated their code
Second pass over the generator, e.g. `// Copy icons from sim app to docs app`
above `copyIconsFile()`. Kept the multi-line runs (those carry reasoning), the
ones with concrete examples, and the one marking a deliberate empty catch.
Verified byte-identical output across all 289 generated files.
Generator: 3903 -> 3870 lines, 4306 at the start of this branch.
* refactor(ci): read package.json once in the audit runner
auditScripts() re-read the manifest the module body had already loaded.
* fix(pdl): name the tools directory after the tool ids
People Data Labs declared `pdl_*` tool ids under `tools/peopledatalabs/`. Every
other integration names the directory after its id prefix -- 259 of 260 before
this, and PDL was the only exception.
The docs generator locates a tool's definition by deriving the directory from the
id prefix, so it looked in `tools/pdl/`, found nothing, and returned null for all
11 tools. peopledatalabs.mdx rendered eleven bare `###` headings with no
description, no Input table and no Output table.
Renaming the directory rather than the ids: tool ids are persisted in saved
workflows, so renaming those would break existing users. The directory is
internal -- 15 files' imports.
Fixed at the source rather than teaching the generator a fallback. A special case
would have left the invariant broken and the next integration free to break it
again; now 260 of 260 hold, and the generator needs no exception.
peopledatalabs.mdx: 11 empty headings -> 456 lines. Repo-wide: zero pages with an
empty action body.
|
||
|
|
8694f5581d |
fix(chat): render HEIC attachments and restyle composer file chips (#6361)
* fix(chat): render HEIC attachments and restyle composer file chips The composer previewed every attachment through URL.createObjectURL of the raw bytes. No browser decodes HEVC-coded HEIF, so a HEIC showed a broken glyph, and the upload-completion handler never replaced that blob URL — so it stayed broken even once a derivative was available. - Skip the blob for HEIC/HEIF and pick up the serve URL (preview=1) once the upload lands, so the server derivative renders. - Fall back to the type icon if the image still fails to decode. - Documents render as labelled cards (icon, name, type) instead of a 9px extension caption; media keeps a thumbnail. - Fix a blob-URL leak: the unmount cleanup closed over the first render's empty array and revoked nothing. * fix(chat): make composer chips read against the composer shell The composer is --white in light and --surface-4 in dark. The chip reused chipFilledFillTokens (--surface-5 / dark:--surface-4), which assumes a page background, so in dark mode the chip fill matched its own container exactly and only the border showed. Same for the remove badge, which sits on the shell and was 5/255 from it. - Chip fills --surface-5 in both themes and hover steps away from the shell in each theme's 'raised' direction. - Remove badge uses --surface-6, readable on white and on --surface-4. - Cap the document card at min(220px,100%) so a long filename truncates on a narrow viewport instead of overflowing the composer. * chore(chat): use the absolute alias for the chip test import |