v0.5.45: light mode fixes, realtime usage indicator, docker build improvements

This commit is contained in:
Waleed
2025-12-27 19:57:42 -08:00
committed by GitHub
16 changed files with 134 additions and 83 deletions

View File

@@ -23,16 +23,17 @@ jobs:
with:
node-version: latest
- name: Cache Bun dependencies
uses: actions/cache@v4
- name: Mount Bun cache (Sticky Disk)
uses: useblacksmith/stickydisk@v1
with:
path: |
~/.bun/install/cache
node_modules
**/node_modules
key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }}
restore-keys: |
${{ runner.os }}-bun-
key: ${{ github.repository }}-bun-cache
path: ~/.bun/install/cache
- name: Mount node_modules (Sticky Disk)
uses: useblacksmith/stickydisk@v1
with:
key: ${{ github.repository }}-node-modules
path: ./node_modules
- name: Install dependencies
run: bun install --frozen-lockfile

View File

@@ -391,6 +391,17 @@
color: var(--text-primary);
}
/**
* Subblock divider visibility
* Hides dividers when adjacent subblocks render empty content (e.g., schedule-info without data).
* Uses CSS :has() to detect empty .subblock-content elements and hide associated dividers.
* Selectors ordered by ascending specificity: (0,4,0) then (0,5,0)
*/
.subblock-row:has(> .subblock-content:empty) > .subblock-divider,
.subblock-row:has(+ .subblock-row > .subblock-content:empty) > .subblock-divider {
display: none;
}
/**
* Dark mode specific overrides
*/

View File

