From 9dc14f0f5276343aec92b8cdee170fae31cff80c Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 1 Oct 2026 17:55:19 -0700 Subject: [PATCH 01/15] chore(lint): ban console in runtime code with Biome noConsole Runtime code logs through createLogger from @sim/logger. Scripts, CLIs, script-migrations, the logger itself, SDK examples, and tests keep console as their interface. Autofix is disabled so lint --unsafe never silently deletes a console call. --- apps/docs/app/api/search/route.ts | 5 ++++- apps/docs/app/llms-full.txt/route.ts | 5 ++++- apps/docs/app/llms.txt/route.ts | 5 ++++- apps/docs/package.json | 1 + .../sim/app/_shell/hydration-error-handler.tsx | 1 + apps/sim/instrumentation-node.ts | 3 +-- biome.json | 18 +++++++++++++++++- bun.lock | 1 + packages/testing/src/environment/index.ts | 1 + 9 files changed, 34 insertions(+), 6 deletions(-) diff --git a/apps/docs/app/api/search/route.ts b/apps/docs/app/api/search/route.ts index 4bea9ead620..30915b87529 100644 --- a/apps/docs/app/api/search/route.ts +++ b/apps/docs/app/api/search/route.ts @@ -1,8 +1,11 @@ +import { createLogger } from '@sim/logger' import { sql } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { db, docsEmbeddings } from '@/lib/db' import { generateSearchEmbedding } from '@/lib/embeddings' +const logger = createLogger('DocsSearchAPI') + export const runtime = 'nodejs' export const revalidate = 0 @@ -196,7 +199,7 @@ export async function GET(request: NextRequest) { return NextResponse.json(searchResults) } catch (error) { - console.error('Semantic search error:', error) + logger.error('Semantic search error:', error) return NextResponse.json([]) } diff --git a/apps/docs/app/llms-full.txt/route.ts b/apps/docs/app/llms-full.txt/route.ts index 57bcd9045c5..244c7a62dc8 100644 --- a/apps/docs/app/llms-full.txt/route.ts +++ b/apps/docs/app/llms-full.txt/route.ts @@ -1,6 +1,9 @@ +import { createLogger } from '@sim/logger' import { getLLMText } from '@/lib/llms' import { source } from '@/lib/source' +const logger = createLogger('DocsLlmsFullText') + export const revalidate = false export async function GET() { @@ -18,7 +21,7 @@ export async function GET() { }, }) } catch (error) { - console.error('Error generating LLM full text:', error) + logger.error('Error generating LLM full text:', error) return new Response('Error generating full documentation text', { status: 500 }) } } diff --git a/apps/docs/app/llms.txt/route.ts b/apps/docs/app/llms.txt/route.ts index 0706a38595a..f43aeba1876 100644 --- a/apps/docs/app/llms.txt/route.ts +++ b/apps/docs/app/llms.txt/route.ts @@ -1,6 +1,9 @@ +import { createLogger } from '@sim/logger' import { source } from '@/lib/source' import { DOCS_BASE_URL } from '@/lib/urls' +const logger = createLogger('DocsLlmsManifest') + export const revalidate = false export async function GET() { @@ -70,7 +73,7 @@ See: https://llmstxt.org for specification` }, }) } catch (error) { - console.error('Error generating LLM manifest:', error) + logger.error('Error generating LLM manifest:', error) return new Response('Error generating documentation manifest', { status: 500 }) } } diff --git a/apps/docs/package.json b/apps/docs/package.json index ac0e88dcf59..fd4a4d1816a 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -20,6 +20,7 @@ "dependencies": { "@sim/db": "workspace:*", "@sim/emcn": "workspace:*", + "@sim/logger": "workspace:*", "@sim/utils": "workspace:*", "@sim/workflow-renderer": "workspace:*", "@xyflow/react": "12.11.3", diff --git a/apps/sim/app/_shell/hydration-error-handler.tsx b/apps/sim/app/_shell/hydration-error-handler.tsx index 61bc828f4a2..24e0baaac09 100644 --- a/apps/sim/app/_shell/hydration-error-handler.tsx +++ b/apps/sim/app/_shell/hydration-error-handler.tsx @@ -33,6 +33,7 @@ function isRadixIdMismatch(args: unknown[]): boolean { */ export function HydrationErrorHandler() { useEffect(() => { + // biome-ignore lint/suspicious/noConsole: wraps console.error itself to filter extension hydration noise const originalError = console.error console.error = (...args) => { if (typeof args[0] === 'string' && args[0].includes('Hydration')) { diff --git a/apps/sim/instrumentation-node.ts b/apps/sim/instrumentation-node.ts index 71d6bd6f73d..4f21b2a9db6 100644 --- a/apps/sim/instrumentation-node.ts +++ b/apps/sim/instrumentation-node.ts @@ -259,8 +259,7 @@ async function initializeOpenTelemetry() { exporter.export = (spans, resultCallback) => { origExport(spans, (result) => { if (result?.code !== 0) { - // eslint-disable-next-line no-console - console.error('[OTEL] exporter export failed', { + logger.error('Exporter export failed', { endpoint: telemetryConfig.endpoint, resultCode: result?.code, error: result?.error?.message, diff --git a/biome.json b/biome.json index eea32f3b26b..46351d6689b 100644 --- a/biome.json +++ b/biome.json @@ -100,7 +100,8 @@ "noControlCharactersInRegex": "off", "noThenProperty": "off", "noAssignInExpressions": "off", - "noDocumentCookie": "off" + "noDocumentCookie": "off", + "noConsole": { "level": "error", "fix": "none" } }, "correctness": { "useExhaustiveDependencies": "off", @@ -155,6 +156,21 @@ "includes": ["apps/sim/app/_styles/tailwind.css"], "linter": { "enabled": false }, "formatter": { "enabled": false } + }, + { + "includes": [ + "**/scripts/**", + "**/*.test.ts", + "**/*.test.tsx", + "apps/sim/vitest.setup.ts", + "packages/cli/**", + "packages/sim-cli/**", + "packages/sim-setup/**", + "packages/db/script-migrations/**", + "packages/logger/src/**", + "packages/ts-sdk/examples/**" + ], + "linter": { "rules": { "suspicious": { "noConsole": "off" } } } } ], "javascript": { diff --git a/bun.lock b/bun.lock index e2719eca5a2..99d433f3586 100644 --- a/bun.lock +++ b/bun.lock @@ -89,6 +89,7 @@ "dependencies": { "@sim/db": "workspace:*", "@sim/emcn": "workspace:*", + "@sim/logger": "workspace:*", "@sim/utils": "workspace:*", "@sim/workflow-renderer": "workspace:*", "@xyflow/react": "12.11.3", diff --git a/packages/testing/src/environment/index.ts b/packages/testing/src/environment/index.ts index e09fdf556d4..c4421cf2e5b 100644 --- a/packages/testing/src/environment/index.ts +++ b/packages/testing/src/environment/index.ts @@ -31,6 +31,7 @@ function toolGuard(label: string, detect: () => { ok: boolean; hint: string }): } if (!warned) { warned = true + // biome-ignore lint/suspicious/noConsole: a skipped suite must tell the test runner's console why console.warn(`[@sim/testing] Skipping tests that require ${label}. ${result.hint}`) } return false From 1dfa35d331959a1aec80cc0423e6b4952859ae16 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 1 Oct 2026 18:03:43 -0700 Subject: [PATCH 02/15] refactor(utils): replace inline toError, isRecordLike, omit, and truncate idioms with @sim/utils helpers --- apps/desktop/src/main/local-filesystem.ts | 5 ++--- apps/sim/app/api/mcp/oauth/start/route.ts | 13 ++++++------- .../components/agent-group/tool-permission-card.tsx | 3 ++- apps/sim/blocks/blocks/google_slides.ts | 5 ++--- apps/sim/connectors/airtable/airtable.ts | 3 ++- apps/sim/executor/execution/block-executor.ts | 4 ++-- .../executor/handlers/pi/cloud/authoring/backend.ts | 7 ++----- .../executor/handlers/pi/cloud/babysit/backend.ts | 2 +- apps/sim/executor/handlers/pi/local/ssh-tools.ts | 3 ++- apps/sim/executor/variables/resolver.ts | 2 +- apps/sim/hooks/queries/tables.ts | 5 ++--- apps/sim/lib/internal/instagram/publishing.ts | 1 + .../mothership/agent-cli/engines/universal-grep.ts | 3 ++- .../providers/anthropic/structured-output-schema.ts | 3 ++- apps/sim/providers/stream-pump.ts | 5 +++-- apps/sim/stores/workflow-diff/store.ts | 9 +++------ apps/sim/tools/github/get_file_content.ts | 10 ++++++---- .../src/commands/protocol/workflow-run-get.ts | 3 ++- packages/sim-cli/src/http/client.ts | 11 ++++------- .../src/mocks/human-in-the-loop-manager.mock.ts | 3 ++- .../src/mocks/providers-trace-enrichment.mock.ts | 5 +++-- 21 files changed, 52 insertions(+), 53 deletions(-) diff --git a/apps/desktop/src/main/local-filesystem.ts b/apps/desktop/src/main/local-filesystem.ts index 46a6c30b9fb..bc3642c3ca7 100644 --- a/apps/desktop/src/main/local-filesystem.ts +++ b/apps/desktop/src/main/local-filesystem.ts @@ -19,7 +19,7 @@ import { } from '@sim/desktop-bridge/local-filesystem-limits' import { generateId } from '@sim/utils/id' import { isRecordLike } from '@sim/utils/object' -import { escapeRegExp } from '@sim/utils/string' +import { escapeRegExp, truncate } from '@sim/utils/string' import { app, dialog, shell } from 'electron' import micromatch from 'micromatch' import safeRegex from 'safe-regex2' @@ -1236,8 +1236,7 @@ export class LocalFilesystemService { matches.push({ uri: resultUri, line: request.lineNumbers === false ? 0 : contextIndex + 1, - text: - line.length > MAX_GREP_LINE_LENGTH ? `${line.slice(0, MAX_GREP_LINE_LENGTH)}…` : line, + text: truncate(line, MAX_GREP_LINE_LENGTH, '…'), }) if (matches.length >= maxResults) { truncated = true diff --git a/apps/sim/app/api/mcp/oauth/start/route.ts b/apps/sim/app/api/mcp/oauth/start/route.ts index 948185368b6..df0455632c1 100644 --- a/apps/sim/app/api/mcp/oauth/start/route.ts +++ b/apps/sim/app/api/mcp/oauth/start/route.ts @@ -3,6 +3,7 @@ import { db } from '@sim/db' import { mcpServers } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { getErrorMessage, toError } from '@sim/utils/errors' +import { truncate } from '@sim/utils/string' import { and, eq, isNull } from 'drizzle-orm' import type { NextRequest } from 'next/server' import { NextResponse } from 'next/server' @@ -61,7 +62,7 @@ function isDynamicClientRegistrationUnsupported(error: unknown): boolean { export function surfaceOauthError(error: unknown): string { // Spec-compliant OAuth servers throw typed subclasses with clean RFC 6749 fields. if (error instanceof OAuthError && !(error instanceof ServerError)) { - return truncate(`${error.errorCode}: ${error.message}`) + return truncateSurfacedError(`${error.errorCode}: ${error.message}`) } // ServerError wraps non-spec response bodies as "HTTP N: Invalid OAuth error @@ -76,18 +77,16 @@ export function surfaceOauthError(error: unknown): string { (typeof body.message === 'string' && body.message) || (typeof body.error === 'string' && body.error) || null - if (vendorMessage) return truncate(`Authorization server: ${vendorMessage}`) + if (vendorMessage) return truncateSurfacedError(`Authorization server: ${vendorMessage}`) } catch {} } - return truncate(error.message.split('\n')[0] || 'Failed to start OAuth flow') + return truncateSurfacedError(error.message.split('\n')[0] || 'Failed to start OAuth flow') } return 'Failed to start OAuth flow' } -function truncate(message: string): string { - return message.length > MAX_SURFACED_ERROR_LENGTH - ? `${message.slice(0, MAX_SURFACED_ERROR_LENGTH)}…` - : message +function truncateSurfacedError(message: string): string { + return truncate(message, MAX_SURFACED_ERROR_LENGTH, '…') } export const dynamic = 'force-dynamic' diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-permission-card.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-permission-card.tsx index 0af0622c44d..d212fb5584e 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-permission-card.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-permission-card.tsx @@ -14,6 +14,7 @@ import { toast, } from '@sim/emcn' import { createLogger } from '@sim/logger' +import { truncate } from '@sim/utils/string' import { useQueryClient } from '@tanstack/react-query' import { ACTIVITY_ICON_SLOT_CLASS, @@ -60,7 +61,7 @@ function argsPreview(params: Record | undefined): string | unde if (!params || Object.keys(params).length === 0) return undefined try { const json = JSON.stringify(params, null, 2) - return json.length > 600 ? `${json.slice(0, 600)}…` : json + return truncate(json, 600, '…') } catch { return undefined } diff --git a/apps/sim/blocks/blocks/google_slides.ts b/apps/sim/blocks/blocks/google_slides.ts index 273785c09bb..1f667b6cea5 100644 --- a/apps/sim/blocks/blocks/google_slides.ts +++ b/apps/sim/blocks/blocks/google_slides.ts @@ -1,3 +1,4 @@ +import { omit } from '@sim/utils/object' import { GoogleSlidesIcon } from '@/components/icons' import { getScopesForService } from '@/lib/oauth/utils' import { resolveHttpsUrlFromFileInput } from '@/lib/uploads/utils/file-utils' @@ -3552,9 +3553,7 @@ const googleSlidesV2SubBlocks = (GoogleSlidesBlock.subBlocks || []).flatMap((sub const googleSlidesV2Inputs = GoogleSlidesBlock.inputs ? { - ...Object.fromEntries( - Object.entries(GoogleSlidesBlock.inputs).filter(([key]) => key !== 'imageSource') - ), + ...omit(GoogleSlidesBlock.inputs, ['imageSource']), imageFile: { type: 'json', description: 'Image source (file or URL)' }, } : {} diff --git a/apps/sim/connectors/airtable/airtable.ts b/apps/sim/connectors/airtable/airtable.ts index 541dc55b8ca..5a262424680 100644 --- a/apps/sim/connectors/airtable/airtable.ts +++ b/apps/sim/connectors/airtable/airtable.ts @@ -1,5 +1,6 @@ import { createLogger } from '@sim/logger' import { getErrorMessage, toError } from '@sim/utils/errors' +import { truncate } from '@sim/utils/string' import { fetchWithRetry } from '@/lib/knowledge/documents/secure-fetch.server' import { VALIDATE_RETRY_OPTIONS } from '@/lib/knowledge/documents/utils' import { airtableConnectorMeta } from '@/connectors/airtable/meta' @@ -98,7 +99,7 @@ function renderTitle(value: unknown): string | null { if (value == null) return null const rendered = typeof value === 'object' ? formatCellValue(value).trim() : String(value).trim() if (!rendered) return null - return rendered.length > MAX_TITLE_LENGTH ? `${rendered.slice(0, MAX_TITLE_LENGTH)}…` : rendered + return truncate(rendered, MAX_TITLE_LENGTH, '…') } /** diff --git a/apps/sim/executor/execution/block-executor.ts b/apps/sim/executor/execution/block-executor.ts index c76fd5def2e..d3780253a93 100644 --- a/apps/sim/executor/execution/block-executor.ts +++ b/apps/sim/executor/execution/block-executor.ts @@ -1,5 +1,5 @@ import { createLogger, type Logger } from '@sim/logger' -import { describeError } from '@sim/utils/errors' +import { describeError, toError } from '@sim/utils/errors' import { sleep } from '@sim/utils/helpers' import { isRecordLike, toRecord } from '@sim/utils/object' import { DrizzleQueryError } from 'drizzle-orm/errors' @@ -1264,7 +1264,7 @@ export class BlockExecutor { if (onStreamPromise) { await onStreamPromise.catch(() => {}) } - throw error instanceof Error ? error : new Error(String(error)) + throw toError(error) } if (onStreamPromise) { diff --git a/apps/sim/executor/handlers/pi/cloud/authoring/backend.ts b/apps/sim/executor/handlers/pi/cloud/authoring/backend.ts index 40d19e23385..8c0e16252d5 100644 --- a/apps/sim/executor/handlers/pi/cloud/authoring/backend.ts +++ b/apps/sim/executor/handlers/pi/cloud/authoring/backend.ts @@ -322,9 +322,7 @@ function mergeChangedFiles( */ function mergePhaseDiffs(createDiff: string | undefined, babysitDiff: string | undefined): string { const merged = [createDiff, babysitDiff].filter((diff): diff is string => !!diff).join('\n') - return merged.length > MAX_DIFF_BYTES - ? `${merged.slice(0, MAX_DIFF_BYTES)}\n[diff truncated]` - : merged + return truncate(merged, MAX_DIFF_BYTES, '\n[diff truncated]') } function combineAuthoringAndBabysit( @@ -554,8 +552,7 @@ async function runCloudAuthoringPi( let diff: string | undefined try { const raw = await runner.readFile(DIFF_PATH) - diff = - raw.length > MAX_DIFF_BYTES ? `${raw.slice(0, MAX_DIFF_BYTES)}\n[diff truncated]` : raw + diff = truncate(raw, MAX_DIFF_BYTES, '\n[diff truncated]') } catch { diff = undefined } diff --git a/apps/sim/executor/handlers/pi/cloud/babysit/backend.ts b/apps/sim/executor/handlers/pi/cloud/babysit/backend.ts index 9a549e430dd..a81d52bba40 100644 --- a/apps/sim/executor/handlers/pi/cloud/babysit/backend.ts +++ b/apps/sim/executor/handlers/pi/cloud/babysit/backend.ts @@ -297,7 +297,7 @@ function isQuotedGitPath(path: string): boolean { * the net guard alone let the reported `diff` grow to megabytes across rounds. */ function capDiff(text: string): string { - return text.length > MAX_DIFF_BYTES ? `${text.slice(0, MAX_DIFF_BYTES)}\n[diff truncated]` : text + return truncate(text, MAX_DIFF_BYTES, '\n[diff truncated]') } function mergeRoundTotals(total: PiRunTotals, round: PiRunTotals): void { diff --git a/apps/sim/executor/handlers/pi/local/ssh-tools.ts b/apps/sim/executor/handlers/pi/local/ssh-tools.ts index 0a7c0f8b78a..9ce52165024 100644 --- a/apps/sim/executor/handlers/pi/local/ssh-tools.ts +++ b/apps/sim/executor/handlers/pi/local/ssh-tools.ts @@ -7,6 +7,7 @@ import { createLogger } from '@sim/logger' import { getErrorMessage } from '@sim/utils/errors' +import { truncate } from '@sim/utils/string' import type { Client, SFTPWrapper } from 'ssh2' import { createSSHConnection, @@ -129,7 +130,7 @@ export async function captureRepoChanges( } const raw = await executeSSHCommand(session.client, `${scoped} && git diff HEAD`) const out = raw.exitCode === 0 ? raw.stdout : '' - const diff = out.length > maxDiffBytes ? `${out.slice(0, maxDiffBytes)}\n[diff truncated]` : out + const diff = truncate(out, maxDiffBytes, '\n[diff truncated]') return { changedFiles: [...fileSet], diff } } catch { return { changedFiles: [], diff: '' } diff --git a/apps/sim/executor/variables/resolver.ts b/apps/sim/executor/variables/resolver.ts index 7184e4660db..4b57450d705 100644 --- a/apps/sim/executor/variables/resolver.ts +++ b/apps/sim/executor/variables/resolver.ts @@ -714,7 +714,7 @@ export class VariableResolver { ) return replacement } catch (error) { - replacementError = error instanceof Error ? error : new Error(String(error)) + replacementError = toError(error) displayResult += match return match } diff --git a/apps/sim/hooks/queries/tables.ts b/apps/sim/hooks/queries/tables.ts index 052673bc1c7..cccacc8e2e7 100644 --- a/apps/sim/hooks/queries/tables.ts +++ b/apps/sim/hooks/queries/tables.ts @@ -6,6 +6,7 @@ import { toast } from '@sim/emcn' import { createLogger } from '@sim/logger' +import { omit } from '@sim/utils/object' import { type InfiniteData, infiniteQueryOptions, @@ -2207,9 +2208,7 @@ export function useDeleteColumn({ workspaceId, tableId }: RowMutationContext) { const nextMetadata = prevWidths ? { ...previousDetail.metadata, - columnWidths: Object.fromEntries( - Object.entries(prevWidths).filter(([k]) => k !== stripKey) - ), + columnWidths: omit(prevWidths, [stripKey]), } : previousDetail.metadata queryClient.setQueryData(tableKeys.detail(tableId), { diff --git a/apps/sim/lib/internal/instagram/publishing.ts b/apps/sim/lib/internal/instagram/publishing.ts index 05f5b4c9a12..82a55a04a8b 100644 --- a/apps/sim/lib/internal/instagram/publishing.ts +++ b/apps/sim/lib/internal/instagram/publishing.ts @@ -113,6 +113,7 @@ function parseMediaFile(input: unknown): { mimeType?: string error?: { status: number; message: string } } { + // utils-lint-allow: narrows to `object` so the RawFileInput assertion compiles; isRecordLike's Record does not overlap it if (typeof input === 'object' && input !== null && !Array.isArray(input)) { const raw = input as RawFileInput if (!raw.name) { diff --git a/apps/sim/lib/mothership/agent-cli/engines/universal-grep.ts b/apps/sim/lib/mothership/agent-cli/engines/universal-grep.ts index a0038cca685..d4496a4f677 100644 --- a/apps/sim/lib/mothership/agent-cli/engines/universal-grep.ts +++ b/apps/sim/lib/mothership/agent-cli/engines/universal-grep.ts @@ -1,5 +1,6 @@ import { sleep } from '@sim/utils/helpers' import { isRecordLike } from '@sim/utils/object' +import { truncate } from '@sim/utils/string' import type { ReadFileTextResponse } from 'sim/embed' import { listCatalogTools } from '@/lib/catalog/application/list-tools' import { readBlockCatalog } from '@/lib/catalog/application/read-block-catalog' @@ -430,7 +431,7 @@ function compilePattern(raw: string, ignoreCase: boolean): (line: string) => boo } function clip(line: string): string { - return line.length > MAX_LINE_CHARS ? `${line.slice(0, MAX_LINE_CHARS)}… [line truncated]` : line + return truncate(line, MAX_LINE_CHARS, '… [line truncated]') } function didYouMean(scope: string): string { diff --git a/apps/sim/providers/anthropic/structured-output-schema.ts b/apps/sim/providers/anthropic/structured-output-schema.ts index ff7532b303f..3b743615249 100644 --- a/apps/sim/providers/anthropic/structured-output-schema.ts +++ b/apps/sim/providers/anthropic/structured-output-schema.ts @@ -1,4 +1,5 @@ import { transformJSONSchema } from '@anthropic-ai/sdk/lib/transform-json-schema' +import { isRecordLike } from '@sim/utils/object' type JsonSchemaNode = Record @@ -71,7 +72,7 @@ interface PreservedConstraint { } function isSchemaNode(value: unknown): value is JsonSchemaNode { - return typeof value === 'object' && value !== null && !Array.isArray(value) + return isRecordLike(value) } /** diff --git a/apps/sim/providers/stream-pump.ts b/apps/sim/providers/stream-pump.ts index 22d4fcf0548..ea3c60524d5 100644 --- a/apps/sim/providers/stream-pump.ts +++ b/apps/sim/providers/stream-pump.ts @@ -13,6 +13,7 @@ * Wired into {@link BlockExecutor} `handleStreamingExecution`. */ +import { toError } from '@sim/utils/errors' import { type AgentStreamEvent, type AgentStreamFormat, @@ -404,7 +405,7 @@ export function createAgentStreamPump(options: CreateAgentStreamPumpOptions): Ag if (drainError && !cancelled) { closeTextStream(drainError) - throw drainError instanceof Error ? drainError : new Error(String(drainError)) + throw toError(drainError) } if (cancelled) { @@ -484,7 +485,7 @@ export function projectStreamingExecutionToByteStream(streamingExec: { return } try { - controller.error(error instanceof Error ? error : new Error(String(error))) + controller.error(toError(error)) } catch { // already closed/errored } diff --git a/apps/sim/stores/workflow-diff/store.ts b/apps/sim/stores/workflow-diff/store.ts index ec46c2192a3..926bb1bb92e 100644 --- a/apps/sim/stores/workflow-diff/store.ts +++ b/apps/sim/stores/workflow-diff/store.ts @@ -1,4 +1,5 @@ import { createLogger } from '@sim/logger' +import { omit } from '@sim/utils/object' import { create } from 'zustand' import { devtools } from 'zustand/middleware' import { stripWorkflowDiffMarkers, WorkflowDiffEngine } from '@/lib/workflows/diff' @@ -511,9 +512,7 @@ export const useWorkflowDiffStore = create id !== workflowId) - ), + reconciliationErrors: omit(state.reconciliationErrors, [workflowId]), })) }, @@ -528,9 +527,7 @@ export const useWorkflowDiffStore = create id !== workflowId) - ), + reconciliationErrors: omit(current.reconciliationErrors, [workflowId]), }) }, diff --git a/apps/sim/tools/github/get_file_content.ts b/apps/sim/tools/github/get_file_content.ts index d7f51feb153..cdc12c09b58 100644 --- a/apps/sim/tools/github/get_file_content.ts +++ b/apps/sim/tools/github/get_file_content.ts @@ -1,3 +1,4 @@ +import { truncate } from '@sim/utils/string' import { getFileExtension, getMimeTypeFromExtension } from '@/lib/uploads/utils/file-utils' import type { FileContentResponse, GetFileContentParams } from '@/tools/github/types' import type { ToolConfig } from '@/tools/types' @@ -105,10 +106,11 @@ export const getFileContentTool: ToolConfig 500 - ? `${decodedContent.substring(0, 500)}...\n\n[Content truncated. Full content available in metadata]` - : decodedContent + const contentPreview = truncate( + decodedContent, + 500, + '...\n\n[Content truncated. Full content available in metadata]' + ) const content = `File: ${data.name} Path: ${data.path} diff --git a/packages/sim-cli/src/commands/protocol/workflow-run-get.ts b/packages/sim-cli/src/commands/protocol/workflow-run-get.ts index 60420654113..37558dcf60e 100644 --- a/packages/sim-cli/src/commands/protocol/workflow-run-get.ts +++ b/packages/sim-cli/src/commands/protocol/workflow-run-get.ts @@ -1,3 +1,4 @@ +import { isRecordLike } from '@sim/utils/object' import type { Command } from 'commander' import { clientFrom } from '../../context' import { CLI_CONTRACT } from '../../contract/commands' @@ -39,7 +40,7 @@ interface WorkflowBlock { } function isRecord(value: unknown): value is Record { - return typeof value === 'object' && value !== null && !Array.isArray(value) + return isRecordLike(value) } /** diff --git a/packages/sim-cli/src/http/client.ts b/packages/sim-cli/src/http/client.ts index 4ffaf7a4b41..c6e2f292894 100644 --- a/packages/sim-cli/src/http/client.ts +++ b/packages/sim-cli/src/http/client.ts @@ -1,3 +1,4 @@ +import { truncate } from '@sim/utils/string' import { writeStderr } from '#sim-cli/output/io' import { hasProgressTerminal, styles } from '#sim-cli/output/presentation' import type { ResolvedProfile, StoredCredential, StoredOAuthCredential } from '../config/index' @@ -139,7 +140,7 @@ function toNonJsonError( const keepSnippet = !isMarkup && text.length > 0 && text.length <= 200 return new SimApiError( `${url} returned ${kind}, not JSON (HTTP ${status}) — check your endpoint.${ - keepSnippet ? ` Response: ${truncate(text, 200)}` : '' + keepSnippet ? ` Response: ${truncate(text, 200, '…')}` : '' }`, status ) @@ -185,10 +186,6 @@ function toApiError( return new SimApiError(`Request failed with status ${status}`, status) } -function truncate(value: string, max: number): string { - return value.length <= max ? value : `${value.slice(0, max)}…` -} - /** * Keeps the useful nested reason from Node/Undici transport failures without * serializing request options, headers, socket objects, or credentials. @@ -203,7 +200,7 @@ function transportErrorMessage(error: unknown): string { const candidate = current as { message?: unknown; code?: unknown; cause?: unknown } const message = typeof candidate.message === 'string' - ? truncate(candidate.message.replace(/\s+/g, ' ').trim(), 300) + ? truncate(candidate.message.replace(/\s+/g, ' ').trim(), 300, '…') : '' const code = typeof candidate.code === 'string' ? candidate.code : '' const detail = `${message}${code && !message.includes(code) ? ` (${code})` : ''}` @@ -448,7 +445,7 @@ export function formatApiErrorDetails(details: unknown): string[] { } visit(details) - if (issues.length === 0) return [` details: ${truncate(JSON.stringify(details), 1000)}`] + if (issues.length === 0) return [` details: ${truncate(JSON.stringify(details), 1000, '…')}`] const kept = dropUnionBranchNoise(issues) const visible = kept.slice(0, 8) diff --git a/packages/testing/src/mocks/human-in-the-loop-manager.mock.ts b/packages/testing/src/mocks/human-in-the-loop-manager.mock.ts index c9011d2bc47..208e18c10c8 100644 --- a/packages/testing/src/mocks/human-in-the-loop-manager.mock.ts +++ b/packages/testing/src/mocks/human-in-the-loop-manager.mock.ts @@ -1,3 +1,4 @@ +import { isRecordLike } from '@sim/utils/object' import { vi } from 'vitest' /** @@ -16,7 +17,7 @@ class MockResumeAdmissionError extends Error { } function isMockRecord(value: unknown): value is Record { - return typeof value === 'object' && value !== null && !Array.isArray(value) + return isRecordLike(value) } function isPausedOutputForContext(output: unknown, contextId: string): boolean { diff --git a/packages/testing/src/mocks/providers-trace-enrichment.mock.ts b/packages/testing/src/mocks/providers-trace-enrichment.mock.ts index 2612281150b..69dff958ad0 100644 --- a/packages/testing/src/mocks/providers-trace-enrichment.mock.ts +++ b/packages/testing/src/mocks/providers-trace-enrichment.mock.ts @@ -1,3 +1,4 @@ +import { isRecordLike } from '@sim/utils/object' import { vi } from 'vitest' /** @@ -22,8 +23,8 @@ export const providersTraceEnrichmentMockFns = { if (typeof rawArguments !== 'string') return '' try { const parsed: unknown = JSON.parse(rawArguments) - if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) { - return parsed as Record + if (isRecordLike(parsed)) { + return parsed } return rawArguments } catch { From 9dc6b4133f2fbf09d09ed902395b02f35cad7127 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 1 Oct 2026 18:03:50 -0700 Subject: [PATCH 03/15] refactor(ui): lazy-init object refs and use size-* for equal height and width useRef(new X()) built a throwaway X on every render; the refs now lazy-init through ??= as sim-react-performance.md prescribes. Equal h-N w-N pairs become size-N per sim-styling.md. --- apps/docs/components/ui/video-placeholder.tsx | 2 +- .../workflow-preview/block-preview.tsx | 2 +- .../workflow-preview/docs-container-node.tsx | 2 +- .../workflow-preview/workflow-preview.tsx | 4 +- .../chat/components/input/input.tsx | 4 +- .../hero-chat-loop/hero-chat-loop.tsx | 2 +- .../hero-platform-loop/hero-platform-loop.tsx | 4 +- .../hero-platform-loop/stage-block-card.tsx | 2 +- .../workflow-menu-preview.tsx | 9 +++-- .../platform-hero-visual.tsx | 2 +- .../build-agents-loop/build-agents-loop.tsx | 4 +- .../governance-loop/governance-loop.tsx | 4 +- .../shared/editor-loop/editor-loop.tsx | 4 +- .../solutions-card/solutions-card.tsx | 2 +- .../solutions-visual-frame.tsx | 4 +- .../components/solutions-page/constants.ts | 4 +- .../components/contact-form/contact-form.tsx | 2 +- .../components/demo-booking/demo-booking.tsx | 8 +--- .../enterprise-home-stage.tsx | 2 +- .../enterprise-platform-loop.tsx | 4 +- .../feature-graphic-shell.tsx | 2 +- .../files/components/files-hero-loop.tsx | 4 +- .../integrations/(shell)/[slug]/page.tsx | 2 +- .../connector-sync-graphic.tsx | 2 +- .../components/knowledge-hero-loop.tsx | 4 +- .../logs/components/logs-hero-loop.tsx | 4 +- .../tables/components/tables-hero-loop.tsx | 6 +-- .../workflow-canvas-graphic.tsx | 2 +- .../components/search-input-bar.tsx | 2 +- .../files/[fileId]/view/file-viewer.tsx | 2 +- .../file-row-context-menu.tsx | 2 +- .../components/file-viewer/image-preview.tsx | 2 +- .../components/file-viewer/preview-panel.tsx | 4 +- .../components/mic-button/mic-button.tsx | 4 +- .../components/send-button/send-button.tsx | 4 +- .../home/hooks/stream/stream-context.ts | 4 +- .../home/hooks/stream/stream-test-helpers.ts | 3 +- .../[workspaceId]/home/hooks/use-chat.ts | 39 ++++++++++--------- .../settings/components/general/general.tsx | 4 +- .../table-grid/headers/column-header-menu.tsx | 6 +-- .../workflow-sidebar/workflow-sidebar.tsx | 2 +- .../w/[workflowId]/components/chat/chat.tsx | 2 +- .../user-input/hooks/use-file-attachments.ts | 23 +++++------ .../components/general/general.tsx | 6 +-- .../panel/components/editor/editor.tsx | 2 +- .../[workspaceId]/w/[workflowId]/workflow.tsx | 2 +- .../preview-editor/preview-editor.tsx | 10 ++--- .../w/components/sidebar/sidebar.tsx | 4 +- .../app/workspace/[workspaceId]/w/page.tsx | 4 +- .../workspace/providers/socket-provider.tsx | 21 +++++----- apps/sim/components/ui/progress.tsx | 2 +- apps/sim/hooks/use-speech-to-text.ts | 11 +++--- .../emcn/src/components/checkbox/checkbox.tsx | 12 +++--- .../src/components/chip-input/chip-input.tsx | 2 +- .../dropdown-menu/dropdown-menu.tsx | 2 +- .../src/components/input-otp/input-otp.tsx | 2 +- .../emcn/src/components/popover/popover.tsx | 4 +- .../src/components/tag-input/tag-input.tsx | 2 +- packages/emcn/src/components/toast/toast.tsx | 18 ++++----- .../workflow-block/workflow-block-view.tsx | 2 +- 60 files changed, 154 insertions(+), 152 deletions(-) diff --git a/apps/docs/components/ui/video-placeholder.tsx b/apps/docs/components/ui/video-placeholder.tsx index e836b418b52..3490e13ca0f 100644 --- a/apps/docs/components/ui/video-placeholder.tsx +++ b/apps/docs/components/ui/video-placeholder.tsx @@ -113,7 +113,7 @@ export function VideoPlaceholder({ pendingSeek.current = null } }} - className='h-full w-full border-0' + className='size-full border-0' > diff --git a/apps/docs/components/workflow-preview/docs-container-node.tsx b/apps/docs/components/workflow-preview/docs-container-node.tsx index 20fbb118870..a81da4a995f 100644 --- a/apps/docs/components/workflow-preview/docs-container-node.tsx +++ b/apps/docs/components/workflow-preview/docs-container-node.tsx @@ -39,7 +39,7 @@ export const DocsContainerNode = memo(function DocsContainerNode({ } return ( -
+
@@ -329,7 +329,7 @@ export function WorkflowPreview({ className='fixed inset-0 m-auto h-[86dvh] max-h-none w-[92vw] max-w-none overflow-hidden rounded-xl border border-[var(--border)] bg-[var(--bg)] p-0 text-[var(--text-body)] backdrop:bg-black/70 backdrop:backdrop-blur-sm' > {expanded && ( -
+
{workflow.name} diff --git a/apps/sim/app/(interfaces)/chat/components/input/input.tsx b/apps/sim/app/(interfaces)/chat/components/input/input.tsx index b2c6c8b48f1..9bd9c9b8b60 100644 --- a/apps/sim/app/(interfaces)/chat/components/input/input.tsx +++ b/apps/sim/app/(interfaces)/chat/components/input/input.tsx @@ -177,10 +177,10 @@ export const ChatInput: React.FC<{ {file.name} ) : ( -
+
{file.name.split('.').pop()} diff --git a/apps/sim/app/(landing)/components/hero/components/hero-chat-loop/hero-chat-loop.tsx b/apps/sim/app/(landing)/components/hero/components/hero-chat-loop/hero-chat-loop.tsx index c02f862bce5..8c828e34d86 100644 --- a/apps/sim/app/(landing)/components/hero/components/hero-chat-loop/hero-chat-loop.tsx +++ b/apps/sim/app/(landing)/components/hero/components/hero-chat-loop/hero-chat-loop.tsx @@ -179,7 +179,7 @@ export function HeroChatLoop({ return (
diff --git a/apps/sim/app/(landing)/components/hero/components/hero-platform-loop/hero-platform-loop.tsx b/apps/sim/app/(landing)/components/hero/components/hero-platform-loop/hero-platform-loop.tsx index 1fa4b426954..7f7e27cc6f8 100644 --- a/apps/sim/app/(landing)/components/hero/components/hero-platform-loop/hero-platform-loop.tsx +++ b/apps/sim/app/(landing)/components/hero/components/hero-platform-loop/hero-platform-loop.tsx @@ -266,7 +266,7 @@ export function HeroPlatformLoop() { >
diff --git a/apps/sim/app/(landing)/components/hero/components/hero-platform-loop/stage-block-card.tsx b/apps/sim/app/(landing)/components/hero/components/hero-platform-loop/stage-block-card.tsx index a679aef3881..7988b9fa5db 100644 --- a/apps/sim/app/(landing)/components/hero/components/hero-platform-loop/stage-block-card.tsx +++ b/apps/sim/app/(landing)/components/hero/components/hero-platform-loop/stage-block-card.tsx @@ -350,7 +350,7 @@ export function StageBlockCard({ if (onSelect) handleKeyboardActivation(event, () => onSelect(block.id)) }} className={cn( - 'relative h-full w-full rounded-2xl', + 'relative size-full rounded-2xl', onSelect && 'cursor-pointer focus-visible:outline-none' )} > diff --git a/apps/sim/app/(landing)/components/navbar/components/nav-menu-chip/components/nav-menu-preview/components/workflow-menu-preview/workflow-menu-preview.tsx b/apps/sim/app/(landing)/components/navbar/components/nav-menu-chip/components/nav-menu-preview/components/workflow-menu-preview/workflow-menu-preview.tsx index 699e664ff05..52c86909022 100644 --- a/apps/sim/app/(landing)/components/navbar/components/nav-menu-chip/components/nav-menu-preview/components/workflow-menu-preview/workflow-menu-preview.tsx +++ b/apps/sim/app/(landing)/components/navbar/components/nav-menu-chip/components/nav-menu-preview/components/workflow-menu-preview/workflow-menu-preview.tsx @@ -31,12 +31,13 @@ interface WorkflowMenuPreviewProps { /** A cropped production canvas with a clear center and progressively softened inner edges. */ export function WorkflowMenuPreview({ layout = 'menu', onReady }: WorkflowMenuPreviewProps) { - const readyBlocksRef = useRef(new Set()) + const readyBlocksRef = useRef | null>(null) + const readyBlocks = (readyBlocksRef.current ??= new Set()) const handleBlockReady = (blockId: string) => { - if (readyBlocksRef.current.has(blockId)) return - readyBlocksRef.current.add(blockId) - if (readyBlocksRef.current.size === BLOCKS.length) onReady?.() + if (readyBlocks.has(blockId)) return + readyBlocks.add(blockId) + if (readyBlocks.size === BLOCKS.length) onReady?.() } return ( diff --git a/apps/sim/app/(landing)/components/platform-hero-visual/platform-hero-visual.tsx b/apps/sim/app/(landing)/components/platform-hero-visual/platform-hero-visual.tsx index 46048f236f6..ab40396ead9 100644 --- a/apps/sim/app/(landing)/components/platform-hero-visual/platform-hero-visual.tsx +++ b/apps/sim/app/(landing)/components/platform-hero-visual/platform-hero-visual.tsx @@ -20,7 +20,7 @@ interface PlatformHeroVisualProps { */ export function PlatformHeroVisual({ children }: PlatformHeroVisualProps) { return ( -
+
@@ -112,7 +112,7 @@ export function BuildAgentsLoop() { stageOpen ? 'w-1/2 border-l' : 'w-0 border-l-0' )} > -
+
-
+
diff --git a/apps/sim/app/(landing)/components/shared/editor-loop/editor-loop.tsx b/apps/sim/app/(landing)/components/shared/editor-loop/editor-loop.tsx index 42aa3408516..332bf4653fb 100644 --- a/apps/sim/app/(landing)/components/shared/editor-loop/editor-loop.tsx +++ b/apps/sim/app/(landing)/components/shared/editor-loop/editor-loop.tsx @@ -98,10 +98,10 @@ export function EditorLoop({ content }: EditorLoopProps) { workflows={content.sidebarWorkflows} activeItem={content.activeItem} > -
+
diff --git a/apps/sim/app/(landing)/components/solutions-page/components/solutions-card-row/components/solutions-card/solutions-card.tsx b/apps/sim/app/(landing)/components/solutions-page/components/solutions-card-row/components/solutions-card/solutions-card.tsx index a74fe87d45a..32e3eb88e6a 100644 --- a/apps/sim/app/(landing)/components/solutions-page/components/solutions-card-row/components/solutions-card/solutions-card.tsx +++ b/apps/sim/app/(landing)/components/solutions-page/components/solutions-card-row/components/solutions-card/solutions-card.tsx @@ -107,7 +107,7 @@ export function SolutionsCard({ card, headingId, tabletSpan = false }: Solutions wide && 'sm:max-lg:mt-0 sm:max-lg:w-auto sm:max-lg:min-w-0' )} > -
{card.visual}
+
{card.visual}
diff --git a/apps/sim/app/(landing)/components/solutions-page/components/solutions-visual-frame/solutions-visual-frame.tsx b/apps/sim/app/(landing)/components/solutions-page/components/solutions-visual-frame/solutions-visual-frame.tsx index d5d9fce410f..0b7b6ecdb06 100644 --- a/apps/sim/app/(landing)/components/solutions-page/components/solutions-visual-frame/solutions-visual-frame.tsx +++ b/apps/sim/app/(landing)/components/solutions-page/components/solutions-visual-frame/solutions-visual-frame.tsx @@ -8,7 +8,7 @@ import { SOLUTIONS_VISUAL } from '@/app/(landing)/components/solutions-page/cons * hero-visual family: `--surface-2` fill, `--border-1` hairline, `rounded-lg`, * `overflow-hidden`) and, crucially, its dimensions: it reserves a full-width * 16:9 aspect ratio for the solutions hero visual. Because the size is reserved - * before paint and the node fills `h-full w-full` inside, a dropped-in node can + * before paint and the node fills `size-full` inside, a dropped-in node can * neither shift surrounding layout (CLS = 0) nor change the frame's own padding. * * The frame is decorative chrome around product visuals, so it is `aria-hidden`; @@ -29,7 +29,7 @@ export function SolutionsVisualFrame({ children }: SolutionsVisualFrameProps) { SOLUTIONS_VISUAL.heroAspect )} > -
{children}
+
{children}
) } diff --git a/apps/sim/app/(landing)/components/solutions-page/constants.ts b/apps/sim/app/(landing)/components/solutions-page/constants.ts index d99293824fb..6675801aeb0 100644 --- a/apps/sim/app/(landing)/components/solutions-page/constants.ts +++ b/apps/sim/app/(landing)/components/solutions-page/constants.ts @@ -74,7 +74,7 @@ export const SOLUTIONS_TEXT_MEASURE = { * Reserved fixed dimensions for the component-owned visual frames. A dropped-in * `ReactNode` renders into a frame of exactly these dimensions, so it can never * shift surrounding layout (CLS = 0) nor change its own frame padding. The node - * fills `h-full w-full` inside; it owns nothing about the frame. + * fills `size-full` inside; it owns nothing about the frame. */ export const SOLUTIONS_VISUAL = { /** Full-width hero visual aspect ratio - reserves height before paint. */ @@ -105,7 +105,7 @@ export const SOLUTIONS_VISUAL = { featureTileContainer: '[container-type:inline-size]', featureTileScale: '[--tile-scale:min(1,tan(atan2(100cqw,352px)))]', featureTileCanvas: - 'h-[calc(100%/var(--tile-scale,1))] w-[calc(100%/var(--tile-scale,1))] origin-top-left [scale:var(--tile-scale,1)]', + 'size-[calc(100%/var(--tile-scale,1))] origin-top-left [scale:var(--tile-scale,1)]', /** * Feature-tile minimum height - the `440px` design-space height, multiplied * by the tile's current scale so height tracks the zoomed content. The diff --git a/apps/sim/app/(landing)/contact/components/contact-form/contact-form.tsx b/apps/sim/app/(landing)/contact/components/contact-form/contact-form.tsx index 534fa557cdc..bf89ed90878 100644 --- a/apps/sim/app/(landing)/contact/components/contact-form/contact-form.tsx +++ b/apps/sim/app/(landing)/contact/components/contact-form/contact-form.tsx @@ -231,7 +231,7 @@ export function ContactForm() { >