Skip to content

Commit 981be93

Browse files
authored
v0.8.11: perf improvements, instant chat navigation, code hygiene
2 parents f1a3456 + 7b761ba commit 981be93

367 files changed

Lines changed: 51406 additions & 2667 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.claude/rules/emcn-components.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,16 @@ The menu surface intentionally diverges from the pill: `dropdown-menu.tsx` items
3333
- **`ChipTimePicker`** — minute-granular time sibling of `ChipDatePicker`, a `ChipInput` that leniently parses typed input (`9:47`, `947`, `2:05pm`, `14:30`), commits on Enter/blur, and re-renders the canonical `9:47 AM` label.
3434
- **`DropdownMenu`** — the canonical context/action menu (Radix-backed). Not a chip, but the standard menu for command/action lists; reach for it instead of a hand-rolled popover. Its surface intentionally diverges from the chip pill (`text-small`, `gap-2`) — keep them distinct. For a pill that opens a value picker, use `ChipDropdown`/`ChipSelect` instead.
3535

36+
## Modal keyboard defaults
37+
38+
Declare keyboard intent on the action-owning primitive; never add document-level or per-callsite Enter listeners.
39+
40+
- `ChipModalFooter` defaults to `defaultAction='primary'`. A plain Enter in a canonical single-line field or a custom plain input invokes the enabled primary action. Use `'none'` when submission must require an explicit click, such as an irreversible destructive action or an editor whose nested control owns Enter. Use `'dismiss'` only when dismissal is genuinely the modal's default decision.
41+
- `ChipConfirmModal` fails safe with `defaultAction='dismiss'`. Opt into `'confirm'` only for an audited, low-impact reversible or non-destructive decision. Deleting an aggregate resource such as a workflow, table, knowledge base, or folder remains `'dismiss'` even when it can be restored, because the action takes a broad dependent graph offline. Use `'none'` for typed confirmations and severe account, ownership, or access changes. Button color never determines keyboard behavior.
42+
- Textareas, native forms, buttons, links, comboboxes, menus, listboxes, tag/email inputs, IME composition, modified Enter, and disabled or pending actions retain their native behavior. A native form remains the sole submission path so browser validation is not bypassed.
43+
- A custom field containing a search, token editor, or another input that owns Enter must set `submitOnEnter={false}` on `ChipModalField`. Do not attach a duplicate `onKeyDown` handler merely to call the footer action.
44+
- Initial focus goes to the first visible editable text control. With no text control, the declared real button receives focus; `'none'` focuses the dialog surface. A safe dismiss default never turns Enter in a text field into data loss—the field simply does not publish a submit action.
45+
3646
## Authoring principles
3747

3848
- **One source of truth for shared chrome.** Compose from `chip-chrome.ts` / `chipVariants`; never duplicate the chrome string.

.claude/rules/sim-react-performance.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,19 @@ const [{ id }, { kbName }] = await Promise.all([params, searchParams])
9090

9191
Only keep awaits sequential when a later call genuinely uses an earlier result, or when the ordering is deliberate (rate-limited batches, retry loops, write-then-read).
9292

93+
## Prefetch dynamic destination lists on intent
94+
95+
For long lists of dynamic destinations, do not viewport-prefetch every row and do not assume
96+
`router.prefetch()` warms the full route: in Next 16 it uses the automatic/PPR strategy. Gate
97+
`<Link prefetch={true}>` behind deliberate hover or keyboard focus, and prefetch destination
98+
server state with the consumer's shared React Query options. A short, cancelable hover dwell
99+
avoids drive-by downloads. Do not treat `touchstart` as intent because it also begins scrolling;
100+
let the actual unmodified click start the data request.
101+
102+
If a continuity-focused surface intentionally omits `loading.tsx` so the current view remains
103+
mounted until its peer is ready, the intent path must warm both the full route and its critical
104+
data. Otherwise keep the loading boundary so dynamic navigation remains responsive.
105+
93106
## Local feature barrels are the convention — do not "fix" them
94107

