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

View File

@@ -391,6 +391,17 @@
color: var(--text-primary); 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 * Dark mode specific overrides
*/ */

View File

@@ -3,7 +3,15 @@
import { useMemo } from 'react' import { useMemo } from 'react'
import { createLogger } from '@sim/logger' import { createLogger } from '@sim/logger'
import { Check } from 'lucide-react' 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 { client } from '@/lib/auth/auth-client'
import { import {
getProviderIdFromServiceId, getProviderIdFromServiceId,
@@ -407,9 +415,9 @@ export function OAuthRequiredModal({
<div className='flex flex-1 items-center gap-[8px] text-[12px] text-[var(--text-primary)]'> <div className='flex flex-1 items-center gap-[8px] text-[12px] text-[var(--text-primary)]'>
<span>{getScopeDescription(scope)}</span> <span>{getScopeDescription(scope)}</span>
{newScopesSet.has(scope) && ( {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 New
</span> </Badge>
)} )}
</div> </div>
</li> </li>

View File

@@ -228,7 +228,7 @@ export function CredentialSelector({
) )
return ( return (
<> <div>
<Combobox <Combobox
options={comboboxOptions} options={comboboxOptions}
value={inputValue} value={inputValue}
@@ -247,9 +247,20 @@ export function CredentialSelector({
/> />
{needsUpdate && ( {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'> <div className='mt-[8px] flex flex-col gap-[4px] rounded-[4px] border bg-[var(--surface-2)] px-[8px] py-[6px]'>
<span>Additional permissions required</span> <div className='flex items-center font-medium text-[12px]'>
{!isForeign && <Button onClick={() => setShowOAuthModal(true)}>Update access</Button>} <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> </div>
)} )}
@@ -264,7 +275,7 @@ export function CredentialSelector({
serviceId={serviceId} serviceId={serviceId}
/> />
)} )}
</> </div>
) )
} }

View File

@@ -537,7 +537,7 @@ export function McpDynamicArgs({
} }
return ( return (
<div className='relative space-y-4'> <div className='relative'>
{/* Hidden dummy inputs to prevent browser password manager autofill */} {/* Hidden dummy inputs to prevent browser password manager autofill */}
<input <input
type='text' type='text'
@@ -563,6 +563,7 @@ export function McpDynamicArgs({
tabIndex={-1} tabIndex={-1}
readOnly readOnly
/> />
<div className='space-y-4'>
{toolSchema.properties && {toolSchema.properties &&
Object.entries(toolSchema.properties).map(([paramName, paramSchema]) => { Object.entries(toolSchema.properties).map(([paramName, paramSchema]) => {
const inputType = getInputType(paramSchema as any) const inputType = getInputType(paramSchema as any)
@@ -586,5 +587,6 @@ export function McpDynamicArgs({
) )
})} })}
</div> </div>
</div>
) )
} }

View File

@@ -200,7 +200,7 @@ export function ToolCredentialSelector({
) )
return ( return (
<> <div>
<Combobox <Combobox
options={comboboxOptions} options={comboboxOptions}
value={inputValue} value={inputValue}
@@ -217,9 +217,20 @@ export function ToolCredentialSelector({
/> />
{needsUpdate && ( {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'> <div className='mt-[8px] flex flex-col gap-[4px] rounded-[4px] border bg-[var(--surface-2)] px-[8px] py-[6px]'>
<span>Additional permissions required</span> <div className='flex items-center font-medium text-[12px]'>
{!isForeign && <Button onClick={() => setShowOAuthModal(true)}>Update access</Button>} <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> </div>
)} )}
@@ -234,7 +245,7 @@ export function ToolCredentialSelector({
serviceId={serviceId} serviceId={serviceId}
/> />
)} )}
</> </div>
) )
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2661,7 +2661,7 @@ export const useCopilotStore = create<CopilotStore>()(
// Invalidate subscription queries to update usage // Invalidate subscription queries to update usage
setTimeout(() => { setTimeout(() => {
const queryClient = getQueryClient() const queryClient = getQueryClient()
queryClient.invalidateQueries({ queryKey: subscriptionKeys.user() }) queryClient.invalidateQueries({ queryKey: subscriptionKeys.all })
}, 1000) }, 1000)
} finally { } finally {
clearTimeout(timeoutId) 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 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 # Dependencies Stage: Install Dependencies
# ======================================== # ========================================
FROM base AS deps FROM base AS deps
WORKDIR /app 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 ./ COPY package.json bun.lock turbo.json ./
RUN mkdir -p apps packages/db packages/testing packages/logger RUN mkdir -p apps packages/db packages/testing packages/logger
COPY apps/sim/package.json ./apps/sim/package.json 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 # Install turbo globally, then dependencies, then rebuild isolated-vm for Node.js
RUN --mount=type=cache,id=bun-cache,target=/root/.bun/install/cache \ 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 && \ bun install -g turbo && \
HUSKY=0 bun install --omit=dev --ignore-scripts && \ HUSKY=0 bun install --omit=dev --ignore-scripts && \
cd $(readlink -f node_modules/isolated-vm) && npx node-gyp rebuild --release && cd /app 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 FROM base AS runner
WORKDIR /app WORKDIR /app
# Install Node.js 22 (for isolated-vm worker), Python, and other runtime dependencies # Node.js 22, Python, ffmpeg, etc. are already installed in base stage
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/*
ENV NODE_ENV=production ENV NODE_ENV=production
# Create non-root user and group # 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 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 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) # Guardrails setup with pip caching
COPY --from=builder --chown=nextjs:nodejs /app/apps/sim/lib/guardrails/setup.sh ./apps/sim/lib/guardrails/setup.sh
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/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 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 # Install Python dependencies with pip cache mount for faster rebuilds
RUN chmod +x ./apps/sim/lib/guardrails/setup.sh && \ RUN --mount=type=cache,target=/root/.cache/pip \
cd ./apps/sim/lib/guardrails && \ python3 -m venv ./apps/sim/lib/guardrails/venv && \
./setup.sh && \ ./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 chown -R nextjs:nodejs /app/apps/sim/lib/guardrails
# Create .next/cache directory with correct ownership # 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 db package (needed by socket)
COPY --from=builder --chown=nextjs:nodejs /app/packages/db ./packages/db 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 sim app (changes most frequently - placed last)
COPY --from=builder --chown=nextjs:nodejs /app/apps/sim ./apps/sim COPY --from=builder --chown=nextjs:nodejs /app/apps/sim ./apps/sim