Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude/rules/sim-content-delivery.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ own unless noted.

| Path | U | E | R / C |
|---|---|---|---|
| File download (CLI `files get`) | Delivered once; invocation withheld from the model; workbench copy stays unknown | Redacted | Delivered |
| File download (CLI `files get`) | Delivered once; invocation withheld from the model; workbench copy stays unknown | Opaque: into a chat workbench with entries imported, so output is redacted; refused without one | Delivered |
| File download (run_code proxy) | Refused before bytes enter workbench history | Recorded in workbench history | Delivered |
| Requested mounts | Refused before code executes, listed in `excludedInputs`; later calls remain usable | Recorded in workbench history | Delivered |
| File text, version text, `files read`, grep | 403 | Redacted (grep redacts before clipping) | Delivered |
Expand Down
2 changes: 1 addition & 1 deletion .cursor/rules/sim-content-delivery.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ own unless noted.

| Path | U | E | R / C |
|---|---|---|---|
| File download (CLI `files get`) | Delivered once; invocation withheld from the model; workbench copy stays unknown | Redacted | Delivered |
| File download (CLI `files get`) | Delivered once; invocation withheld from the model; workbench copy stays unknown | Opaque: into a chat workbench with entries imported, so output is redacted; refused without one | Delivered |
| File download (run_code proxy) | Refused before bytes enter workbench history | Recorded in workbench history | Delivered |
| Requested mounts | Refused before code executes, listed in `excludedInputs`; later calls remain usable | Recorded in workbench history | Delivered |
| File text, version text, `files read`, grep | 403 | Redacted (grep redacts before clipping) | Delivered |
Expand Down
3 changes: 2 additions & 1 deletion .github/scripts/promote-app-image.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@ REGISTRY="${1:?registry required}"
REPOSITORY="${2:?repository required}"
SOURCE_TAG="${3:?source tag required}"
DEPLOY_TAG="${4:?deploy tag required}"
RELEASE_TAG="${5:-}"
: "${GITHUB_OUTPUT:?GitHub output file required}"
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
PREVIOUS=$(bash "$SCRIPT_DIR/get-ecr-image-digest.sh" "$REPOSITORY" "$DEPLOY_TAG" --allow-missing)
EPOCH=$(date +%s)
docker buildx imagetools create -t "$REGISTRY/$REPOSITORY:$DEPLOY_TAG" "$REGISTRY/$REPOSITORY:$SOURCE_TAG"
bash "$SCRIPT_DIR/promote-image.sh" "$REGISTRY" "$REPOSITORY" "$SOURCE_TAG" "$DEPLOY_TAG" "$RELEASE_TAG"
DIGEST=$(bash "$SCRIPT_DIR/get-ecr-image-digest.sh" "$REPOSITORY" "$DEPLOY_TAG")
CHANGED=true
if [ "$DIGEST" = "$PREVIOUS" ]; then CHANGED=false; fi
Expand Down
12 changes: 12 additions & 0 deletions .github/scripts/promote-image.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#!/usr/bin/env bash
# Point a deploy tag (and, for production, its release-<sha> alias) at a built image.
# One imagetools call writes both tags, so they always share one list digest.
set -euo pipefail
REGISTRY="${1:?registry required}"
REPOSITORY="${2:?repository required}"
SOURCE_TAG="${3:?source tag required}"
DEPLOY_TAG="${4:?deploy tag required}"
RELEASE_TAG="${5:-}"
TAG_ARGS=(-t "$REGISTRY/$REPOSITORY:$DEPLOY_TAG")
if [ -n "$RELEASE_TAG" ]; then TAG_ARGS+=(-t "$REGISTRY/$REPOSITORY:$RELEASE_TAG"); fi
docker buildx imagetools create "${TAG_ARGS[@]}" "$REGISTRY/$REPOSITORY:$SOURCE_TAG"
11 changes: 11 additions & 0 deletions .github/workflows/checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,17 @@ jobs:
working-directory: packages/db
run: bun run db:migrate

# JOBS=4 caps node-gyp parallelism as docker/app.Dockerfile does (laverdet/isolated-vm#428).
- name: Build native isolate addon for Project file rendering
if: matrix.group == 'project-files'
working-directory: apps/sim
env:
JOBS: 4
run: |
addon_dir="$(node -p 'require("node:path").dirname(require.resolve("isolated-vm/package.json", { paths: ["."] }))')"
cd "$addon_dir"
bun run install