95108
Tooling (e.g. react-doctor's `no-barrel-import`) will flag imports from local `index.ts` barrels as a bundle cost. In this repo that is a **false positive**: barrel imports for 3+ export folders are mandated by `.claude/rules/sim-imports.md`. Leave them.

.claude/rules/sim-settings-pages.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,9 @@ The Next.js `settings/[section]/layout.tsx` owns all settings page chrome via
1313
`SettingsHeaderShell` — a fixed header bar (a left back chip + right-aligned
1414
action chips), a scroll region, and a centered `max-w-[48rem]` content column led
1515
by a **title + description from navigation metadata**. The chrome stays mounted
16-
across section navigation (it never re-renders or re-lays-out). Each section
17-
renders through the **`SettingsPanel`** registrar
16+
across section navigation. Its routed title and description are available before
17+
the section body resolves. Each section renders through the **`SettingsPanel`**
18+
registrar
1819
(`@/app/workspace/[workspaceId]/settings/components/settings-panel`), which feeds
1920
the shell its header data and renders only the section body. Sections supply
2021
**data**, never chrome.
@@ -82,6 +83,9 @@ return (
8283
`children` instead and omit the prop.
8384
- `title?` / `description?` — overrides for the nav-driven defaults. **Only** for a
8485
detail sub-view that needs a different heading; normal pages never pass these.
86+
A top-level page's header identity must remain stable while its data loads:
87+
never replace navigation metadata with client-fetched copy after first paint.
88+
Put data-dependent context in the page body instead.
8589
- `scrollContainerRef?: React.Ref<HTMLDivElement>` — forwards a ref to the scroll
8690
region (e.g. programmatic scroll-to-bottom).
8791

.claude/rules/sim-url-state.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,11 @@ import KnowledgeBaseLoading from '@/app/workspace/[workspaceId]/knowledge/[id]/l
143143

144144
Reference: `apps/sim/app/workspace/[workspaceId]/knowledge/[id]/page.tsx`.
145145

146+
The narrow exception is a continuity-focused peer switch that deliberately keeps the current
147+
view mounted and follows the full-route plus critical-data intent-prefetch rule in
148+
`sim-react-performance.md`. It still needs a real in-page Suspense fallback; it only omits the
149+
route-level `loading.tsx` that would replace the current peer before the destination is ready.
150+
146151
This applies to **page entries**. An inner `<Suspense>` wrapping a `lazy()` component is the exception: there `fallback={null}` is correct, precisely so the suspend resolves at the nearest boundary instead of flashing the whole route — see `sim-imports.md`, "Code-splitting through barrels".
147152

148153
## Debounced text inputs

.github/workflows/test-build.yml

Lines changed: 41 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,18 @@ jobs:
1414
timeout-minutes: 15
1515

1616
steps:
17+
# The diff-based audits below need a base commit to read, and the default
18+
# depth of 1 clones a single commit with no parent. They normally fetch
19+
# their base by SHA (see "Resolve base ref"), so this depth only covers the
20+
# `HEAD~1` fallback — but without it that fallback resolves to nothing.
21+
#
22+
# Worth stating because the failure was invisible for so long: the migration
23+
# audit read the resulting `git diff` failure as "no migrations changed" and
24+
# exited 0, so it had never actually run on a push build.
1725
- name: Checkout code
1826
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
27+
with:
28+
fetch-depth: 2
1929

2030
- name: Setup Bun
2131
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
@@ -104,15 +114,40 @@ jobs:
104114
105115
echo "✅ All env flags are properly configured"
106116
107-
- name: Check block registry invariants
117+
# One fetch for both base-ref audits, and no `|| true`: a swallowed fetch leaves
118+
# the base ref absent, which neither audit can tell apart from a branch that
119+
# changed nothing. The block-registry check at least degrades to a visible
120+
# `⚠ … skipping` line; the migration audit printed `✓ No new migrations to
121+
# check` and exited 0, clearing the only guard on production DDL.
122+
#
123+
# Depth stays at 1 — without a merge-base the migration audit diffs the two
124+
# tips, which under `--diff-filter=AM` is exactly the migrations new here.
125+
# Resolved once for both diff-based audits, and never with `|| true`: a
126+
# swallowed fetch leaves the base absent, which neither audit can tell apart
127+
# from a branch that changed nothing.
128+
#
129+
# On push the base is `github.event.before`, the tip the branch had before
130+
# this push — not `HEAD~1`, which names only the last commit and would let a
131+
# multi-commit push slip every earlier commit's migrations past the audit.
132+
# It is fetched by SHA at depth 1; the audits diff two tips and need no
133+
# common ancestry. An all-zero `before` means the branch is new and has no
134+
# predecessor to diff, so `HEAD~1` remains the fallback there.
135+
- name: Resolve base ref for diff-based audits
136+
id: audit_base
108137
run: |
109138
if [ "${{ github.event_name }}" = "pull_request" ]; then
110-
BASE_REF="origin/${{ github.base_ref }}"
111-
git fetch --depth=1 origin "${{ github.base_ref }}" 2>/dev/null || true
139+
git fetch --depth=1 origin "${{ github.base_ref }}"
140+
echo "ref=origin/${{ github.base_ref }}" >> "$GITHUB_OUTPUT"
141+
elif [ -n "${{ github.event.before }}" ] &&
142+
[ "${{ github.event.before }}" != "0000000000000000000000000000000000000000" ]; then
143+
git fetch --depth=1 origin "${{ github.event.before }}"
144+
echo "ref=${{ github.event.before }}" >> "$GITHUB_OUTPUT"
112145
else
113-
BASE_REF="HEAD~1"
146+
echo "ref=HEAD~1" >> "$GITHUB_OUTPUT"
114147
fi
115-
bun run apps/sim/scripts/check-block-registry.ts "$BASE_REF"
148+
149+
- name: Check block registry invariants
150+
run: bun run apps/sim/scripts/check-block-registry.ts "${{ steps.audit_base.outputs.ref }}"
116151

117152
- name: Lint code
118153
run: bun run lint:check
@@ -127,14 +162,7 @@ jobs:
127162
run: bun run docs-manifest:check
128163

129164
- name: Migration safety (zero-downtime) audit
130-
run: |
131-
if [ "${{ github.event_name }}" = "pull_request" ]; then
132-
BASE_REF="origin/${{ github.base_ref }}"
133-
git fetch --depth=1 origin "${{ github.base_ref }}" 2>/dev/null || true
134-
else
135-
BASE_REF="HEAD~1"
136-
fi
137-
bun run check:migrations "$BASE_REF"
165+
run: bun run check:migrations "${{ steps.audit_base.outputs.ref }}"
138166

139167
# Every workspace, not just realtime. packages/emcn, packages/utils,
140168
# apps/desktop and apps/docs had no type check in CI at all; apps/sim's

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ npx sim-setup add sandbox
8888
npx sim-setup add jobs
8989
npx sim-setup add cache
9090
npx sim-setup add knowledge
91+
npx sim-setup add chat
9192
npx sim-setup add llm
9293
npx sim-setup add integration slack
9394
```

apps/docs/components/workflow-preview/docs-container-node.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ interface DocsContainerData {
88
name: string
99
blockType: string
1010
size?: { width: number; height: number }
11+
parentId?: string
1112
}
1213

1314
/**
@@ -24,6 +25,7 @@ export const DocsContainerNode = memo(function DocsContainerNode({
2425
name: data.name,
2526
width: data.size?.width,
2627
height: data.size?.height,
28+
parentId: data.parentId,
2729
isPreview: true,
2830
}
2931

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/**
2+
* @vitest-environment node
3+
*/
4+
import { BLOCK_Z_BASE, CONTAINER_CHILD_Z_BASE, getEdgeZIndex } from '@sim/workflow-renderer'
5+
import { describe, expect, it } from 'vitest'
6+
import { type PreviewBlock, type PreviewWorkflow, toReactFlowElements } from './workflow-data'
7+
8+
const block = (
9+
overrides: Partial<PreviewBlock> & Pick<PreviewBlock, 'id' | 'type'>
10+
): PreviewBlock => ({
11+
name: overrides.id,
12+
bgColor: '#000000',
13+
rows: [],
14+
position: { x: 0, y: 0 },
15+
...overrides,
16+
})
17+
18+
const workflow: PreviewWorkflow = {
19+
id: 'nested-subflows',
20+
name: 'Nested subflows',
21+
blocks: [
22+
block({ id: 'start', type: 'starter' }),
23+
block({ id: 'loop', type: 'loop', size: { width: 500, height: 300 } }),
24+
block({
25+
id: 'parallel',
26+
type: 'parallel',
27+
parentId: 'loop',
28+
position: { x: 24, y: 64 },
29+
size: { width: 400, height: 200 },
30+
}),
31+
block({ id: 'agent', type: 'agent', parentId: 'loop', position: { x: 24, y: 140 } }),
32+
],
33+
edges: [
34+
{ id: 'start-loop', source: 'start', target: 'loop' },
35+
{ id: 'loop-parallel', source: 'loop', target: 'parallel' },
36+
{ id: 'loop-agent', source: 'loop', target: 'agent' },
37+
],
38+
}
39+
40+
describe('toReactFlowElements layering', () => {
41+
it('places incoming edges on their container target layer', () => {
42+
const { nodes, edges } = toReactFlowElements(workflow, false, {
43+
highlightEdge: 'loop-parallel',
44+
})
45+
const nodeById = new Map(nodes.map((node) => [node.id, node]))
46+
const edgeById = new Map(edges.map((edge) => [edge.id, edge]))
47+
48+
expect(nodeById.get('loop')?.zIndex).toBe(0)
49+
expect(nodeById.get('parallel')?.zIndex).toBe(1)
50+
expect(edgeById.get('start-loop')?.zIndex).toBe(0)
51+
expect(edgeById.get('loop-parallel')?.zIndex).toBe(1)
52+
})
53+
54+
it('keeps ordinary cards above normally layered edges', () => {
55+
const { nodes, edges } = toReactFlowElements(workflow)
56+
const nodeById = new Map(nodes.map((node) => [node.id, node]))
57+
const edgeById = new Map(edges.map((edge) => [edge.id, edge]))
58+
59+
expect(nodeById.get('start')?.zIndex).toBe(BLOCK_Z_BASE)
60+
expect(nodeById.get('agent')?.zIndex).toBe(CONTAINER_CHILD_Z_BASE)
61+
expect(edgeById.get('loop-agent')?.zIndex).toBe(getEdgeZIndex(0))
62+
})
63+
})

apps/docs/components/workflow-preview/workflow-data.ts

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
import {
2+
BLOCK_Z_BASE,
3+
CONTAINER_CHILD_Z_BASE,
4+
getEdgeZIndex,
5+
getEdgeZIndexForTarget,
6+
} from '@sim/workflow-renderer'
17
import { type Edge, type Node, Position } from 'reactflow'
28

39
/**
@@ -61,6 +67,24 @@ export interface HighlightOptions {
6167
selectedBlock?: string
6268
}
6369

70+
/** Semantic container depth used for z-order while docs positions stay flattened. */
71+
function getNestingDepth(block: PreviewBlock, blocksById: Map<string, PreviewBlock>): number {
72+
let depth = 0
73+
let parentId = block.parentId
74+
const visited = new Set<string>()
75+
76+
while (parentId && !visited.has(parentId)) {
77+
const parent = blocksById.get(parentId)
78+
if (!parent) break
79+
80+
visited.add(parentId)
81+
depth += 1
82+
parentId = parent.parentId
83+
}
84+
85+
return depth
86+
}
87+
6488
/**
6589
* Converts a {@link PreviewWorkflow} to React Flow nodes and edges.
6690
*
@@ -81,6 +105,7 @@ export function toReactFlowElements(
81105

82106
const nodes: Node[] = workflow.blocks.map((block, index) => {
83107
const isContainer = Boolean(block.size)
108+
const nestingDepth = getNestingDepth(block, blocksById)
84109
// Nested blocks are authored relative to their container; render them at
85110
// absolute coordinates (not React Flow parentNode children) so the edges
86111
// between a container and its nested blocks render reliably and on top.
@@ -92,7 +117,7 @@ export function toReactFlowElements(
92117
id: block.id,
93118
type: isContainer ? 'previewContainer' : 'previewBlock',
94119
position,
95-
zIndex: isContainer ? 0 : 1,
120+
zIndex: isContainer ? nestingDepth : block.parentId ? CONTAINER_CHILD_Z_BASE : BLOCK_Z_BASE,
96121
...(block.size ? { style: { width: block.size.width, height: block.size.height } } : {}),
97122
data: {
98123
name: block.name,
@@ -103,6 +128,7 @@ export function toReactFlowElements(
103128
tools: block.tools,
104129
hideTargetHandle: block.hideTargetHandle,
105130
size: block.size,
131+
parentId: block.parentId,
106132
index,
107133
animate,
108134
isHighlighted: highlightBlock === block.id || selectedBlock === block.id,
@@ -127,6 +153,14 @@ export function toReactFlowElements(
127153
// so edges into and out of Loop/Parallel containers still connect.
128154
const sourceBlock = blocksById.get(e.source)
129155
const targetBlock = blocksById.get(e.target)
156+
const parentContainer = blocksById.get(sourceBlock?.parentId ?? targetBlock?.parentId ?? '')
157+
const baseZIndex = getEdgeZIndex(
158+
parentContainer ? getNestingDepth(parentContainer, blocksById) : undefined,
159+
{ isHighlighted: isEdgeHighlight }
160+
)
161+
const targetContainerZIndex = targetBlock?.size
162+
? getNestingDepth(targetBlock, blocksById)
163+
: undefined
130164
const sourceHandle =
131165
e.sourceHandle ?? (sourceBlock?.size ? `${sourceBlock.type}-end-source` : 'source')
132166
const targetHandle = targetBlock?.size ? undefined : 'target'
@@ -142,6 +176,7 @@ export function toReactFlowElements(
142176
},
143177
sourceHandle,
144178
targetHandle,
179+
zIndex: getEdgeZIndexForTarget(baseZIndex, targetContainerZIndex),
145180
data: {
146181
animate,
147182
delay: animate ? sourceIndex * BLOCK_STAGGER + BLOCK_STAGGER : 0,

apps/docs/content/docs/en/platform/credentials.mdx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,12 +119,27 @@ Click **Details** on any secret row to open its detail view.
119119
From here you can:
120120

121121
- View the **Key** and edit the **Value**
122+
- Toggle **Visibility** — show the value unmasked in run output; see [Visibility](#visibility)
122123
- Edit the **Description** — an optional note telling teammates what the secret is for. Workspace secrets only; a personal secret is not shared, so it has none
123124
- Manage **Members** — invite teammates by email and assign them an **Admin** or **Member** role
124125
- Open **See usage** — where this secret has actually been used
125126

126127
Click **Save** to apply changes, or **Back** to return to the list.
127128

129+
### Visibility
130+
131+
By default, a secret's resolved value is masked everywhere Sim shows run output (see [Execution log protection](#execution-log-protection)). For values that aren't actually sensitive — a staging key, a shared base URL — that masking makes your own logs harder to read.
132+
133+
**Show value in logs and Chat** turns masking off for one workspace secret. With it on:
134+
135+
- Run logs, Chat, and code output show the real value instead of `{{KEY}}`
136+
- Files a run writes with the value in them stay readable and attachable
137+
- The Secrets API list includes the value for this secret, so external agents can read it directly instead of scraping logs
138+
139+
The value becomes visible to **anyone who can see this workspace's runs** — including publicly shared log links and log exports, and regardless of member restrictions on the secret itself. Only turn it on for values you'd be comfortable printing in a log.
140+
141+
The switch applies to future runs only. Logs written while the secret was masked stay masked, and anything written while it was visible keeps the value even if you turn masking back on. If another secret holds the same value, that value stays masked — masking always wins a conflict. Workspace secrets only; the same people who can edit the description can flip it.
142+
128143
### See usage
129144

130145
**See usage** lists the runs that resolved this secret: when it was last used, what used it (a workflow, the Sim agent, or an MCP server), how it was triggered, who it resolved under, and a link to the most recent run in Logs. Rows are grouped by day, so a workflow on a schedule reads as one row per day rather than thousands.

0 commit comments

Comments
 (0)