Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
0249516
fix(compare): swap LangChain logo, fix comparison-table horizontal sc…
waleedlatif1 Jul 4, 2026
818fa00
fix(mothership): stop chat perf decay from permanently-animated strea…
TheodoreSpeaks Jul 4, 2026
82eff54
feat(custom-block): deploy a workflow as a reusable org-scoped block …
TheodoreSpeaks Jul 4, 2026
acf4afb
fix(mothership): block subflow re-parenting in embedded workflow view…
TheodoreSpeaks Jul 4, 2026
04432b6
fix(copilot): validate credential-link URL scheme before rendering (#…
waleedlatif1 Jul 4, 2026
b98c7c4
fix(tools): bound download response size for drive/slack/onedrive (#5…
waleedlatif1 Jul 4, 2026
1a371e5
fix(uploads): bound multipart body read in workspace-file and knowled…
waleedlatif1 Jul 4, 2026
a1fbb57
fix(stt): bound audio download response size (#5412)
waleedlatif1 Jul 4, 2026
41fb863
fix(webhooks): validate and pin EmailBison apiBaseUrl before outbound…
waleedlatif1 Jul 4, 2026
0d6994d
refactor(uploads): hoist execution permission check out of upload loo…
waleedlatif1 Jul 4, 2026
4b13398
fix(mcp): make SSRF-guarded fetch structurally mandatory in OAuth aut…
waleedlatif1 Jul 4, 2026
f456ed9
improvement(chat): smooth streaming — eased stick-to-bottom glide and…
TheodoreSpeaks Jul 4, 2026
3edaae3
fix(mcp): correct fetchFn fallback order in mcpAuthGuarded (#5423)
waleedlatif1 Jul 4, 2026
ee1824c
improvement(compare): accuracy and consistency pass on comparison pag…
waleedlatif1 Jul 4, 2026
82eb6dc
fix(404): use ChipLink for return-home CTA on root not-found page (#5…
waleedlatif1 Jul 5, 2026
85f80fa
fix(compare): render two already-populated fact rows that were missin…
waleedlatif1 Jul 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { createLogger } from '@sim/logger'
import { sleep } from '@sim/utils/helpers'
import { Music } from 'lucide-react'
import { DefaultFileIcon, getDocumentIcon } from '@/components/icons/document-icons'
import { getBrowserOrigin } from '@/lib/core/utils/urls'
import { isSafeHttpUrl } from '@/lib/core/utils/urls'
import type { ChatFile } from '@/app/(interfaces)/chat/components/message/message'

const logger = createLogger('ChatFileDownload')
Expand Down Expand Up @@ -54,21 +54,6 @@ function getFileUrl(file: ChatFile): string {
return `/api/files/serve/${encodeURIComponent(file.key)}?context=${file.context || 'execution'}`
}

/**
* Validates that a URL uses an http(s) scheme before it is opened in a new window.
* Rejects `javascript:`, `data:`, `blob:`, `vbscript:`, and other schemes that could
* execute script in the chat origin, since `file.url` originates from untrusted
* workflow/agent output.
*/
export function isSafeHttpUrl(url: string): boolean {
try {
const parsed = new URL(url, getBrowserOrigin() ?? undefined)
return parsed.protocol === 'http:' || parsed.protocol === 'https:'
} catch {
return false
}
}

async function triggerDownload(url: string, filename: string): Promise<void> {
const response = await fetch(url)
if (!response.ok) {
Expand Down
3 changes: 3 additions & 0 deletions apps/sim/app/(landing)/comparison/comparison-sections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ export const COMPARISON_SECTIONS: ComparisonSectionDef[] = [
{ key: 'versionControlDepth', label: 'Version control' },
{ key: 'realtimeCollaboration', label: 'Realtime collaboration' },
{ key: 'nativeFileStorage', label: 'Native file storage' },
{ key: 'dataTables', label: 'Native data tables' },
{ key: 'richTextEditor', label: 'Rich-text document editor' },
{ key: 'subWorkflows', label: 'Sub-workflows (composition)' },
],
}),
Expand Down Expand Up @@ -142,6 +144,7 @@ export const COMPARISON_SECTIONS: ComparisonSectionDef[] = [
{ key: 'asyncExecution', label: 'Async execution' },
{ key: 'executionLimits', label: 'Execution limits' },
{ key: 'partialFailureHandling', label: 'Partial-failure handling' },
{ key: 'unattendedExecution', label: 'Unattended execution' },
],
}),
defineSection({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,39 @@ export interface ComparisonTableProps {
competitor: CompetitorProfile
}

/**
* Pins the row-label column during horizontal scroll on genuinely spacious
* viewports (the standard pattern for responsive data tables, e.g.
* Stripe/GitHub/Notion comparison tables) so a reader keeps row context while
* scrolling to see the Sim/competitor values. Below `lg` (this page's own
* tablet-and-below tier, per `.claude/rules` for this route group) the table
* switches to a stacked layout instead (see `MOBILE_STACK_*`) rather than
* relying on horizontal scroll at a width too narrow to render a 3-column
* table comfortably, so sticky positioning is scoped to `lg:` only. The
* shadow is a permanent CSS-only affordance (no scroll-position JS) so this
* stays a zero-hydration server component.
*/
const STICKY_LABEL_COL = 'lg:sticky lg:left-0 lg:z-10 lg:shadow-[2px_0_4px_-2px_rgba(0,0,0,0.08)]'

/**
* Below `lg` (1024px) a 3-column grid doesn't reliably have room to be
* legible, so each fact instead stacks as label -> Sim's value -> the
* competitor's value, with a small name tag on each value since the column
* headers are no longer directly above. Applied to the label cell.
*/
const MOBILE_STACK_LABEL = 'max-lg:border-r-0 max-lg:border-b-0 max-lg:pt-3 max-lg:pb-1'

/**
* Applied to a value cell (Sim's or the competitor's) in the stacked mobile
* layout. `items-stretch` overrides the cell's base `items-center` (which
* would otherwise shrink-wrap and center each child horizontally in a
* flex-col): stretching gives the name tag and the value their own
* full-width box to left-align and truncate within, instead of a
* content-sized box with no boundary to clip against.
*/
const MOBILE_STACK_VALUE =
'max-lg:flex-col max-lg:items-stretch max-lg:gap-0.5 max-lg:border-r-0 max-lg:px-4'

function ColumnHeader({
name,
iconTile,
Expand All @@ -22,12 +55,14 @@ function ColumnHeader({
return (
<div
className={cn(
'flex flex-col items-center gap-2 border-[var(--border-1)] border-b px-3 py-4 text-center',
'flex min-w-0 flex-col items-center gap-2 border-[var(--border-1)] border-b px-3 py-4 text-center',
isSim ? 'bg-[var(--surface-2)]' : 'bg-[var(--surface-1)]'
)}
>
{iconTile}
<span className='font-medium text-[var(--text-primary)] text-base'>{name}</span>
<span className='w-full truncate font-medium text-[var(--text-primary)] text-base'>
{name}
</span>
</div>
)
}
Expand All @@ -49,15 +84,21 @@ export function ComparisonTable({ sim, competitor }: ComparisonTableProps) {
<div
role='table'
aria-label={`Sim vs ${competitor.name} feature comparison`}
className='grid min-w-[560px] grid-cols-[1.2fr_1fr_1fr]'
className='grid grid-cols-1 lg:min-w-[560px] lg:grid-cols-[minmax(140px,max-content)_1fr_1fr]'
>
<div className='contents' role='row'>
<div
role='columnheader'
className='flex flex-col justify-center border-[var(--border)] border-r border-b bg-[var(--surface-1)] px-4 py-4'
className={cn(
'flex min-w-0 flex-col justify-center border-[var(--border)] border-r border-b bg-[var(--surface-1)] px-4 py-4',
STICKY_LABEL_COL,
'max-lg:border-r-0'
)}
>
<span className='font-medium text-[var(--text-primary)] text-base'>Compare</span>
<span className='text-[var(--text-muted)] text-small'>
<span className='truncate font-medium text-[var(--text-primary)] text-base'>
Compare
</span>
<span className='truncate text-[var(--text-muted)] text-small'>
{sim.name} vs {competitor.name}
</span>
</div>
Expand Down Expand Up @@ -89,6 +130,8 @@ export function ComparisonTable({ sim, competitor }: ComparisonTableProps) {
role='columnheader'
className={cn(
'border-[var(--border)] border-r bg-[var(--surface-1)] px-4 py-2',
STICKY_LABEL_COL,
'max-lg:border-r-0',
sectionIdx > 0 && 'border-[var(--border-1)] border-t'
)}
>
Expand All @@ -99,7 +142,7 @@ export function ComparisonTable({ sim, competitor }: ComparisonTableProps) {
<div
role='presentation'
className={cn(
'col-span-2 bg-[var(--surface-1)]',
'col-span-2 bg-[var(--surface-1)] max-lg:hidden',
sectionIdx > 0 && 'border-[var(--border-1)] border-t'
)}
/>
Expand All @@ -115,28 +158,42 @@ export function ComparisonTable({ sim, competitor }: ComparisonTableProps) {
<div
role='rowheader'
className={cn(
'flex items-center border-[var(--border)] border-r bg-[var(--surface-1)] px-4 py-2.5',
'flex min-w-0 items-center border-[var(--border)] border-r bg-[var(--surface-1)] px-4 py-2.5',
STICKY_LABEL_COL,
MOBILE_STACK_LABEL,
isNotLastRow && 'border-[var(--border-1)] border-b'
)}
>
<span className='text-[var(--text-body)] text-small'>{row.label}</span>
<span className='text-[var(--text-body)] text-small max-lg:font-medium max-lg:text-[var(--text-primary)]'>
{row.label}
</span>
</div>
<div
role='cell'
className={cn(
'flex items-center border-[var(--border)] border-r bg-[var(--surface-2)] px-3 py-2.5',
'flex min-w-0 items-center gap-1 border-[var(--border)] border-r bg-[var(--surface-2)] px-3 py-2.5',
MOBILE_STACK_VALUE,
'max-lg:border-b-0 max-lg:pt-1 max-lg:pb-1',
isNotLastRow && 'border-[var(--border-1)] border-b'
)}
>
<span className='font-medium text-[var(--text-muted)] text-caption lg:hidden'>
{sim.name}
</span>
<FactValue fact={simFact} />
</div>
<div
role='cell'
className={cn(
'flex items-center bg-[var(--surface-2)] px-3 py-2.5',
'flex min-w-0 items-center gap-1 bg-[var(--surface-2)] px-3 py-2.5',
MOBILE_STACK_VALUE,
'max-lg:pt-1 max-lg:pb-3',
isNotLastRow && 'border-[var(--border-1)] border-b'
)}
>
<span className='font-medium text-[var(--text-muted)] text-caption lg:hidden'>
{competitor.name}
</span>
<FactValue fact={competitorFact} />
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use client'

import type { ReactNode } from 'react'
import { Tooltip } from '@sim/emcn'
import { cn, Tooltip } from '@sim/emcn'
import type { FactSource } from '@/lib/compare/data'

export interface SourceLinkProps {
Expand Down Expand Up @@ -29,7 +29,7 @@ export function SourceLink({ source, children, className }: SourceLinkProps) {
target='_blank'
rel='noopener noreferrer'
aria-label={`${source.label} (opens source)`}
className={className}
className={cn('block min-w-0', className)}
>
{children}
</a>
Expand Down
12 changes: 9 additions & 3 deletions apps/sim/app/_styles/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@ html.sidebar-booting .sidebar-shell-inner {
--brand-accent: #33c482;
--brand-accent-hover: #2dac72;
--selection: #1a5cf6;
--selection-muted: #1a5cf647;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 This edit modifies globals.css, which the team style rule reserves for cases where a local component file genuinely cannot carry the change. ::selection is a global pseudo-element that cannot be scoped to a single component, so the change is functionally justified — but the new --selection-muted variable is only consumed by ::selection, so it could be inlined as a literal there instead of adding two new global custom properties.

Suggested change
--selection-muted: #1a5cf647;
/* --selection-muted: #1a5cf647; — inline in ::selection instead of adding a global variable */

Rule Used: Avoid editing the globals.css file unless absolute... (source)

Learned From
simstudioai/sim#367

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

--warning: #ea580c;

/* Inverted surface (dark chrome on light page, e.g. tag dropdown) */
Expand Down Expand Up @@ -382,6 +383,7 @@ html.sidebar-booting .sidebar-shell-inner {
--brand-accent: #33c482;
--brand-accent-hover: #2dac72;
--selection: #4b83f7;
--selection-muted: #4b83f759;
--warning: #ff6600;

/* Inverted surface (in dark mode, falls back to standard surfaces) */
Expand Down Expand Up @@ -492,10 +494,14 @@ html.sidebar-booting .sidebar-shell-inner {
}

/* Consistent branded text-selection color everywhere, independent of the
browser/OS default (which dims when the window loses focus). */
browser/OS default (which dims when the window loses focus). Translucent,
with no color override, so selected text keeps its own paint on every
surface — solid-brand + forced white broke wherever glyphs aren't the
selected element's own (Chromium ignores ::selection color in form
controls, and the chat/workflow inputs render transparent textareas over
styled mirrors, which left black text on solid brand blue). */
::selection {
background-color: var(--selection);
color: var(--white);
background-color: var(--selection-muted);
}

body {
Expand Down
Loading
Loading