mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-07 22:24:06 -05:00
v0.5.45: light mode fixes, realtime usage indicator, docker build improvements
This commit is contained in:
19
.github/workflows/test-build.yml
vendored
19
.github/workflows/test-build.yml
vendored
@@ -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
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) })
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user