# No step timeout: the job's bound covers a hang without cutting a slow but healthy suite
# short of writing its report.
- name: Run end-to-end suites
Expand Down
11 changes: 6 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -520,8 +520,11 @@ jobs:
run: |
REGISTRY="${{ steps.login-ecr.outputs.registry }}"

RELEASE_TAG=""
if [ "${{ github.ref }}" = "refs/heads/main" ]; then
ECR_TAG="latest"
# ECR lifecycle retains release- aliases as prod rollback targets.
RELEASE_TAG="release-${SOURCE_TAG}"
elif [ "${{ github.ref }}" = "refs/heads/dev" ]; then
ECR_TAG="dev"
else
Expand All @@ -540,13 +543,11 @@ jobs:
# Move the app last so a preceding tag failure cannot start app rollout
# and then skip the downstream Trigger promotion job.
for repo in $ECR_REPOS; do
echo "🚀 Promoting ${repo}:${SOURCE_TAG} to ${ECR_TAG}"
echo "🚀 Promoting ${repo}:${SOURCE_TAG} to ${ECR_TAG}${RELEASE_TAG:+ and ${RELEASE_TAG}}"
if [ "$repo" = "$APP_REPO" ]; then
bash .github/scripts/promote-app-image.sh "$REGISTRY" "$APP_REPO" "$SOURCE_TAG" "$ECR_TAG"
bash .github/scripts/promote-app-image.sh "$REGISTRY" "$APP_REPO" "$SOURCE_TAG" "$ECR_TAG" "$RELEASE_TAG"
else
docker buildx imagetools create \
-t "${REGISTRY}/${repo}:${ECR_TAG}" \
"${REGISTRY}/${repo}:${SOURCE_TAG}"
bash .github/scripts/promote-image.sh "$REGISTRY" "$repo" "$SOURCE_TAG" "$ECR_TAG" "$RELEASE_TAG"
fi
done