@@ -3,7 +3,15 @@
import { useMemo } from 'react'
import { createLogger } from '@sim/logger'
import { Check } from 'lucide-react'
import { Button, Modal, ModalBody, ModalContent, ModalFooter, ModalHeader } from '@/components/emcn'
import {
Badge,
Button,
Modal,
ModalBody,
ModalContent,
ModalFooter,
ModalHeader,
} from '@/components/emcn'
import { client } from '@/lib/auth/auth-client'
import {
getProviderIdFromServiceId,
@@ -407,9 +415,9 @@ export function OAuthRequiredModal({
<div className='flex flex-1 items-center gap-[8px] text-[12px] text-[var(--text-primary)]'>
<span>{getScopeDescription(scope)}</span>
{newScopesSet.has(scope) && (
<span className='inline-flex items-center gap-[6px] rounded-[6px] bg-[#fde68a] px-[7px] py-[1px] font-medium text-[#a16207] text-[11px] dark:bg-[rgba(245,158,11,0.2)] dark:text-[#fcd34d]'>
<Badge variant='amber' size='sm'>
New
</span>
</Badge>
)}
</div>
</li>

View File

@@ -228,7 +228,7 @@ export function CredentialSelector({
)
return (
<>
<div>
<Combobox
options={comboboxOptions}
value={inputValue}
@@ -247,9 +247,20 @@ export function CredentialSelector({
/>
{needsUpdate && (
<div className='mt-2 flex items-center justify-between rounded-[6px] border border-amber-300/40 bg-amber-50/60 px-2 py-1 font-medium text-[12px] transition-colors dark:bg-amber-950/10'>
<span>Additional permissions required</span>
{!isForeign && <Button onClick={() => setShowOAuthModal(true)}>Update access</Button>}
<div className='mt-[8px] flex flex-col gap-[4px] rounded-[4px] border bg-[var(--surface-2)] px-[8px] py-[6px]'>
<div className='flex items-center font-medium text-[12px]'>
<span className='mr-[6px] inline-block h-[6px] w-[6px] rounded-[2px] bg-amber-500' />
Additional permissions required
</div>
{!isForeign && (
<Button
variant='active'
onClick={() => setShowOAuthModal(true)}
className='w-full px-[8px] py-[4px] font-medium text-[12px]'
>
Update access
</Button>
)}
</div>
)}
@@ -264,7 +275,7 @@ export function CredentialSelector({
serviceId={serviceId}
/>
)}
</>
</div>
)
}

View File

@@ -537,7 +537,7 @@ export function McpDynamicArgs({
}
return (
<div className='relative space-y-4'>
<div className='relative'>
{/* Hidden dummy inputs to prevent browser password manager autofill */}
<input
type='text'
@@ -563,28 +563,30 @@ export function McpDynamicArgs({
tabIndex={-1}
readOnly
/>
{toolSchema.properties &&
Object.entries(toolSchema.properties).map(([paramName, paramSchema]) => {
const inputType = getInputType(paramSchema as any)
const showLabel = inputType !== 'switch'
<div className='space-y-4'>
{toolSchema.properties &&
Object.entries(toolSchema.properties).map(([paramName, paramSchema]) => {
const inputType = getInputType(paramSchema as any)
const showLabel = inputType !== 'switch'
return (
<div key={paramName} className='space-y-2'>
{showLabel && (
<Label
className={cn(
'font-medium text-sm',
toolSchema.required?.includes(paramName) &&
'after:ml-1 after:text-red-500 after:content-["*"]'
)}
>
{formatParameterLabel(paramName)}
</Label>
)}
{renderParameterInput(paramName, paramSchema as any)}
</div>
)
})}
return (
<div key={paramName} className='space-y-2'>
{showLabel && (
<Label
className={cn(
'font-medium text-sm',
toolSchema.required?.includes(paramName) &&
'after:ml-1 after:text-red-500 after:content-["*"]'
)}
>
{formatParameterLabel(paramName)}
</Label>
)}
{renderParameterInput(paramName, paramSchema as any)}
</div>
)
})}
</div>
</div>
)
}

View File

@@ -200,7 +200,7 @@ export function ToolCredentialSelector({
)
return (
<>
<div>
<Combobox
options={comboboxOptions}
value={inputValue}
@@ -217,9 +217,20 @@ export function ToolCredentialSelector({
/>
{needsUpdate && (
<div className='mt-2 flex items-center justify-between rounded-[6px] border border-amber-300/40 bg-amber-50/60 px-2 py-1 font-medium text-[12px] transition-colors dark:bg-amber-950/10'>
<span>Additional permissions required</span>
{!isForeign && <Button onClick={() => setShowOAuthModal(true)}>Update access</Button>}
<div className='mt-[8px] flex flex-col gap-[4px] rounded-[4px] border bg-[var(--surface-2)] px-[8px] py-[6px]'>
<div className='flex items-center font-medium text-[12px]'>
<span className='mr-[6px] inline-block h-[6px] w-[6px] rounded-[2px] bg-amber-500' />
Additional permissions required
</div>
{!isForeign && (
<Button
variant='active'
onClick={() => setShowOAuthModal(true)}
className='w-full px-[8px] py-[4px] font-medium text-[12px]'
>
Update access
</Button>
)}
</div>
)}
@@ -234,7 +245,7 @@ export function ToolCredentialSelector({
serviceId={serviceId}
/>
)}
</>
</div>
)
}

View File

@@ -866,7 +866,7 @@ function SubBlockComponent({
}
return (
<div onMouseDown={handleMouseDown} className='flex flex-col gap-[10px]'>
<div onMouseDown={handleMouseDown} className='subblock-content flex flex-col gap-[10px]'>
{renderLabel(
config,
isValidJson,

View File

@@ -341,7 +341,7 @@ export function Editor() {
)
return (
<div key={stableKey}>
<div key={stableKey} className='subblock-row'>
<SubBlock
blockId={currentBlockId}
config={subBlock}
@@ -352,7 +352,7 @@ export function Editor() {
allowExpandInPreview={false}
/>
{index < subBlocks.length - 1 && (
<div className='px-[2px] pt-[16px] pb-[13px]'>
<div className='subblock-divider px-[2px] pt-[16px] pb-[13px]'>
<div
className='h-[1.25px]'
style={{

View File

@@ -252,7 +252,7 @@ export function useWand({
})
setTimeout(() => {
queryClient.invalidateQueries({ queryKey: subscriptionKeys.user() })
queryClient.invalidateQueries({ queryKey: subscriptionKeys.all })
}, 1000)
} catch (error: any) {
if (error.name === 'AbortError') {

View File

@@ -573,7 +573,7 @@ export function useWorkflowExecution() {
// Invalidate subscription queries to update usage
setTimeout(() => {
queryClient.invalidateQueries({ queryKey: subscriptionKeys.user() })
queryClient.invalidateQueries({ queryKey: subscriptionKeys.all })
}, 1000)
safeEnqueue(encodeSSE({ event: 'final', data: result }))
@@ -646,7 +646,7 @@ export function useWorkflowExecution() {
// Invalidate subscription queries to update usage
setTimeout(() => {
queryClient.invalidateQueries({ queryKey: subscriptionKeys.user() })
queryClient.invalidateQueries({ queryKey: subscriptionKeys.all })
}, 1000)
}
return result

View File

@@ -165,7 +165,7 @@ export function CancelSubscription({ subscription, subscriptionData }: CancelSub
logger.info('Subscription restored successfully', result)
}
await queryClient.invalidateQueries({ queryKey: subscriptionKeys.user() })
await queryClient.invalidateQueries({ queryKey: subscriptionKeys.all })
if (activeOrgId) {
await queryClient.invalidateQueries({ queryKey: organizationKeys.detail(activeOrgId) })
await queryClient.invalidateQueries({ queryKey: organizationKeys.billing(activeOrgId) })

View File

@@ -199,7 +199,7 @@ export function UsageIndicator({ onClick }: UsageIndicatorProps) {
useEffect(() => {
const handleOperationConfirmed = () => {
setTimeout(() => {
queryClient.invalidateQueries({ queryKey: subscriptionKeys.user() })
queryClient.invalidateQueries({ queryKey: subscriptionKeys.all })
}, 1000)
}
onOperationConfirmed(handleOperationConfirmed)

View File

@@ -98,13 +98,13 @@ export function useUpdateUsageLimit() {
return response.json()
},
onMutate: async ({ limit }) => {
await queryClient.cancelQueries({ queryKey: subscriptionKeys.user() })
await queryClient.cancelQueries({ queryKey: subscriptionKeys.usage() })
await queryClient.cancelQueries({ queryKey: subscriptionKeys.all })
const previousSubscriptionData = queryClient.getQueryData(subscriptionKeys.user())
const previousSubscriptionData = queryClient.getQueryData(subscriptionKeys.user(false))
const previousSubscriptionDataWithOrg = queryClient.getQueryData(subscriptionKeys.user(true))
const previousUsageData = queryClient.getQueryData(subscriptionKeys.usage())
queryClient.setQueryData(subscriptionKeys.user(), (old: any) => {
const updateSubscriptionData = (old: any) => {
if (!old) return old
const currentUsage = old.data?.usage?.current || 0
const newPercentUsed = limit > 0 ? (currentUsage / limit) * 100 : 0
@@ -120,7 +120,10 @@ export function useUpdateUsageLimit() {
},
},
}
})
}
queryClient.setQueryData(subscriptionKeys.user(false), updateSubscriptionData)
queryClient.setQueryData(subscriptionKeys.user(true), updateSubscriptionData)
queryClient.setQueryData(subscriptionKeys.usage(), (old: any) => {
if (!old) return old
@@ -133,19 +136,24 @@ export function useUpdateUsageLimit() {
}
})
return { previousSubscriptionData, previousUsageData }
return { previousSubscriptionData, previousSubscriptionDataWithOrg, previousUsageData }
},
onError: (_err, _variables, context) => {
if (context?.previousSubscriptionData) {
queryClient.setQueryData(subscriptionKeys.user(), context.previousSubscriptionData)
queryClient.setQueryData(subscriptionKeys.user(false), context.previousSubscriptionData)
}
if (context?.previousSubscriptionDataWithOrg) {
queryClient.setQueryData(
subscriptionKeys.user(true),
context.previousSubscriptionDataWithOrg
)
}
if (context?.previousUsageData) {
queryClient.setQueryData(subscriptionKeys.usage(), context.previousUsageData)
}
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: subscriptionKeys.user() })
queryClient.invalidateQueries({ queryKey: subscriptionKeys.usage() })
queryClient.invalidateQueries({ queryKey: subscriptionKeys.all })
},
})
}

View File

@@ -2661,7 +2661,7 @@ export const useCopilotStore = create<CopilotStore>()(
// Invalidate subscription queries to update usage
setTimeout(() => {
const queryClient = getQueryClient()
queryClient.invalidateQueries({ queryKey: subscriptionKeys.user() })
queryClient.invalidateQueries({ queryKey: subscriptionKeys.all })
}, 1000)
} finally {
clearTimeout(timeoutId)

View File

@@ -1,21 +1,22 @@
# ========================================
# Base Stage: Debian-based Bun
# Base Stage: Debian-based Bun with Node.js 22
# ========================================
FROM oven/bun:1.3.3-slim AS base
# Install Node.js 22 and common dependencies once in base stage
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,target=/var/lib/apt,sharing=locked \
apt-get update && apt-get install -y --no-install-recommends \
python3 python3-pip python3-venv make g++ curl ca-certificates bash ffmpeg \
&& curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \
&& apt-get install -y nodejs
# ========================================
# Dependencies Stage: Install Dependencies
# ========================================
FROM base AS deps
WORKDIR /app
# Install Node.js 22 for isolated-vm compilation (requires node-gyp and V8)
RUN apt-get update && apt-get install -y --no-install-recommends \
python3 make g++ curl ca-certificates \
&& curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \
&& apt-get install -y nodejs \
&& rm -rf /var/lib/apt/lists/*
COPY package.json bun.lock turbo.json ./
RUN mkdir -p apps packages/db packages/testing packages/logger
COPY apps/sim/package.json ./apps/sim/package.json
@@ -25,6 +26,7 @@ COPY packages/logger/package.json ./packages/logger/package.json
# Install turbo globally, then dependencies, then rebuild isolated-vm for Node.js
RUN --mount=type=cache,id=bun-cache,target=/root/.bun/install/cache \
--mount=type=cache,id=npm-cache,target=/root/.npm \
bun install -g turbo && \
HUSKY=0 bun install --omit=dev --ignore-scripts && \
cd $(readlink -f node_modules/isolated-vm) && npx node-gyp rebuild --release && cd /app
@@ -89,13 +91,7 @@ RUN bun run build
FROM base AS runner
WORKDIR /app
# Install Node.js 22 (for isolated-vm worker), Python, and other runtime dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
python3 python3-pip python3-venv bash ffmpeg curl ca-certificates \
&& curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \
&& apt-get install -y nodejs \
&& rm -rf /var/lib/apt/lists/*
# Node.js 22, Python, ffmpeg, etc. are already installed in base stage
ENV NODE_ENV=production
# Create non-root user and group
@@ -113,15 +109,15 @@ COPY --from=deps --chown=nextjs:nodejs /app/node_modules/isolated-vm ./node_modu
# Copy the isolated-vm worker script
COPY --from=builder --chown=nextjs:nodejs /app/apps/sim/lib/execution/isolated-vm-worker.cjs ./apps/sim/lib/execution/isolated-vm-worker.cjs
# Guardrails setup (files need to be owned by nextjs for runtime)
COPY --from=builder --chown=nextjs:nodejs /app/apps/sim/lib/guardrails/setup.sh ./apps/sim/lib/guardrails/setup.sh
# Guardrails setup with pip caching
COPY --from=builder --chown=nextjs:nodejs /app/apps/sim/lib/guardrails/requirements.txt ./apps/sim/lib/guardrails/requirements.txt
COPY --from=builder --chown=nextjs:nodejs /app/apps/sim/lib/guardrails/validate_pii.py ./apps/sim/lib/guardrails/validate_pii.py
# Run guardrails setup as root, then fix ownership of generated venv files
RUN chmod +x ./apps/sim/lib/guardrails/setup.sh && \
cd ./apps/sim/lib/guardrails && \
./setup.sh && \
# Install Python dependencies with pip cache mount for faster rebuilds
RUN --mount=type=cache,target=/root/.cache/pip \
python3 -m venv ./apps/sim/lib/guardrails/venv && \
./apps/sim/lib/guardrails/venv/bin/pip install --upgrade pip && \
./apps/sim/lib/guardrails/venv/bin/pip install -r ./apps/sim/lib/guardrails/requirements.txt && \
chown -R nextjs:nodejs /app/apps/sim/lib/guardrails
# Create .next/cache directory with correct ownership

View File

@@ -62,6 +62,9 @@ COPY --from=builder --chown=nextjs:nodejs /app/node_modules ./node_modules
# Copy db package (needed by socket)
COPY --from=builder --chown=nextjs:nodejs /app/packages/db ./packages/db
# Copy logger package (workspace dependency used by socket)
COPY --from=builder --chown=nextjs:nodejs /app/packages/logger ./packages/logger
# Copy sim app (changes most frequently - placed last)
COPY --from=builder --chown=nextjs:nodejs /app/apps/sim ./apps/sim