Expand Down
120 changes: 92 additions & 28 deletions apps/desktop/e2e/chat-panel.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,9 @@ test('chat panel sizes survive navigation, chat switches, collapse, and layout c
})
const shell = app
const page = await shell.firstWindow()
const inputSession = await page.context().newCDPSession(page)
// Playwright otherwise keeps the document focused when its native window loses focus.
await inputSession.send('Emulation.setFocusEmulationEnabled', { enabled: false })
page.on('pageerror', (error) => errors.push(error.message))
page.on('crash', () => {
rendererCrashed = true
Expand Down Expand Up @@ -253,41 +256,102 @@ test('chat panel sizes survive navigation, chat switches, collapse, and layout c
'detach',
'chat switch',
] as const) {
await check(`${interruption} keeps the previous saved width`, async () => {
const keepsDraggedWidth = interruption === 'capture loss' || interruption === 'blur'
await check(
`${interruption} keeps the ${keepsDraggedWidth ? 'dragged' : 'previous saved'} width`,
async () => {
const before = await page.evaluate(() => JSON.stringify(localStorage))
const rect = await beginDrag()
await page.mouse.move(rect.x + 100, rect.y + 100, { steps: 12 })
await expectWidth(598)
if (interruption === 'pointercancel') {
await divider.dispatchEvent('pointercancel', { pointerId: 1 })
} else if (interruption === 'capture loss') {
await divider.evaluate((element) => element.releasePointerCapture(1))
await page.mouse.move(rect.x + 100, rect.y + 100)
} else if (interruption === 'blur') {
await shell.evaluate(({ BrowserWindow }) => BrowserWindow.getAllWindows()[0].blur())
await expect.poll(() => page.evaluate(() => document.hasFocus())).toBe(false)
} else if (interruption === 'chat switch') {
await page
.getByRole('button', { name: 'organization-chat-a', exact: true })
.evaluate((element: HTMLButtonElement) => element.click())
await expectWidth(560)
} else {
await page
.getByRole('button', { name: 'Settings', exact: true })
.evaluate((element: HTMLButtonElement) => element.click())
await expect(panel).toHaveCount(0)
}
if (keepsDraggedWidth) {
await expectWidth(598)
expect(await page.evaluate(() => JSON.stringify(localStorage))).not.toBe(before)
if (interruption === 'blur') {
await shell.evaluate(({ app, BrowserWindow }) => {
app.focus({ steal: true })
BrowserWindow.getAllWindows()[0].focus()
})
}
await page.mouse.move(rect.x + 150, rect.y + 100)
await expectWidth(598)
}
await page.mouse.up()
if (interruption === 'chat switch') {
await page.getByRole('button', { name: 'workspace-chat-b', exact: true }).click()
}
if (interruption === 'detach') {
await page.getByRole('button', { name: 'Back', exact: true }).click()
}
await expectWidth(keepsDraggedWidth ? 598 : 698)
if (keepsDraggedWidth) {
await page.reload()
await page.getByRole('button', { name: 'workspace-chat-b', exact: true }).click()
await expectWidth(598)
await dragTo(698)
} else {
expect(await page.evaluate(() => JSON.stringify(localStorage))).toBe(before)
}
}
)
}

for (const button of ['middle', 'right'] as const) {
await check(`${button}-button drags leave the panel width unchanged`, async () => {
const before = await page.evaluate(() => JSON.stringify(localStorage))
const rect = await beginDrag()
await divider.hover({ position: { x: 4, y: 100 } })
const rect = await panel.boundingBox()
if (!rect) throw new Error('Missing panel bounds')
await page.mouse.down({ button })
await page.mouse.move(rect.x + 100, rect.y + 100, { steps: 12 })
await expectWidth(598)
if (interruption === 'pointercancel') {
await divider.dispatchEvent('pointercancel', { pointerId: 1 })
} else if (interruption === 'capture loss') {
await divider.evaluate((element) => element.releasePointerCapture(1))
await page.mouse.move(rect.x + 101, rect.y + 100)
} else if (interruption === 'blur') {
await page.evaluate(() => window.dispatchEvent(new Event('blur')))
} else if (interruption === 'chat switch') {
await page
.getByRole('button', { name: 'organization-chat-a', exact: true })
.evaluate((element: HTMLButtonElement) => element.click())
await expectWidth(560)
} else {
await page
.getByRole('button', { name: 'Settings', exact: true })
.evaluate((element: HTMLButtonElement) => element.click())
await expect(panel).toHaveCount(0)
}
await page.mouse.up()
if (interruption === 'chat switch') {
await page.getByRole('button', { name: 'workspace-chat-b', exact: true }).click()
}
if (interruption === 'detach') {
await page.getByRole('button', { name: 'Back', exact: true }).click()
}
await page.mouse.up({ button })
await expectWidth(698)
expect(await page.evaluate(() => JSON.stringify(localStorage))).toBe(before)
})
}

await check('a touch drag resizes the panel without becoming a scroll gesture', async () => {
const rect = await panel.boundingBox()
if (!rect) throw new Error('Missing panel bounds')
await inputSession.send('Emulation.setTouchEmulationEnabled', { enabled: true })
await inputSession.send('Input.dispatchTouchEvent', {
type: 'touchStart',
touchPoints: [{ x: rect.x, y: rect.y + 100 }],
})
for (let offset = 10; offset <= 100; offset += 10) {
await inputSession.send('Input.dispatchTouchEvent', {
type: 'touchMove',
touchPoints: [{ x: rect.x + offset, y: rect.y + 100 }],
})
}
await inputSession.send('Input.dispatchTouchEvent', { type: 'touchEnd', touchPoints: [] })
await inputSession.send('Emulation.setTouchEmulationEnabled', { enabled: false })
await expectWidth(598)
await page.reload()
await page.getByRole('button', { name: 'workspace-chat-b', exact: true }).click()
await expectWidth(598)
await dragTo(698)
})

await check('a viewport change during a drag clamps the committed display width', async () => {
await expectWidth(698)
const rect = await beginDrag()
Expand Down
21 changes: 21 additions & 0 deletions apps/docs/content/docs/cli/files.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -802,6 +802,27 @@ sim files mkdir <path>

API request and response schemas: [createFileFolder](/api-reference/files/createFilesFolder).

## Other commands

### Copy file items

OAuth login or personal API key required.

```bash
sim files copy [options]
```

**Options**

<CommandTable>

| Option | Required | Description |
| --- | --- | --- |
| `--source <json\|@file>` | Yes | Source owner and selected fileIds or folderIds (JSON, or @path / @- to read a file or stdin). |
| `--destination <json\|@file>` | Yes | Destination owner and optional folderId (JSON, or @path / @- to read a file or stdin). |

</CommandTable>

## Related docs

- [Files](/files)
Expand Down
19 changes: 19 additions & 0 deletions apps/docs/content/docs/cli/reference.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -988,6 +988,25 @@ sim files batch-delete [options]

API request and response schemas: [bulkDeleteFiles](/api-reference/files/bulkDeleteFiles).

### sim files copy

Copy file items (OAuth login or personal API key required)

```bash
sim files copy [options]
```

**Options**

<CommandTable>

| Option | Required | Description |
| --- | --- | --- |
| `--source <json\|@file>` | Yes | Source owner and selected fileIds or folderIds (JSON, or @path / @- to read a file or stdin). |
| `--destination <json\|@file>` | Yes | Destination owner and optional folderId (JSON, or @path / @- to read a file or stdin). |

</CommandTable>

### sim files create

Create file
Expand Down
28 changes: 1 addition & 27 deletions apps/docs/lib/llms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,32 +4,6 @@ import { getIntegrationLandingPage, INTEGRATION_LANDING_LABEL } from '@/lib/inte
import { getApiSpecContent } from '@/lib/openapi'
import type { PageData, source } from '@/lib/source'

const TABLE_ROW = /^\|.*\|$/
const TABLE_DELIMITER_CELL = /^\s*(:?)-+(:?)\s*$/
const UNESCAPED_PIPE = /(?<!\\)\|/

/**
* The processed-markdown serializer pads every table cell to its column's widest cell, so one long
* enum (a parameter listing every timezone) pads every row of its table and roughly doubles the
* integration reference. Drop that trailing padding; a cell's own leading indentation (the `↳`
* nesting in output tables) and inline code are kept, and the tables stay valid GFM.
*/
function compactTables(markdown: string): string {
let inFence = false
return markdown
.split('\n')
.map((line) => {
if (line.startsWith('```')) inFence = !inFence
if (inFence || !TABLE_ROW.test(line)) return line
const cells = line.slice(1, -1).split(UNESCAPED_PIPE)
if (cells.every((cell) => TABLE_DELIMITER_CELL.test(cell))) {
return `|${cells.map((cell) => cell.replace(TABLE_DELIMITER_CELL, ' $1---$2 ')).join('|')}|`
}
return `|${cells.map((cell) => ` ${cell.replace(/^ /, '').trimEnd()} `).join('|')}|`
})
.join('\n')
}

type LLMPageData = PageData & { getAPIPageProps?: () => ApiPageProps }

/**
Expand Down Expand Up @@ -62,5 +36,5 @@ export async function getLLMText(page: InferPageType<typeof source>) {
: ''
return `${heading}

${landingLink}${compactTables(processed)}`
${landingLink}${processed}`
}
17 changes: 17 additions & 0 deletions apps/docs/source.config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { execFileSync } from 'node:child_process'
import path from 'node:path'
import { remarkGfm } from 'fumadocs-core/mdx-plugins'
import { defineConfig, defineDocs, frontmatterSchema } from 'fumadocs-mdx/config'
import lastModified from 'fumadocs-mdx/plugins/last-modified'
import { curlJsonBodyGrammar } from './lib/shiki-curl-json'
Expand Down Expand Up @@ -66,6 +67,22 @@ export default defineConfig({
lastModified({ versionControl: async (file) => gitLastModified?.get(path.resolve(file)) }),
],
mdxOptions: {
/**
* The processed markdown behind `.md` pages, `llms-full.txt`, and Copy page is serialized by
* remark-gfm, which by default pads every table cell to its column's widest cell — one long
* enum pads a whole table. `tablePipeAlign: false` keeps the table and drops the padding; it
* only affects serializing to markdown, never the rendered page.
*/
remarkPlugins: (plugins) => {
if (!plugins.includes(remarkGfm)) {
throw new Error(
'The fumadocs-mdx preset no longer registers remarkGfm; reapply tablePipeAlign'
)
}
return plugins.map((plugin) =>
plugin === remarkGfm ? [remarkGfm, { tablePipeAlign: false }] : plugin
)
},
/**
* Shiki defaults to `github-light` / `github-dark`, whose blues and purples appear nowhere
* in the product. These themes carry the platform's own token colors instead — see
Expand Down
Loading
Loading