Compare commits

...

99 Commits

Author SHA1 Message Date
Dax 96902d1a10 fix(core): disable WAL on network filesystems 2026-08-18 00:25:11 +00:00
Brendan Allan e55a91eadd refactor(app): use shared server data (#43017) 2026-08-18 08:18:33 +08:00
Dax 2f8fa6afef fix(core): serialize MCP token refresh (#43074) 2026-08-17 19:57:27 -04:00
opencode-agent[bot] 5e11cf5fc9 chore: generate 2026-08-17 23:13:55 +00:00
Aiden Cline f5917726d3 feat(ai): support Vertex request labels (#43129) 2026-08-17 18:12:31 -05:00
Luke Parker eb22768a23 refactor(app): remove V1 client compatibility (#43045) 2026-08-18 08:03:51 +10:00
opencode-agent[bot] 8644db2c2a feat(app): render Mermaid code blocks (#43024)
Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com>
2026-08-18 08:02:06 +10:00
opencode-agent[bot] aca42423d3 chore: generate 2026-08-17 21:55:57 +00:00
Kit Langton d9b81d2233 chore(effect): update to beta.107 (#43109) 2026-08-17 21:54:08 +00:00
Aiden Cline 8fc65b3038 test(ai): cover Gemini parallel signatures (#43114) 2026-08-17 16:02:02 -05:00
opencode-agent[bot] f161c90057 chore: generate 2026-08-17 20:21:54 +00:00
opencode-agent[bot] 728ae7c949 fix(stats): prefer Go catalog pricing (#43120) 2026-08-17 15:20:06 -05:00
Dax 42fc297d30 feat(tui): inherit terminal environment per session (#42957) 2026-08-17 20:18:28 +00:00
Aiden Cline 43dd33842e fix(ai): default Gemini function args (#43111) 2026-08-17 13:43:52 -05:00
opencode-agent[bot] 3d13b6c5b6 chore: generate 2026-08-17 18:14:05 +00:00
Aiden Cline 996b05432f fix(ai): preserve Gemini prompt safety blocks (#43070) 2026-08-17 13:12:51 -05:00
Aiden Cline c53f4cfb09 fix(core): clarify temporary directory guidance (#43095) 2026-08-17 11:50:51 -05:00
Kit Langton 45a49ae32a fix(core): preserve previous V2 database lineage (#43092) 2026-08-17 12:39:02 -04:00
Kit Langton 238ce304a9 fix(tui): expose queued prompt shortcut (#43083) 2026-08-17 12:33:18 -04:00
opencode-agent[bot] cf606660fb fix(core): settle externally signaled shells (#43086)
Co-authored-by: Shoubhit Dash <shoubhit2005@gmail.com>
2026-08-17 21:57:35 +05:30
Kit Langton 759695d87c fix(tui): show full tab numbers (#43081) 2026-08-17 11:30:49 -04:00
Dax Raad f14724dfb1 docs(core): reference CLI configuration 2026-08-17 09:57:50 -04:00
Shoubhit Dash cc53db4406 feat(core): discover vLLM models (#43022) 2026-08-17 17:56:03 +05:30
Shoubhit Dash fa055143ea feat(core): discover Ollama models (#43021) 2026-08-17 15:46:47 +05:30
opencode-agent[bot] 875b28658f fix(tui): preserve scroll position across tabs (#43013)
Co-authored-by: Shoubhit Dash <shoubhit2005@gmail.com>
2026-08-17 13:56:30 +05:30
Brendan Allan 0e022036fb refactor(app): use shared client connection (#43016) 2026-08-17 07:53:03 +00:00
Shoubhit Dash f0ae3b9569 fix(core): activate discovered LM Studio provider (#43015) 2026-08-17 13:10:57 +05:30
opencode-agent[bot] 3b5837d354 chore: generate 2026-08-17 07:35:07 +00:00
Brendan Allan e1ff217e44 refactor(client): share Solid server data (#42999) 2026-08-17 15:33:57 +08:00
opencode-agent[bot] ecda3779fa chore: generate 2026-08-17 07:33:17 +00:00
Shoubhit Dash a3e69a967b feat(core): discover LM Studio models (#42607)
Co-authored-by: Kit Langton <kit.langton@gmail.com>
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: Aiden Cline <rekram1-node@users.noreply.github.com>
2026-08-17 13:02:08 +05:30
opencode-agent[bot] 6359623e24 fix(ui): align settled animated numbers (#42998)
Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com>
2026-08-17 16:53:43 +10:00
Luke Parker e26473f0bc fix(app): show search result counts (#42992) 2026-08-17 16:21:13 +10:00
opencode-agent[bot] 9b4b36dfd0 chore: generate 2026-08-17 06:08:25 +00:00
Luke Parker 100487719b refactor(app): remove legacy layout (#42993) 2026-08-17 06:07:15 +00:00
opencode-agent[bot] 6106cb64c7 chore: generate 2026-08-17 03:35:05 +00:00
opencode-agent[bot] c786ab92d3 test(cli): ignore logs before standalone readiness (#42968)
Co-authored-by: Aiden Cline <rekram1-node@users.noreply.github.com>
2026-08-16 22:33:55 -05:00
Luke Parker c400746dd5 fix(app): render code mode executions (#42949) 2026-08-17 13:03:06 +10:00
opencode-agent[bot] e3ce37899d fix(core): run HTTP hooks for session generate (#42965)
Co-authored-by: Aiden Cline <rekram1-node@users.noreply.github.com>
2026-08-16 21:44:22 -05:00
opencode-agent[bot] c5e58b38a8 fix(app): omit shell prompts when copying commands (#42958)
Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com>
2026-08-17 12:33:43 +10:00
Aiden Cline 1f86a6d3f0 fix(core): restore OpenAI request headers (#42959) 2026-08-16 21:25:48 -05:00
Luke Parker be8f975a99 fix(app): reduce session spinner CPU usage (#42952) 2026-08-17 11:04:02 +10:00
opencode-agent[bot] c7abb340f1 chore: generate 2026-08-16 23:37:10 +00:00
opencode-agent[bot] 45771a7e39 chore(util): log spawned processes (#42948)
Co-authored-by: Dax Raad <d@ironbay.co>
2026-08-16 19:36:02 -04:00
Dax 39810da936 docs: reorganize v2 documentation (#42947) 2026-08-16 19:35:48 -04:00
Luke Parker 01c8bf20f9 fix(app): correct background subagent status (#42944) 2026-08-17 09:20:51 +10:00
opencode-agent[bot] 9241a79dc9 chore: generate 2026-08-16 22:50:26 +00:00
Luke Parker e73fa8f4b7 fix(app): clarify skill timeline presentation (#42945) 2026-08-17 08:49:18 +10:00
opencode-agent[bot] fd2699b4d7 fix(tui): clarify saved permission copy (#41144)
Co-authored-by: Kit Langton <kit.langton@gmail.com>
Co-authored-by: opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com>
2026-08-16 17:14:19 -05:00
Aiden Cline ec10d71f22 fix(core): keep plan mode reminders in sync with session agent (#42652) 2026-08-16 16:01:02 -05:00
opencode-agent[bot] ca589273c7 fix(core): preserve tool-result images for AI SDK providers (#42849)
Co-authored-by: Aiden Cline <rekram1-node@users.noreply.github.com>
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2026-08-16 15:20:37 -05:00
opencode-agent[bot] 75a979ec5c fix(ai): pass media through protocol lowering (#42921)
Co-authored-by: Aiden Cline <rekram1-node@users.noreply.github.com>
2026-08-16 15:18:43 -05:00
Dax Raad d4f10fa9be fix(release): stop advancing next npm tags 2026-08-16 11:24:58 -04:00
opencode-agent[bot] 08dd3f51ed fix(app): space no git status (#42868)
Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com>
2026-08-16 08:14:58 +00:00
Dax Raad 0e99cb987a fix(desktop): use built CLI during beta build 2026-08-16 03:20:15 -04:00
opencode-agent[bot] 7731d1235d fix(cli): clarify debug config output (#42860)
Co-authored-by: Dax Raad <d@ironbay.co>
2026-08-16 02:39:47 -04:00
opencode-agent[bot] 174c0a742c chore: generate 2026-08-16 06:39:30 +00:00
Dax b080f216a5 feat(cli): add native CPU profiling (#42862) 2026-08-16 02:38:23 -04:00
Simon Klee 0d7fe6e074 fix(cli): omit undefined compile executable path 2026-08-16 08:03:11 +02:00
opencode-agent[bot] 01b7b53eeb chore: generate 2026-08-16 05:32:10 +00:00
Dax Raad 613c570a3b feat(cli): add heap snapshot signal 2026-08-16 01:30:44 -04:00
Dax Raad a48d44955e fix(cli): suppress compiled runtime warnings 2026-08-16 01:03:25 -04:00
Dax Raad 8251934007 fix(core): batch initial streamed delta 2026-08-15 19:26:16 -04:00
Dax Raad 42e345e1bc fix(cli): resolve Bun canary compile assets 2026-08-15 18:56:03 -04:00
Dax 6a7d6c5adc refactor(core): use numeric event timestamps (#42828) 2026-08-15 18:48:58 -04:00
Dax Raad 9e22c40fff feat: align V2 beta release channels 2026-08-15 18:40:23 -04:00
opencode-agent[bot] 467722c2f9 fix(app): use tree directory picker everywhere (#42820)
Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com>
2026-08-16 08:19:44 +10:00
Luke Parker d9dfceddf8 fix(app): release virtualized timeline elements (#42825) 2026-08-16 08:19:06 +10:00
Dax 4fee4d7d86 fix(core): batch streamed session deltas 2026-08-15 18:16:01 -04:00
Luke Parker 61d7f942b5 fix(app): show new session header immediately (#42822) 2026-08-16 08:04:28 +10:00
Dax Raad d8e1753330 chore(cli): build binaries with Bun 1.4 2026-08-15 17:51:09 -04:00
opencode-agent[bot] d94d520f45 fix(tui): make running subagents clickable (#42797)
Co-authored-by: Dax Raad <mail@thdxr.com>
2026-08-15 19:54:40 +00:00
Dax Raad fcc1e9c42f docs(core): add MCP setup guidance 2026-08-15 15:13:28 -04:00
opencode-agent[bot] 64b9ba339e docs(console): prohibit abusive multi-account use (#42813)
Co-authored-by: Dax Raad <mail@thdxr.com>
2026-08-15 15:08:52 -04:00
opencode-agent[bot] 73700065bf chore: generate 2026-08-15 18:35:53 +00:00
Kit Langton 07a82a442e fix(core): keep queued work parked after interrupt (#42682) 2026-08-15 18:34:43 +00:00
Kit Langton 55be895e14 fix(tui): scope session picker by directory (#42804) 2026-08-15 18:30:35 +00:00
Kit Langton 58a36a1560 fix(tui): preserve path suffix in prompt footer (#42800) 2026-08-15 18:21:07 +00:00
Dax 3d782efee4 docs(core): fix opencode skill source link (#42792) 2026-08-15 16:37:34 +00:00
opencode-agent[bot] c054bb183e chore: generate 2026-08-15 16:30:19 +00:00
Dax 0d7904c91f refactor(core): model provider activation explicitly (#42791)
Co-authored-by: Michael Hart <mhart@cloudflare.com>
2026-08-15 16:29:09 +00:00
opencode-agent[bot] 75ec0b454c chore: generate 2026-08-15 12:13:32 +00:00
Luke Parker f4baba2824 fix(app): use fixed message page size (#42743) 2026-08-15 22:12:20 +10:00
Luke Parker 79fc74afbf fix(app): show skill names (#42749) 2026-08-15 20:20:53 +10:00
Luke Parker 30dfe5352c fix(app): show read tool filenames (#42740) 2026-08-15 20:05:13 +10:00
Luke Parker f725443e30 fix(core): normalize Windows location keys (#42745) 2026-08-15 19:56:43 +10:00
opencode-agent[bot] cae205a3d9 fix(core): bound fuzzy search memory (#42741)
Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com>
2026-08-15 19:40:24 +10:00
Luke Parker d35c6f04ed fix(ui): add keybind label padding (#42726) 2026-08-15 17:54:31 +10:00
opencode-agent[bot] 4d021b4660 fix(app): fade tab titles beneath close button (#42730)
Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com>
2026-08-15 17:10:56 +10:00
Luke Parker f63f912178 test(app): typecheck complete e2e suite (#42728) 2026-08-15 17:06:58 +10:00
opencode-agent[bot] 6564d1442a chore: generate 2026-08-15 06:56:31 +00:00
Luke Parker a5f3e9e735 fix(app): preserve assistant content order (#42721) 2026-08-15 16:55:22 +10:00
Luke Parker d24a24a2ca fix(app): route global events without directory sentinel (#42719) 2026-08-15 16:47:37 +10:00
Luke Parker 8610d90838 fix(app): project sent messages through inbox events (#42714) 2026-08-15 15:57:46 +10:00
Luke Parker 57b050e9fc fix(desktop): render v2 patch metadata (#42716) 2026-08-15 15:57:34 +10:00
Luke Parker 51091be7e4 fix(desktop): verify updates before install (#42715) 2026-08-15 14:39:01 +10:00
Luke Parker c8584ec0c8 fix(app): derive popular providers from integrations (#42713) 2026-08-15 14:29:19 +10:00
Luke Parker 7301c5e798 fix(desktop): keep staged updates fresh with silent re-checks (#42707) 2026-08-15 14:22:24 +10:00
Luke Parker 41f70bfbb1 fix(app): generate blob ids without crypto.subtle in non-secure contexts (#42706)
Co-authored-by: OpenCode Vite Investigation <opencode-vite-investigation@local>
2026-08-15 14:12:57 +10:00
663 changed files with 18785 additions and 59512 deletions
+10 -3
View File
@@ -1,6 +1,10 @@
name: "Setup Bun" name: "Setup Bun"
description: "Setup Bun with caching and install dependencies" description: "Setup Bun with caching and install dependencies"
inputs: inputs:
bun-version:
description: "Bun version to install instead of the root packageManager version"
required: false
default: ""
install-flags: install-flags:
description: "Additional flags to pass to 'bun install'" description: "Additional flags to pass to 'bun install'"
required: false required: false
@@ -20,19 +24,22 @@ runs:
shell: bash shell: bash
run: | run: |
if [ "$RUNNER_ARCH" = "X64" ]; then if [ "$RUNNER_ARCH" = "X64" ]; then
V=$(node -p "require('./package.json').packageManager.split('@')[1]") V="${{ inputs.bun-version }}"
if [ -z "$V" ]; then V=$(node -p "require('./package.json').packageManager.split('@')[1]"); fi
TAG=$([ "$V" = "canary" ] && echo "canary" || echo "bun-v${V}")
case "$RUNNER_OS" in case "$RUNNER_OS" in
macOS) OS=darwin ;; macOS) OS=darwin ;;
Linux) OS=linux ;; Linux) OS=linux ;;
Windows) OS=windows ;; Windows) OS=windows ;;
esac esac
echo "url=https://github.com/oven-sh/bun/releases/download/bun-v${V}/bun-${OS}-x64-baseline.zip" >> "$GITHUB_OUTPUT" echo "url=https://github.com/oven-sh/bun/releases/download/${TAG}/bun-${OS}-x64-baseline.zip" >> "$GITHUB_OUTPUT"
fi fi
- name: Setup Bun - name: Setup Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with: with:
bun-version-file: ${{ !steps.bun-url.outputs.url && 'package.json' || '' }} bun-version: ${{ !steps.bun-url.outputs.url && inputs.bun-version || '' }}
bun-version-file: ${{ !steps.bun-url.outputs.url && !inputs.bun-version && 'package.json' || '' }}
bun-download-url: ${{ steps.bun-url.outputs.url }} bun-download-url: ${{ steps.bun-url.outputs.url }}
- name: Get cache directory - name: Get cache directory
-37
View File
@@ -1,37 +0,0 @@
name: beta
on:
workflow_dispatch:
schedule:
- cron: "0 * * * *"
jobs:
sync:
runs-on: blacksmith-4vcpu-ubuntu-2404
permissions:
contents: write
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 0
- name: Setup Bun
uses: ./.github/actions/setup-bun
- name: Setup Git Committer
id: setup-git-committer
uses: ./.github/actions/setup-git-committer
with:
opencode-app-id: ${{ vars.OPENCODE_APP_ID }}
opencode-app-secret: ${{ secrets.OPENCODE_APP_SECRET }}
- name: Install OpenCode
run: bun i -g opencode-ai
- name: Sync beta branch
env:
GH_TOKEN: ${{ steps.setup-git-committer.outputs.token }}
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
run: bun script/beta.ts
+23 -5
View File
@@ -7,6 +7,7 @@ on:
- ci - ci
- dev - dev
- beta - beta
- v2
- fix/npm-native-binary-install - fix/npm-native-binary-install
- snapshot-* - snapshot-*
workflow_dispatch: workflow_dispatch:
@@ -32,7 +33,7 @@ permissions:
packages: write packages: write
env: env:
OPENCODE_CHANNEL: ${{ (github.ref_name == 'v2' && 'next') || '' }} OPENCODE_CHANNEL: ${{ (github.ref_name == 'v2' && 'dev') || '' }}
jobs: jobs:
version: version:
@@ -45,6 +46,13 @@ jobs:
- uses: ./.github/actions/setup-bun - uses: ./.github/actions/setup-bun
- name: Deploy update service
if: github.ref_name == 'v2' || github.ref_name == 'beta'
working-directory: packages/updates
run: bun run deploy
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
- name: Setup git committer - name: Setup git committer
id: committer id: committer
uses: ./.github/actions/setup-git-committer uses: ./.github/actions/setup-git-committer
@@ -74,13 +82,15 @@ jobs:
build-cli: build-cli:
needs: version needs: version
runs-on: blacksmith-4vcpu-ubuntu-2404 runs-on: blacksmith-4vcpu-ubuntu-2404
if: github.repository == 'anomalyco/opencode' && github.ref_name != 'beta' if: github.repository == 'anomalyco/opencode'
steps: steps:
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
with: with:
fetch-tags: true fetch-tags: true
- uses: ./.github/actions/setup-bun - uses: ./.github/actions/setup-bun
with:
bun-version: canary # Bun 1.4 until its stable release is published
- name: Setup git committer - name: Setup git committer
id: committer id: committer
@@ -102,6 +112,7 @@ jobs:
id: build id: build
run: ./packages/cli/script/build.ts ${{ (github.ref_name == 'beta' && '--sourcemaps') || '' }} run: ./packages/cli/script/build.ts ${{ (github.ref_name == 'beta' && '--sourcemaps') || '' }}
env: env:
BUN_COMPILE_RELEASE: canary
OPENCODE_VERSION: ${{ needs.version.outputs.version }} OPENCODE_VERSION: ${{ needs.version.outputs.version }}
OPENCODE_RELEASE: ${{ needs.version.outputs.release }} OPENCODE_RELEASE: ${{ needs.version.outputs.release }}
GH_REPO: ${{ needs.version.outputs.repo }} GH_REPO: ${{ needs.version.outputs.repo }}
@@ -185,7 +196,7 @@ jobs:
build-node-cli: build-node-cli:
needs: version needs: version
if: github.repository == 'anomalyco/opencode' && github.ref_name != 'beta' if: github.repository == 'anomalyco/opencode'
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
@@ -335,6 +346,7 @@ jobs:
build-electron: build-electron:
needs: needs:
- version - version
- sign-cli-macos
if: github.repository == 'anomalyco/opencode' && github.ref_name != 'v2' if: github.repository == 'anomalyco/opencode' && github.ref_name != 'v2'
continue-on-error: false continue-on-error: false
env: env:
@@ -373,6 +385,12 @@ jobs:
steps: steps:
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
if: github.ref_name == 'beta'
with:
name: opencode-preview-cli
path: packages/cli/dist
- uses: apple-actions/import-codesign-certs@8f3fb608891dd2244cdab3d69cd68c0d37a7fe93 # v2.0.0 - uses: apple-actions/import-codesign-certs@8f3fb608891dd2244cdab3d69cd68c0d37a7fe93 # v2.0.0
if: runner.os == 'macOS' if: runner.os == 'macOS'
with: with:
@@ -431,6 +449,7 @@ jobs:
OPENCODE_VERSION: ${{ needs.version.outputs.version }} OPENCODE_VERSION: ${{ needs.version.outputs.version }}
OPENCODE_CHANNEL: ${{ (github.ref_name == 'beta' && 'beta') || 'prod' }} OPENCODE_CHANNEL: ${{ (github.ref_name == 'beta' && 'beta') || 'prod' }}
OPENCODE_CLI_TARGET: ${{ matrix.settings.target }} OPENCODE_CLI_TARGET: ${{ matrix.settings.target }}
OPENCODE_CLI_DIST: ${{ (github.ref_name == 'beta' && format('{0}/packages/cli/dist', github.workspace)) || '' }}
- name: Build - name: Build
run: bun run build run: bun run build
@@ -447,6 +466,7 @@ jobs:
VITE_SENTRY_ENVIRONMENT: ${{ (github.ref_name == 'beta' && 'beta') || 'production' }} VITE_SENTRY_ENVIRONMENT: ${{ (github.ref_name == 'beta' && 'beta') || 'production' }}
VITE_SENTRY_RELEASE: desktop@${{ needs.version.outputs.version }} VITE_SENTRY_RELEASE: desktop@${{ needs.version.outputs.version }}
OPENCODE_CLI_TARGET: ${{ matrix.settings.target }} OPENCODE_CLI_TARGET: ${{ matrix.settings.target }}
OPENCODE_CLI_DIST: ${{ github.workspace }}/packages/cli/dist
- name: Package - name: Package
if: needs.version.outputs.release if: needs.version.outputs.release
@@ -569,13 +589,11 @@ jobs:
path: packages/opencode/dist path: packages/opencode/dist
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
if: github.ref_name != 'beta'
with: with:
name: opencode-preview-cli name: opencode-preview-cli
path: packages/cli/dist path: packages/cli/dist
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
if: github.ref_name != 'beta'
with: with:
pattern: opencode-node-cli-* pattern: opencode-node-cli-*
path: packages/cli/dist/node path: packages/cli/dist/node
+4
View File
@@ -80,6 +80,8 @@ jobs:
if: always() if: always()
timeout-minutes: 10 timeout-minutes: 10
working-directory: packages/cli working-directory: packages/cli
env:
NODE_OPTIONS: ${{ runner.os == 'Windows' && '--max-old-space-size=4096' || '' }}
run: | run: |
bun run script/build.ts --single --skip-install bun run script/build.ts --single --skip-install
bun run script/service-smoke.ts bun run script/service-smoke.ts
@@ -94,6 +96,8 @@ jobs:
if: always() if: always()
timeout-minutes: 15 timeout-minutes: 15
working-directory: packages/cli working-directory: packages/cli
env:
NODE_OPTIONS: ${{ runner.os == 'Windows' && '--max-old-space-size=4096' || '' }}
run: | run: |
bun run script/build-node.ts --single --skip-install --outdir=dist/node bun run script/build-node.ts --single --skip-install --outdir=dist/node
bun run script/service-smoke.ts --node bun run script/service-smoke.ts --node
+1 -1
View File
@@ -9,7 +9,7 @@
- Run `bun run dev:live` from a development worktree to test its TUI against the currently elected `opencode2` background server and live sessions. - Run `bun run dev:live` from a development worktree to test its TUI against the currently elected `opencode2` background server and live sessions.
- Pass a directory after the script when needed, for example `bun run dev:live /path/to/project`. - Pass a directory after the script when needed, for example `bun run dev:live /path/to/project`.
- The script discovers the server with `opencode2 service status`, injects its private local credential from `opencode2 service get password`, and uses the `next` TUI storage channel so tabs and other client-local state match the installed client. - The script discovers the server with `opencode2 service status`, injects its private local credential from `opencode2 service get password`, and uses the `dev` TUI storage channel so tabs and other client-local state match the installed client.
- Prefer `dev:live` over plain `bun run dev` for this workflow. An implicit managed-service connection may replace the live server when the worktree client version differs; explicit `--server` warns and continues without replacing it. - Prefer `dev:live` over plain `bun run dev` for this workflow. An implicit managed-service connection may replace the live server when the worktree client version differs; explicit `--server` warns and continues without replacing it.
## V2 TUI Stories ## V2 TUI Stories
+242 -95
View File
@@ -81,7 +81,6 @@
"@solidjs/router": "catalog:", "@solidjs/router": "catalog:",
"@tanstack/solid-query": "5.91.4", "@tanstack/solid-query": "5.91.4",
"@tanstack/solid-virtual": "catalog:", "@tanstack/solid-virtual": "catalog:",
"@thisbeyond/solid-dnd": "0.7.5",
"diff": "catalog:", "diff": "catalog:",
"effect": "catalog:", "effect": "catalog:",
"fuzzysort": "catalog:", "fuzzysort": "catalog:",
@@ -189,12 +188,15 @@
"@types/bun": "catalog:", "@types/bun": "catalog:",
"@typescript/native-preview": "catalog:", "@typescript/native-preview": "catalog:",
"effect": "catalog:", "effect": "catalog:",
"solid-js": "catalog:",
}, },
"peerDependencies": { "peerDependencies": {
"effect": "4.0.0-beta.101", "effect": "4.0.0-beta.107",
"solid-js": ">=1.9.0",
}, },
"optionalPeers": [ "optionalPeers": [
"effect", "effect",
"solid-js",
], ],
}, },
"packages/codemode": { "packages/codemode": {
@@ -518,7 +520,7 @@
"name": "@opencode-ai/http-recorder", "name": "@opencode-ai/http-recorder",
"version": "1.18.15", "version": "1.18.15",
"dependencies": { "dependencies": {
"@effect/platform-node-shared": "4.0.0-beta.101", "@effect/platform-node-shared": "4.0.0-beta.107",
}, },
"devDependencies": { "devDependencies": {
"@effect/platform-node": "catalog:", "@effect/platform-node": "catalog:",
@@ -698,6 +700,7 @@
"fuzzysort": "catalog:", "fuzzysort": "catalog:",
"luxon": "catalog:", "luxon": "catalog:",
"marked": "catalog:", "marked": "catalog:",
"mermaid": "11.16.1",
"morphdom": "2.7.8", "morphdom": "2.7.8",
"motion": "12.34.5", "motion": "12.34.5",
"remeda": "catalog:", "remeda": "catalog:",
@@ -914,7 +917,7 @@
"diff": "catalog:", "diff": "catalog:",
"dompurify": "3.3.1", "dompurify": "3.3.1",
"fuzzysort": "catalog:", "fuzzysort": "catalog:",
"katex": "0.16.27", "katex": "0.16.47",
"luxon": "catalog:", "luxon": "catalog:",
"marked": "catalog:", "marked": "catalog:",
"marked-shiki": "catalog:", "marked-shiki": "catalog:",
@@ -1028,7 +1031,7 @@
"packages/www": { "packages/www": {
"name": "@opencode-ai/www", "name": "@opencode-ai/www",
"dependencies": { "dependencies": {
"blume": "1.1.4", "blume": "1.5.1",
}, },
"devDependencies": { "devDependencies": {
"@astrojs/cloudflare": "14.1.4", "@astrojs/cloudflare": "14.1.4",
@@ -1051,9 +1054,9 @@
], ],
"patchedDependencies": { "patchedDependencies": {
"@pierre/trees@1.0.0-beta.4": "patches/@pierre%2Ftrees@1.0.0-beta.4.patch", "@pierre/trees@1.0.0-beta.4": "patches/@pierre%2Ftrees@1.0.0-beta.4.patch",
"effect@4.0.0-beta.101": "patches/effect@4.0.0-beta.101.patch",
"@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch",
"@tanstack/virtual-core@3.17.3": "patches/@tanstack%2Fvirtual-core@3.17.3.patch", "@tanstack/virtual-core@3.17.3": "patches/@tanstack%2Fvirtual-core@3.17.3.patch",
"drizzle-orm@1.0.0-rc.2": "patches/drizzle-orm@1.0.0-rc.2.patch",
"@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch",
"@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch", "@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch",
"@ai-sdk/mistral@3.0.51": "patches/@ai-sdk%2Fmistral@3.0.51.patch", "@ai-sdk/mistral@3.0.51": "patches/@ai-sdk%2Fmistral@3.0.51.patch",
"@ff-labs/fff-bun@0.10.1": "patches/@ff-labs%2Ffff-bun@0.10.1.patch", "@ff-labs/fff-bun@0.10.1": "patches/@ff-labs%2Ffff-bun@0.10.1.patch",
@@ -1065,6 +1068,7 @@
"@ai-sdk/openai-compatible@2.0.41": "patches/@ai-sdk%2Fopenai-compatible@2.0.41.patch", "@ai-sdk/openai-compatible@2.0.41": "patches/@ai-sdk%2Fopenai-compatible@2.0.41.patch",
}, },
"overrides": { "overrides": {
"@effect/platform-node-shared": "catalog:",
"@opentui/core": "catalog:", "@opentui/core": "catalog:",
"@opentui/keymap": "catalog:", "@opentui/keymap": "catalog:",
"@opentui/solid": "catalog:", "@opentui/solid": "catalog:",
@@ -1075,9 +1079,10 @@
"catalog": { "catalog": {
"@cloudflare/workers-types": "4.20251008.0", "@cloudflare/workers-types": "4.20251008.0",
"@corvu/drawer": "0.2.4", "@corvu/drawer": "0.2.4",
"@effect/opentelemetry": "4.0.0-beta.101", "@effect/opentelemetry": "4.0.0-beta.107",
"@effect/platform-node": "4.0.0-beta.101", "@effect/platform-node": "4.0.0-beta.107",
"@effect/sql-sqlite-bun": "4.0.0-beta.101", "@effect/platform-node-shared": "4.0.0-beta.107",
"@effect/sql-sqlite-bun": "4.0.0-beta.107",
"@hono/standard-validator": "0.2.0", "@hono/standard-validator": "0.2.0",
"@hono/zod-validator": "0.4.2", "@hono/zod-validator": "0.4.2",
"@kobalte/core": "0.13.11", "@kobalte/core": "0.13.11",
@@ -1114,7 +1119,7 @@
"dompurify": "3.3.1", "dompurify": "3.3.1",
"drizzle-kit": "1.0.0-rc.2", "drizzle-kit": "1.0.0-rc.2",
"drizzle-orm": "1.0.0-rc.2", "drizzle-orm": "1.0.0-rc.2",
"effect": "4.0.0-beta.101", "effect": "4.0.0-beta.107",
"fuzzysort": "3.1.0", "fuzzysort": "3.1.0",
"get-east-asian-width": "1.6.0", "get-east-asian-width": "1.6.0",
"hono": "4.10.7", "hono": "4.10.7",
@@ -1289,6 +1294,12 @@
"@astrojs/yaml2ts": ["@astrojs/yaml2ts@0.2.4", "", { "dependencies": { "yaml": "^2.8.3" } }, "sha512-8oddpOae35pJsXPQXhTkM0ypfKPskVsh2bCxRtbf7e+/Epw2nReakFYpLKjZMEr75CsoF203PMnCocpfz0s69A=="], "@astrojs/yaml2ts": ["@astrojs/yaml2ts@0.2.4", "", { "dependencies": { "yaml": "^2.8.3" } }, "sha512-8oddpOae35pJsXPQXhTkM0ypfKPskVsh2bCxRtbf7e+/Epw2nReakFYpLKjZMEr75CsoF203PMnCocpfz0s69A=="],
"@asyncapi/converter": ["@asyncapi/converter@2.0.2", "", { "dependencies": { "@asyncapi/parser": "^3.6.2", "js-yaml": "^3.14.1", "path": "^0.12.7" } }, "sha512-tFvT2ijEriTe9CPCMP32fh2VPzdQiUupK63MyW7UBDCtENANPZJ0aboZuVPhgBhmqo2TxcHWbZANpmXc5sEFiw=="],
"@asyncapi/parser": ["@asyncapi/parser@3.6.3", "", { "dependencies": { "@asyncapi/specs": "^6.11.1", "@openapi-contrib/openapi-schema-to-json-schema": "~3.2.0", "@stoplight/json": "3.21.0", "@stoplight/json-ref-readers": "^1.2.2", "@stoplight/json-ref-resolver": "^3.1.5", "@stoplight/spectral-core": "^1.18.3", "@stoplight/spectral-functions": "^1.7.2", "@stoplight/spectral-parsers": "^1.0.2", "@stoplight/spectral-ref-resolver": "^1.0.3", "@stoplight/types": "^13.12.0", "@types/json-schema": "^7.0.11", "@types/urijs": "^1.19.19", "ajv": "^8.18.0", "ajv-errors": "^3.0.0", "ajv-formats": "^2.1.1", "avsc": "^5.7.5", "js-yaml": "^4.3.1", "jsonpath-plus": "^10.0.7", "node-fetch": "2.6.7" } }, "sha512-MUC8xIUMcS2qNvqrqyx/ie0txu3d/OdIsrXs7UCzawdyR6P07gh35DpOqPz/z57s1UA3vERVpcheZYl3h8cVtw=="],
"@asyncapi/specs": ["@asyncapi/specs@6.11.1", "", { "dependencies": { "@types/json-schema": "^7.0.11" } }, "sha512-A3WBLqAKGoJ2+6FWFtpjBlCQ1oFCcs4GxF7zsIGvNqp/klGUHjlA3aAcZ9XMMpLGE8zPeYDz2x9FmO6DSuKraQ=="],
"@aws-crypto/crc32": ["@aws-crypto/crc32@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg=="], "@aws-crypto/crc32": ["@aws-crypto/crc32@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg=="],
"@aws-crypto/crc32c": ["@aws-crypto/crc32c@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag=="], "@aws-crypto/crc32c": ["@aws-crypto/crc32c@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag=="],
@@ -1555,13 +1566,13 @@
"@drizzle-team/brocli": ["@drizzle-team/brocli@0.11.0", "", {}, "sha512-hD3pekGiPg0WPCCGAZmusBBJsDqGUR66Y452YgQsZOnkdQ7ViEPKuyP4huUGEZQefp8g34RRodXYmJ2TbCH+tg=="], "@drizzle-team/brocli": ["@drizzle-team/brocli@0.11.0", "", {}, "sha512-hD3pekGiPg0WPCCGAZmusBBJsDqGUR66Y452YgQsZOnkdQ7ViEPKuyP4huUGEZQefp8g34RRodXYmJ2TbCH+tg=="],
"@effect/opentelemetry": ["@effect/opentelemetry@4.0.0-beta.101", "", { "peerDependencies": { "@opentelemetry/api": "^1.9", "@opentelemetry/api-logs": ">=0.203.0 <0.300.0", "@opentelemetry/resources": "^2.0.0", "@opentelemetry/sdk-logs": ">=0.203.0 <0.300.0", "@opentelemetry/sdk-metrics": "^2.0.0", "@opentelemetry/sdk-trace-base": "^2.0.0", "@opentelemetry/sdk-trace-node": "^2.0.0", "@opentelemetry/sdk-trace-web": "^2.0.0", "@opentelemetry/semantic-conventions": "^1.33.0", "effect": "^4.0.0-beta.101" }, "optionalPeers": ["@opentelemetry/api", "@opentelemetry/api-logs", "@opentelemetry/resources", "@opentelemetry/sdk-logs", "@opentelemetry/sdk-metrics", "@opentelemetry/sdk-trace-base", "@opentelemetry/sdk-trace-node", "@opentelemetry/sdk-trace-web"] }, "sha512-IdejlqRLbjRHJgVnea4s8CxTWfvkSjM0HlnpNfP07IGTbhmAvPs7PMaWt1xzYWbOnI7CriQwxv+54eW1PwIkZg=="], "@effect/opentelemetry": ["@effect/opentelemetry@4.0.0-beta.107", "", { "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <2.0.0", "@opentelemetry/api-logs": ">=0.203.0 <0.300.0", "@opentelemetry/resources": ">=2.0.0 <3.0.0", "@opentelemetry/sdk-logs": ">=0.203.0 <0.300.0", "@opentelemetry/sdk-metrics": ">=2.0.0 <3.0.0", "@opentelemetry/sdk-trace-base": ">=2.0.0 <3.0.0", "@opentelemetry/sdk-trace-node": ">=2.0.0 <3.0.0", "@opentelemetry/sdk-trace-web": ">=2.0.0 <3.0.0", "@opentelemetry/semantic-conventions": ">=1.33.0 <2.0.0", "effect": "^4.0.0-beta.107" }, "optionalPeers": ["@opentelemetry/api", "@opentelemetry/api-logs", "@opentelemetry/resources", "@opentelemetry/sdk-logs", "@opentelemetry/sdk-metrics", "@opentelemetry/sdk-trace-base", "@opentelemetry/sdk-trace-node", "@opentelemetry/sdk-trace-web"] }, "sha512-WxR3OEcwVtckNYGxvERA4kiS8cb2B46lSWxQw8P6dCCzW0j0VC7hkWyzryJ16MVXfI/5xQHS3r5j9mud+JVvsg=="],
"@effect/platform-node": ["@effect/platform-node@4.0.0-beta.101", "", { "dependencies": { "@effect/platform-node-shared": "^4.0.0-beta.101", "mime": "^4.1.0", "undici": "^8.7.0" }, "peerDependencies": { "effect": "^4.0.0-beta.101", "ioredis": "^5.7.0" } }, "sha512-pClk7dmMtHgM6Byu7CzGfrPvZ1/4BwmrRlCg2Op+iJozMkwVUxq8v4beK8b9SvxsliJjHZFznjvkVLX7LQjBqw=="], "@effect/platform-node": ["@effect/platform-node@4.0.0-beta.107", "", { "dependencies": { "@effect/platform-node-shared": "^4.0.0-beta.107", "mime": "^4.1.0", "undici": "^8.7.0" }, "peerDependencies": { "effect": "^4.0.0-beta.107", "ioredis": ">=5.7.0 <6.0.0" } }, "sha512-k+6YNbV4Ck0L6YXtlgkvEnuP5tlxWD8EeWOrpn46PDqbGEwt4ONpRltTwm3tn2cyBXD0i+2P11cUH/6sdFagTA=="],
"@effect/platform-node-shared": ["@effect/platform-node-shared@4.0.0-beta.101", "", { "dependencies": { "@types/ws": "^8.18.1", "ws": "^8.21.0" }, "peerDependencies": { "effect": "^4.0.0-beta.101" } }, "sha512-g4L7XiyJSNJLJVhlslyg2zBCQsoKQf1y1gd+Yfd+3wD9ymC+m7ymbd/5FGqnT1aXV6E2AwRr4D/R1eyRUikvWQ=="], "@effect/platform-node-shared": ["@effect/platform-node-shared@4.0.0-beta.107", "", { "dependencies": { "@types/ws": "^8.18.1", "ws": "^8.21.0" }, "peerDependencies": { "effect": "^4.0.0-beta.107" } }, "sha512-y6BqcRi86BfTJv+tvDrob4ozYVHxxlHYcn/zIQqZjXI9CvKnkgD6ng+38G1o45c4f2ucU+6HRI9POCmFdMoVGA=="],
"@effect/sql-sqlite-bun": ["@effect/sql-sqlite-bun@4.0.0-beta.101", "", { "peerDependencies": { "effect": "^4.0.0-beta.101" } }, "sha512-s6AC7LXCEjCN+nKegKFY4MOi6bmT1+SLR9YHEYwhY3P5qyQQB4R5yYLgt+3J4EPp5fa3t4FIDdPEUwz9LdKm6g=="], "@effect/sql-sqlite-bun": ["@effect/sql-sqlite-bun@4.0.0-beta.107", "", { "peerDependencies": { "effect": "^4.0.0-beta.107" } }, "sha512-BuSSCUoXz6JSR30wT4Y0ukmJKvIFhm/gROwEeA0nLO5QYf33CdTunwX+q35+/MNOOLT8jL3xaDsz5R5aAF1vYQ=="],
"@electron/asar": ["@electron/asar@3.4.1", "", { "dependencies": { "commander": "^5.0.0", "glob": "^7.1.6", "minimatch": "^3.0.4" }, "bin": { "asar": "bin/asar.js" } }, "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA=="], "@electron/asar": ["@electron/asar@3.4.1", "", { "dependencies": { "commander": "^5.0.0", "glob": "^7.1.6", "minimatch": "^3.0.4" }, "bin": { "asar": "bin/asar.js" } }, "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA=="],
@@ -1799,6 +1810,12 @@
"@js-temporal/polyfill": ["@js-temporal/polyfill@0.5.1", "", { "dependencies": { "jsbi": "^4.3.0" } }, "sha512-hloP58zRVCRSpgDxmqCWJNlizAlUgJFqG2ypq79DCvyv9tHjRYMDOcPFjzfl/A1/YxDvRCZz8wvZvmapQnKwFQ=="], "@js-temporal/polyfill": ["@js-temporal/polyfill@0.5.1", "", { "dependencies": { "jsbi": "^4.3.0" } }, "sha512-hloP58zRVCRSpgDxmqCWJNlizAlUgJFqG2ypq79DCvyv9tHjRYMDOcPFjzfl/A1/YxDvRCZz8wvZvmapQnKwFQ=="],
"@jsep-plugin/assignment": ["@jsep-plugin/assignment@1.3.0", "", { "peerDependencies": { "jsep": "^0.4.0||^1.0.0" } }, "sha512-VVgV+CXrhbMI3aSusQyclHkenWSAm95WaiKrMxRFam3JSUiIaQjoMIw2sEs/OX4XifnqeQUN4DYbJjlA8EfktQ=="],
"@jsep-plugin/regex": ["@jsep-plugin/regex@1.0.4", "", { "peerDependencies": { "jsep": "^0.4.0||^1.0.0" } }, "sha512-q7qL4Mgjs1vByCaTnDFcBnV9HS7GVPJX5vyVoCgZHNSC9rjwIlmbXG5sUuorR5ndfHAIlJ8pVStxvjXHbNvtUg=="],
"@jsep-plugin/ternary": ["@jsep-plugin/ternary@1.1.4", "", { "peerDependencies": { "jsep": "^0.4.0||^1.0.0" } }, "sha512-ck5wiqIbqdMX6WRQztBL7ASDty9YLgJ3sSAK5ZpBzXeySvFGCzIvM6UiAI4hTZ22fEcYQVV/zhUbNscggW+Ukg=="],
"@jsx-email/all": ["@jsx-email/all@2.2.3", "", { "dependencies": { "@jsx-email/body": "1.0.2", "@jsx-email/button": "1.0.4", "@jsx-email/column": "1.0.3", "@jsx-email/container": "1.0.2", "@jsx-email/font": "1.0.3", "@jsx-email/head": "1.0.2", "@jsx-email/heading": "1.0.2", "@jsx-email/hr": "1.0.2", "@jsx-email/html": "1.0.2", "@jsx-email/img": "1.0.2", "@jsx-email/link": "1.0.2", "@jsx-email/markdown": "2.0.4", "@jsx-email/preview": "1.0.2", "@jsx-email/render": "1.1.1", "@jsx-email/row": "1.0.2", "@jsx-email/section": "1.0.2", "@jsx-email/tailwind": "2.4.4", "@jsx-email/text": "1.0.2" }, "peerDependencies": { "react": "^18.2.0" } }, "sha512-OBvLe/hVSQc0LlMSTJnkjFoqs3bmxcC4zpy/5pT5agPCSKMvAKQjzmsc2xJ2wO73jSpRV1K/g38GmvdCfrhSoQ=="], "@jsx-email/all": ["@jsx-email/all@2.2.3", "", { "dependencies": { "@jsx-email/body": "1.0.2", "@jsx-email/button": "1.0.4", "@jsx-email/column": "1.0.3", "@jsx-email/container": "1.0.2", "@jsx-email/font": "1.0.3", "@jsx-email/head": "1.0.2", "@jsx-email/heading": "1.0.2", "@jsx-email/hr": "1.0.2", "@jsx-email/html": "1.0.2", "@jsx-email/img": "1.0.2", "@jsx-email/link": "1.0.2", "@jsx-email/markdown": "2.0.4", "@jsx-email/preview": "1.0.2", "@jsx-email/render": "1.1.1", "@jsx-email/row": "1.0.2", "@jsx-email/section": "1.0.2", "@jsx-email/tailwind": "2.4.4", "@jsx-email/text": "1.0.2" }, "peerDependencies": { "react": "^18.2.0" } }, "sha512-OBvLe/hVSQc0LlMSTJnkjFoqs3bmxcC4zpy/5pT5agPCSKMvAKQjzmsc2xJ2wO73jSpRV1K/g38GmvdCfrhSoQ=="],
"@jsx-email/body": ["@jsx-email/body@1.0.2", "", { "peerDependencies": { "react": "^18.2.0" } }, "sha512-NjR2tgLH4XGfGkm+O8kcVwi9MBqZsXZCLlmk3HlMux3/n/+a5zB+yhJqXWZBJl2i+6cSF+E2O6hK11ekyK9WWQ=="], "@jsx-email/body": ["@jsx-email/body@1.0.2", "", { "peerDependencies": { "react": "^18.2.0" } }, "sha512-NjR2tgLH4XGfGkm+O8kcVwi9MBqZsXZCLlmk3HlMux3/n/+a5zB+yhJqXWZBJl2i+6cSF+E2O6hK11ekyK9WWQ=="],
@@ -1985,6 +2002,8 @@
"@one-ini/wasm": ["@one-ini/wasm@0.1.1", "", {}, "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw=="], "@one-ini/wasm": ["@one-ini/wasm@0.1.1", "", {}, "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw=="],
"@openapi-contrib/openapi-schema-to-json-schema": ["@openapi-contrib/openapi-schema-to-json-schema@3.2.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3" } }, "sha512-Gj6C0JwCr8arj0sYuslWXUBSP/KnUlEGnPW4qxlXvAl543oaNQgMgIgkQUA6vs5BCCvwTEiL8m/wdWzfl4UvSw=="],
"@openauthjs/openauth": ["@openauthjs/openauth@0.0.0-20250322224806", "", { "dependencies": { "@standard-schema/spec": "1.0.0-beta.3", "aws4fetch": "1.0.20", "jose": "5.9.6" }, "peerDependencies": { "arctic": "^2.2.2", "hono": "^4.0.0" } }, "sha512-p5IWSRXvABcwocH2dNI0w8c1QJelIOFulwhKk+aLLFfUbs8u1pr7kQbYe8yCSM2+bcLHiwbogpUQc2ovrGwCuw=="], "@openauthjs/openauth": ["@openauthjs/openauth@0.0.0-20250322224806", "", { "dependencies": { "@standard-schema/spec": "1.0.0-beta.3", "aws4fetch": "1.0.20", "jose": "5.9.6" }, "peerDependencies": { "arctic": "^2.2.2", "hono": "^4.0.0" } }, "sha512-p5IWSRXvABcwocH2dNI0w8c1QJelIOFulwhKk+aLLFfUbs8u1pr7kQbYe8yCSM2+bcLHiwbogpUQc2ovrGwCuw=="],
"@opencode-ai/ai": ["@opencode-ai/ai@workspace:packages/ai"], "@opencode-ai/ai": ["@opencode-ai/ai@workspace:packages/ai"],
@@ -2585,6 +2604,8 @@
"@scalar/validation": ["@scalar/validation@0.6.2", "", {}, "sha512-Sc1TkcwGV6aVCO51AyKeaGiP8gpwAHxEtO5d3tZzPV+KsnlC/YokQxFxwBrbIXw73k9hmcExnJyGu3k5i6n6VA=="], "@scalar/validation": ["@scalar/validation@0.6.2", "", {}, "sha512-Sc1TkcwGV6aVCO51AyKeaGiP8gpwAHxEtO5d3tZzPV+KsnlC/YokQxFxwBrbIXw73k9hmcExnJyGu3k5i6n6VA=="],
"@scarf/scarf": ["@scarf/scarf@1.4.0", "", {}, "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ=="],
"@selderee/plugin-htmlparser2": ["@selderee/plugin-htmlparser2@0.11.0", "", { "dependencies": { "domhandler": "^5.0.3", "selderee": "^0.11.0" } }, "sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ=="], "@selderee/plugin-htmlparser2": ["@selderee/plugin-htmlparser2@0.11.0", "", { "dependencies": { "domhandler": "^5.0.3", "selderee": "^0.11.0" } }, "sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ=="],
"@sentry-internal/browser-utils": ["@sentry-internal/browser-utils@10.36.0", "", { "dependencies": { "@sentry/core": "10.36.0" } }, "sha512-WILVR8HQBWOxbqLRuTxjzRCMIACGsDTo6jXvzA8rz6ezElElLmIrn3CFAswrESLqEEUa4CQHl5bLgSVJCRNweA=="], "@sentry-internal/browser-utils": ["@sentry-internal/browser-utils@10.36.0", "", { "dependencies": { "@sentry/core": "10.36.0" } }, "sha512-WILVR8HQBWOxbqLRuTxjzRCMIACGsDTo6jXvzA8rz6ezElElLmIrn3CFAswrESLqEEUa4CQHl5bLgSVJCRNweA=="],
@@ -2807,6 +2828,36 @@
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@stoplight/better-ajv-errors": ["@stoplight/better-ajv-errors@1.0.3", "", { "dependencies": { "jsonpointer": "^5.0.0", "leven": "^3.1.0" }, "peerDependencies": { "ajv": ">=8" } }, "sha512-0p9uXkuB22qGdNfy3VeEhxkU5uwvp/KrBTAbrLBURv6ilxIVwanKwjMc41lQfIVgPGcOkmLbTolfFrSsueu7zA=="],
"@stoplight/json": ["@stoplight/json@3.21.0", "", { "dependencies": { "@stoplight/ordered-object-literal": "^1.0.3", "@stoplight/path": "^1.3.2", "@stoplight/types": "^13.6.0", "jsonc-parser": "~2.2.1", "lodash": "^4.17.21", "safe-stable-stringify": "^1.1" } }, "sha512-5O0apqJ/t4sIevXCO3SBN9AHCEKKR/Zb4gaj7wYe5863jme9g02Q0n/GhM7ZCALkL+vGPTe4ZzTETP8TFtsw3g=="],
"@stoplight/json-ref-readers": ["@stoplight/json-ref-readers@1.2.2", "", { "dependencies": { "node-fetch": "^2.6.0", "tslib": "^1.14.1" } }, "sha512-nty0tHUq2f1IKuFYsLM4CXLZGHdMn+X/IwEUIpeSOXt0QjMUbL0Em57iJUDzz+2MkWG83smIigNZ3fauGjqgdQ=="],
"@stoplight/json-ref-resolver": ["@stoplight/json-ref-resolver@3.1.6", "", { "dependencies": { "@stoplight/json": "^3.21.0", "@stoplight/path": "^1.3.2", "@stoplight/types": "^12.3.0 || ^13.0.0", "@types/urijs": "^1.19.19", "dependency-graph": "~0.11.0", "fast-memoize": "^2.5.2", "immer": "^9.0.6", "lodash": "^4.17.21", "tslib": "^2.6.0", "urijs": "^1.19.11" } }, "sha512-YNcWv3R3n3U6iQYBsFOiWSuRGE5su1tJSiX6pAPRVk7dP0L7lqCteXGzuVRQ0gMZqUl8v1P0+fAKxF6PLo9B5A=="],
"@stoplight/ordered-object-literal": ["@stoplight/ordered-object-literal@1.0.5", "", {}, "sha512-COTiuCU5bgMUtbIFBuyyh2/yVVzlr5Om0v5utQDgBCuQUOPgU1DwoffkTfg4UBQOvByi5foF4w4T+H9CoRe5wg=="],
"@stoplight/path": ["@stoplight/path@1.3.2", "", {}, "sha512-lyIc6JUlUA8Ve5ELywPC8I2Sdnh1zc1zmbYgVarhXIp9YeAB0ReeqmGEOWNtlHkbP2DAA1AL65Wfn2ncjK/jtQ=="],
"@stoplight/spectral-core": ["@stoplight/spectral-core@1.23.1", "", { "dependencies": { "@scarf/scarf": "^1.4.0", "@stoplight/better-ajv-errors": "1.0.3", "@stoplight/json": "~3.21.0", "@stoplight/path": "1.3.2", "@stoplight/spectral-parsers": "^1.0.0", "@stoplight/spectral-ref-resolver": "^1.0.4", "@stoplight/spectral-runtime": "^1.1.2", "@stoplight/types": "~13.6.0", "@types/es-aggregate-error": "^1.0.2", "@types/json-schema": "^7.0.11", "ajv": "^8.18.0", "ajv-errors": "~3.0.0", "ajv-formats": "~2.1.1", "es-aggregate-error": "^1.0.7", "expr-eval-fork": "^3.0.1", "jsonpath-plus": "^10.3.0", "lodash": "^4.18.1", "lodash.topath": "^4.5.2", "minimatch": "^3.1.4", "nimma": "0.2.3", "pony-cause": "^1.1.1", "tslib": "^2.8.1" } }, "sha512-VLC8OhpO/pMJKb6IHhurxJjXO1qB56Ng1unIb8b+hNxdw0+SEcASvmR+RpjfHYX/jv/DfSaA1x8QhFBJBmqBOQ=="],
"@stoplight/spectral-formats": ["@stoplight/spectral-formats@1.8.5", "", { "dependencies": { "@scarf/scarf": "^1.4.0", "@stoplight/json": "^3.17.0", "@stoplight/spectral-core": "^1.23.0", "@types/json-schema": "^7.0.7", "tslib": "^2.8.1" } }, "sha512-xaC0rCH0p7/bzNJsz+JgLSj+Cp6uwYGWpePQxdLkF2G6a8Zyp3OyS7umkGYNiimEwKrOjvCNNTFJpeuiENZSBA=="],
"@stoplight/spectral-functions": ["@stoplight/spectral-functions@1.10.5", "", { "dependencies": { "@scarf/scarf": "^1.4.0", "@stoplight/better-ajv-errors": "1.0.3", "@stoplight/json": "^3.17.1", "@stoplight/spectral-core": "^1.23.0", "@stoplight/spectral-formats": "^1.8.1", "@stoplight/spectral-runtime": "^1.1.2", "ajv": "^8.18.0", "ajv-draft-04": "~1.0.0", "ajv-errors": "~3.0.0", "ajv-formats": "~2.1.1", "lodash": "^4.18.1", "tslib": "^2.8.1" } }, "sha512-vDCd0NJ93715bcUpZZ5vNHiyxd4cgHF6tuXsDiXOXKAByg+I1fR5/dMijEo6Ce1Lz95a+RZ22JKYhF1YuzVvuA=="],
"@stoplight/spectral-parsers": ["@stoplight/spectral-parsers@1.0.5", "", { "dependencies": { "@stoplight/json": "~3.21.0", "@stoplight/types": "^14.1.1", "@stoplight/yaml": "~4.3.0", "tslib": "^2.8.1" } }, "sha512-ANDTp2IHWGvsQDAY85/jQi9ZrF4mRrA5bciNHX+PUxPr4DwS6iv4h+FVWJMVwcEYdpyoIdyL+SRmHdJfQEPmwQ=="],
"@stoplight/spectral-ref-resolver": ["@stoplight/spectral-ref-resolver@1.0.5", "", { "dependencies": { "@stoplight/json-ref-readers": "1.2.2", "@stoplight/json-ref-resolver": "~3.1.6", "@stoplight/spectral-runtime": "^1.1.2", "dependency-graph": "0.11.0", "tslib": "^2.8.1" } }, "sha512-gj3TieX5a9zMW29z3mBlAtDOCgN3GEc1VgZnCVlr5irmR4Qi5LuECuFItAq4pTn5Zu+sW5bqutsCH7D4PkpyAA=="],
"@stoplight/spectral-runtime": ["@stoplight/spectral-runtime@1.1.6", "", { "dependencies": { "@stoplight/json": "^3.20.1", "@stoplight/path": "^1.3.2", "@stoplight/types": "^13.6.0", "lodash": "^4.18.1", "node-fetch": "^2.7.0", "tslib": "^2.8.1" } }, "sha512-Y8rEDyMN4bSMJCrDs2shdcVHYyCnH3FvXRP4dBhha4Z8iJv+JPp7KqOV/hwVB/hWFC209upiwj2oDmLfR0qCDg=="],
"@stoplight/types": ["@stoplight/types@13.20.0", "", { "dependencies": { "@types/json-schema": "^7.0.4", "utility-types": "^3.10.0" } }, "sha512-2FNTv05If7ib79VPDA/r9eUet76jewXFH2y2K5vuge6SXbRHtWBhcaRmu+6QpF4/WRNoJj5XYRSwLGXDxysBGA=="],
"@stoplight/yaml": ["@stoplight/yaml@4.3.0", "", { "dependencies": { "@stoplight/ordered-object-literal": "^1.0.5", "@stoplight/types": "^14.1.1", "@stoplight/yaml-ast-parser": "0.0.50", "tslib": "^2.2.0" } }, "sha512-JZlVFE6/dYpP9tQmV0/ADfn32L9uFarHWxfcRhReKUnljz1ZiUM5zpX+PH8h5CJs6lao3TuFqnPm9IJJCEkE2w=="],
"@stoplight/yaml-ast-parser": ["@stoplight/yaml-ast-parser@0.0.50", "", {}, "sha512-Pb6M8TDO9DtSVla9yXSTAxmo9GVEouq5P40DWXdOie69bXogZTkgvopCq+yEvTMA0F6PEvdJmbtTV3ccIp11VQ=="],
"@storybook/addon-a11y": ["@storybook/addon-a11y@10.5.7", "", { "dependencies": { "@storybook/global": "^5.0.0", "axe-core": "^4.2.0" }, "peerDependencies": { "storybook": "^10.5.7" } }, "sha512-I30rsNz6aA3xg3811MEry40uJDHP3l5SOkfqtmNkp7y4NdqTDdKhmbhhDAZQX1WWEk6GeMBGr3AJ3TCz7r7JmQ=="], "@storybook/addon-a11y": ["@storybook/addon-a11y@10.5.7", "", { "dependencies": { "@storybook/global": "^5.0.0", "axe-core": "^4.2.0" }, "peerDependencies": { "storybook": "^10.5.7" } }, "sha512-I30rsNz6aA3xg3811MEry40uJDHP3l5SOkfqtmNkp7y4NdqTDdKhmbhhDAZQX1WWEk6GeMBGr3AJ3TCz7r7JmQ=="],
"@storybook/addon-docs": ["@storybook/addon-docs@10.5.7", "", { "dependencies": { "@mdx-js/react": "^3.0.0", "@storybook/csf-plugin": "10.5.7", "@storybook/icons": "^2.0.2", "@storybook/react-dom-shim": "10.5.7", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "ts-dedent": "^2.0.0" }, "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "storybook": "^10.5.7" }, "optionalPeers": ["@types/react"] }, "sha512-KNARJfjICaizinsR3INMEiipZm1ObYo+xw+E26gteu50Bcy2dIZUtk5uHY5XdtardU3AXX6yRXoBZ2HCY3lbHA=="], "@storybook/addon-docs": ["@storybook/addon-docs@10.5.7", "", { "dependencies": { "@mdx-js/react": "^3.0.0", "@storybook/csf-plugin": "10.5.7", "@storybook/icons": "^2.0.2", "@storybook/react-dom-shim": "10.5.7", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "ts-dedent": "^2.0.0" }, "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "storybook": "^10.5.7" }, "optionalPeers": ["@types/react"] }, "sha512-KNARJfjICaizinsR3INMEiipZm1ObYo+xw+E26gteu50Bcy2dIZUtk5uHY5XdtardU3AXX6yRXoBZ2HCY3lbHA=="],
@@ -2907,8 +2958,6 @@
"@testing-library/user-event": ["@testing-library/user-event@14.6.3", "", { "peerDependencies": { "@testing-library/dom": ">=7.21.4" } }, "sha512-6dBq67jT8lE+JTE8Exm02Kt6ze43hz1jdiSpSJwtTZiT1xQQ6b7nZYTTQ9njdArdU8XklOwaDp/AbT/eYSKF4g=="], "@testing-library/user-event": ["@testing-library/user-event@14.6.3", "", { "peerDependencies": { "@testing-library/dom": ">=7.21.4" } }, "sha512-6dBq67jT8lE+JTE8Exm02Kt6ze43hz1jdiSpSJwtTZiT1xQQ6b7nZYTTQ9njdArdU8XklOwaDp/AbT/eYSKF4g=="],
"@thisbeyond/solid-dnd": ["@thisbeyond/solid-dnd@0.7.5", "", { "peerDependencies": { "solid-js": "^1.5" } }, "sha512-DfI5ff+yYGpK9M21LhYwIPlbP2msKxN2ARwuu6GF8tT1GgNVDTI8VCQvH4TJFoVApP9d44izmAcTh/iTCH2UUw=="],
"@tsconfig/bun": ["@tsconfig/bun@1.0.9", "", {}, "sha512-4M0/Ivfwcpz325z6CwSifOBZYji3DFOEpY6zEUt0+Xi2qRhzwvmqQN9XAHJh3OVvRJuAqVTLU2abdCplvp6mwQ=="], "@tsconfig/bun": ["@tsconfig/bun@1.0.9", "", {}, "sha512-4M0/Ivfwcpz325z6CwSifOBZYji3DFOEpY6zEUt0+Xi2qRhzwvmqQN9XAHJh3OVvRJuAqVTLU2abdCplvp6mwQ=="],
"@tsconfig/node22": ["@tsconfig/node22@22.0.2", "", {}, "sha512-Kmwj4u8sDRDrMYRoN9FDEcXD8UpBSaPQQ24Gz+Gamqfm7xxn+GBR7ge/Z7pK8OXNGyUzbSwJj+TH6B+DS/epyA=="], "@tsconfig/node22": ["@tsconfig/node22@22.0.2", "", {}, "sha512-Kmwj4u8sDRDrMYRoN9FDEcXD8UpBSaPQQ24Gz+Gamqfm7xxn+GBR7ge/Z7pK8OXNGyUzbSwJj+TH6B+DS/epyA=="],
@@ -3023,6 +3072,8 @@
"@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="], "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="],
"@types/es-aggregate-error": ["@types/es-aggregate-error@1.0.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-qJ7LIFp06h1QE1aVxbVd+zJP2wdaugYXYfd6JxsyRMrYHaxb6itXPogW2tz+ylUJ1n1b+JF1PHyYCfYHm0dvUg=="],
"@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="], "@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="],
"@types/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="], "@types/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="],
@@ -3127,6 +3178,8 @@
"@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
"@types/urijs": ["@types/urijs@1.19.26", "", {}, "sha512-wkXrVzX5yoqLnndOwFsieJA7oKM8cNkOKJtf/3vVGSUFkWDKZvFHpIl9Pvqb/T9UsawBBFMTTD8xu7sK5MWuvg=="],
"@types/whatwg-mimetype": ["@types/whatwg-mimetype@3.0.2", "", {}, "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA=="], "@types/whatwg-mimetype": ["@types/whatwg-mimetype@3.0.2", "", {}, "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA=="],
"@types/which": ["@types/which@3.0.4", "", {}, "sha512-liyfuo/106JdlgSchJzXEQCVArk0CvevqPote8F8HgWgJ3dRCcTHgJIsLDuee0kxk/mhbInzIZk3QWSZJ8R+2w=="], "@types/which": ["@types/which@3.0.4", "", {}, "sha512-liyfuo/106JdlgSchJzXEQCVArk0CvevqPote8F8HgWgJ3dRCcTHgJIsLDuee0kxk/mhbInzIZk3QWSZJ8R+2w=="],
@@ -3213,6 +3266,8 @@
"@webgpu/types": ["@webgpu/types@0.1.54", "", {}, "sha512-81oaalC8LFrXjhsczomEQ0u3jG+TqE6V9QHLA8GNZq/Rnot0KDugu3LhSYSlie8tSdooAN1Hov05asrUUp9qgg=="], "@webgpu/types": ["@webgpu/types@0.1.54", "", {}, "sha512-81oaalC8LFrXjhsczomEQ0u3jG+TqE6V9QHLA8GNZq/Rnot0KDugu3LhSYSlie8tSdooAN1Hov05asrUUp9qgg=="],
"@workflow/serde": ["@workflow/serde@4.1.0", "", {}, "sha512-pav4F2BoirECWR7Nf1TKt+2eETcBj7jj4cBefQ8VXQCA6NPkaKeLfj/zMgi+3zYV5ZIBT4GuUiphsj0/b9hPQQ=="],
"@xmldom/xmldom": ["@xmldom/xmldom@0.8.13", "", {}, "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw=="], "@xmldom/xmldom": ["@xmldom/xmldom@0.8.13", "", {}, "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw=="],
"@yuuang/ffi-rs-android-arm64": ["@yuuang/ffi-rs-android-arm64@1.3.7", "", { "os": "android", "cpu": "arm64" }, "sha512-t6Wx3Xll6c07Nuk0k3xnZsxKFxlshm92i0U/BiTHc6kQbvu+fMJF+gKsj4yEj886jH51CM3EqZT9Xdhq9CdUVw=="], "@yuuang/ffi-rs-android-arm64": ["@yuuang/ffi-rs-android-arm64@1.3.7", "", { "os": "android", "cpu": "arm64" }, "sha512-t6Wx3Xll6c07Nuk0k3xnZsxKFxlshm92i0U/BiTHc6kQbvu+fMJF+gKsj4yEj886jH51CM3EqZT9Xdhq9CdUVw=="],
@@ -3265,6 +3320,8 @@
"ajv-draft-04": ["ajv-draft-04@1.0.0", "", { "peerDependencies": { "ajv": "^8.5.0" }, "optionalPeers": ["ajv"] }, "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw=="], "ajv-draft-04": ["ajv-draft-04@1.0.0", "", { "peerDependencies": { "ajv": "^8.5.0" }, "optionalPeers": ["ajv"] }, "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw=="],
"ajv-errors": ["ajv-errors@3.0.0", "", { "peerDependencies": { "ajv": "^8.0.1" } }, "sha512-V3wD15YHfHz6y0KdhYFjyy9vWtEVALT9UrxfN3zqlI6dMioHnJrqOYfyPKol3oqrnCM9uwkcdCwkJ0WUcbLMTQ=="],
"ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="],
"ajv-i18n": ["ajv-i18n@4.2.0", "", { "peerDependencies": { "ajv": "^8.0.0-beta.0" } }, "sha512-v/ei2UkCEeuKNXh8RToiFsUclmU+G57LO1Oo22OagNMENIw+Yb8eMwvHu7Vn9fmkjJyv6XclhJ8TbuigSglPkg=="], "ajv-i18n": ["ajv-i18n@4.2.0", "", { "peerDependencies": { "ajv": "^8.0.0-beta.0" } }, "sha512-v/ei2UkCEeuKNXh8RToiFsUclmU+G57LO1Oo22OagNMENIw+Yb8eMwvHu7Vn9fmkjJyv6XclhJ8TbuigSglPkg=="],
@@ -3343,6 +3400,8 @@
"available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="], "available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="],
"avsc": ["avsc@5.7.9", "", {}, "sha512-yOA4wFeI7ET3v32Di/sUybQ+ttP20JHSW3mxLuNGeO0uD6PPcvLrIQXSvy/rhJOWU5JrYh7U4OHplWMmtAtjMg=="],
"aws-ssl-profiles": ["aws-ssl-profiles@1.1.2", "", {}, "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g=="], "aws-ssl-profiles": ["aws-ssl-profiles@1.1.2", "", {}, "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g=="],
"aws4": ["aws4@1.13.2", "", {}, "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw=="], "aws4": ["aws4@1.13.2", "", {}, "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw=="],
@@ -3409,7 +3468,7 @@
"bluebird": ["bluebird@3.7.2", "", {}, "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg=="], "bluebird": ["bluebird@3.7.2", "", {}, "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg=="],
"blume": ["blume@1.1.4", "", { "dependencies": { "@astrojs/check": "^0.9.0", "@astrojs/markdown-satteri": "^0.3.2", "@astrojs/mdx": "^7.0.0", "@astrojs/node": "^11.0.0", "@astrojs/react": "^6.0.0", "@astrojs/vercel": "^11.0.0", "@clack/prompts": "^1.7.0", "@iconify-json/lucide": "^1.2.115", "@iconify/types": "^2.0.0", "@iconify/utils": "^3.1.3", "@modelcontextprotocol/sdk": "^1.29.0", "@orama/orama": "^3.1.18", "@pierre/diffs": "^1.2.11", "@scalar/astro": "^0.4.5", "@scalar/openapi-parser": "^0.28.8", "@scalar/openapi-types": "^0.9.1", "@shikijs/transformers": "^4.2.0", "@shikijs/twoslash": "^4.2.0", "@tailwindcss/typography": "^0.5.20", "@tailwindcss/vite": "^4", "@vercel/analytics": "^2.0.1", "ai": "^5.0.0", "astro": "^7.0.2", "babel-plugin-react-compiler": "^1.0.0", "citty": "^0.1.6", "consola": "^3.4.0", "dompurify": "^3.4.11", "epub-gen-memory": "^1.1.2", "github-slugger": "^2.0.0", "gray-matter": "^4.0.3", "jiti": "^2.4.0", "js-yaml": "^4.1.0", "katex": "^0.17.0", "marked": "^18.0.5", "mermaid": "^11.15.0", "node-html-parser": "^9.0.0", "pagefind": "^1.3.0", "pathe": "^2.0.0", "react": "^19.0.0", "react-dom": "^19.0.0", "satteri": "^0.9.5", "shiki": "^4.2.0", "simple-icons": "^13.0.0", "tailwindcss": "^4", "takumi-js": "^2.2.1", "tinyglobby": "^0.2.10", "twoslash": "^0.3.9", "typescript": "^6.0.3", "undici": "^8.6.0", "zod": "^3.24.0" }, "peerDependencies": { "@ai-sdk/openai-compatible": "^1.0.41", "@astrojs/cloudflare": "^14.0.0", "@astrojs/netlify": "^8.0.0", "@astrojs/svelte": "^9.0.0", "@astrojs/vue": "^7.0.0", "@mixedbread/sdk": "^0.76.0", "@notionhq/client": "^2.2.15", "@openrouter/ai-sdk-provider": "^1.5.4", "@oramacloud/client": "^2.1.0", "@sanity/client": "^6.21.0", "algoliasearch": "^5.55.0", "flexsearch": "^0.8.0", "typesense": "^3.0.0" }, "optionalPeers": ["@ai-sdk/openai-compatible", "@astrojs/cloudflare", "@astrojs/netlify", "@astrojs/svelte", "@astrojs/vue", "@mixedbread/sdk", "@notionhq/client", "@openrouter/ai-sdk-provider", "@oramacloud/client", "@sanity/client", "algoliasearch", "flexsearch", "typesense"], "bin": { "blume": "bin/blume.mjs" } }, "sha512-boCWAMfuyc2788hjGJPrmxu4mNUojG+XkkPypjq+z8KErq1yV4XrHgET8bzVJtmOdpWVGLIjfzmbLniTePBU8Q=="], "blume": ["blume@1.5.1", "", { "dependencies": { "@astrojs/check": "^0.9.0", "@astrojs/markdown-satteri": "^0.3.2", "@astrojs/mdx": "^7.0.0", "@astrojs/node": "^11.0.0", "@astrojs/react": "^6.0.0", "@astrojs/vercel": "^11.0.3", "@asyncapi/converter": "^2.0.2", "@clack/prompts": "^1.7.0", "@iconify-json/lucide": "^1.2.115", "@iconify/types": "^2.0.0", "@iconify/utils": "^3.1.3", "@modelcontextprotocol/sdk": "^1.29.0", "@orama/orama": "^3.1.18", "@pierre/diffs": "^1.2.11", "@scalar/astro": "^0.4.5", "@scalar/openapi-parser": "^0.28.8", "@scalar/openapi-types": "^0.9.1", "@shikijs/transformers": "^4.2.0", "@shikijs/twoslash": "^4.2.0", "@tailwindcss/typography": "^0.5.20", "@tailwindcss/vite": "^4", "@types/mdast": "^4.0.4", "@vercel/analytics": "^2.0.1", "ai": "^7.0.42", "astro": "^7.1.0", "babel-plugin-react-compiler": "^1.0.0", "chokidar": "^5.0.0", "citty": "^0.1.6", "consola": "^3.4.0", "cross-spawn": "^7.0.6", "dompurify": "^3.4.13", "dotenv": "^17.4.2", "epub-gen-memory": "^1.1.2", "fast-xml-parser": "^5.10.1", "get-tsconfig": "^4.14.1", "github-slugger": "^2.0.0", "gray-matter": "^4.0.3", "html-escaper": "^3.0.3", "image-size": "^2.0.2", "jiti": "^2.4.0", "js-yaml": "^4.3.1", "katex": "^0.18.1", "markdown-table": "^3.0.4", "marked": "^18.0.5", "mdast-util-from-markdown": "^2.0.3", "mdast-util-gfm": "^3.1.0", "mdast-util-to-string": "^4.0.0", "medium-zoom": "^1.1.0", "mermaid": "^11.16.1", "micromark-extension-gfm": "^3.0.0", "nanotar": "^0.3.0", "node-html-parser": "^9.0.0", "openapi-sampler": "^1.7.4", "p-limit": "^7.3.1", "p-map": "^7.0.6", "p-retry": "^8.0.0", "package-manager-detector": "^1.8.0", "pagefind": "^1.3.0", "pathe": "^2.0.0", "perfect-debounce": "^2.1.0", "picomatch": "^4.0.5", "react": "^19.0.0", "react-dom": "^19.0.0", "robots-parser": "^3.0.1", "satteri": "^0.9.5", "semver": "^7.8.5", "sharp": "^0.35.3", "shiki": "^4.2.0", "simple-icons": "^13.0.0", "string-width": "^8.1.0", "tailwindcss": "^4.3.3", "takumi-js": "^2.2.1", "tinyglobby": "^0.2.10", "twoslash": "^0.3.9", "typescript": "^6.0.3", "ufo": "^1.6.4", "undici": "^8.9.0", "write-file-atomic": "^8.0.0", "zod": "^4.3.6" }, "peerDependencies": { "@ai-sdk/openai-compatible": "^3.0.0", "@astrojs/cloudflare": "^14.0.0", "@astrojs/netlify": "^8.0.0", "@astrojs/svelte": "^9.0.0", "@astrojs/vue": "^7.0.0", "@mixedbread/sdk": "^0.76.0", "@notionhq/client": "^2.2.15", "@openrouter/ai-sdk-provider": "^3.0.0", "@oramacloud/client": "^2.1.0", "@sanity/client": "^6.21.0 || ^7.0.0", "algoliasearch": "^5.55.0", "flexsearch": "^0.8.0", "typesense": "^3.0.0" }, "optionalPeers": ["@ai-sdk/openai-compatible", "@astrojs/cloudflare", "@astrojs/netlify", "@astrojs/svelte", "@astrojs/vue", "@mixedbread/sdk", "@notionhq/client", "@openrouter/ai-sdk-provider", "@oramacloud/client", "@sanity/client", "algoliasearch", "flexsearch", "typesense"], "bin": { "blume": "bin/blume.mjs" } }, "sha512-RnzoY+OHvchUJkXjF7s45SUW5rhuL81URWATEZoEl3psWeajsUu6JRyqDuy8bpYZjoth7P6gpS4IijZspv8Yrw=="],
"body-parser": ["body-parser@2.3.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^2.0.0", "debug": "^4.4.3", "http-errors": "^2.0.1", "iconv-lite": "^0.7.2", "on-finished": "^2.4.1", "qs": "^6.15.2", "raw-body": "^3.0.2", "type-is": "^2.1.0" } }, "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw=="], "body-parser": ["body-parser@2.3.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^2.0.0", "debug": "^4.4.3", "http-errors": "^2.0.1", "iconv-lite": "^0.7.2", "on-finished": "^2.4.1", "qs": "^6.15.2", "raw-body": "^3.0.2", "type-is": "^2.1.0" } }, "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw=="],
@@ -3737,6 +3796,8 @@
"depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
"dependency-graph": ["dependency-graph@0.11.0", "", {}, "sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg=="],
"deprecation": ["deprecation@2.3.1", "", {}, "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ=="], "deprecation": ["deprecation@2.3.1", "", {}, "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ=="],
"dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
@@ -3791,7 +3852,7 @@
"dot-prop": ["dot-prop@8.0.2", "", { "dependencies": { "type-fest": "^3.8.0" } }, "sha512-xaBe6ZT4DHPkg0k4Ytbvn5xoxgpG0jOS1dYxSOwAHPuNLjP3/OzN0gH55SrLqpx8cBfSaVt91lXYkApjb+nYdQ=="], "dot-prop": ["dot-prop@8.0.2", "", { "dependencies": { "type-fest": "^3.8.0" } }, "sha512-xaBe6ZT4DHPkg0k4Ytbvn5xoxgpG0jOS1dYxSOwAHPuNLjP3/OzN0gH55SrLqpx8cBfSaVt91lXYkApjb+nYdQ=="],
"dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], "dotenv": ["dotenv@17.4.2", "", {}, "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw=="],
"dotenv-expand": ["dotenv-expand@11.0.7", "", { "dependencies": { "dotenv": "^16.4.5" } }, "sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA=="], "dotenv-expand": ["dotenv-expand@11.0.7", "", { "dependencies": { "dotenv": "^16.4.5" } }, "sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA=="],
@@ -3813,7 +3874,7 @@
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
"effect": ["effect@4.0.0-beta.101", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.9.0", "find-my-way-ts": "^0.1.6", "ini": "^7.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^2.0.4", "multipasta": "^0.2.8", "toml": "^4.1.2", "uuid": "^14.0.1", "yaml": "^2.9.0" } }, "sha512-HjowumlIo+orthn4jMlEJPuzIYPBV+uq/XiciHWhiedLsXQpWHdNJHO5d59BVDP5s1LPuvERcktwFqRXnJqnhA=="], "effect": ["effect@4.0.0-beta.107", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.9.0", "kubernetes-types": "^1.30.0", "msgpackr": "^2.0.4", "uuid": "^14.0.1" } }, "sha512-OoBAv8eF+yanc+C6xhgEUnWeXUSHA6ynnscYqpkAY9GSnzZWystsIjBowVqCkLpHGlnRtdIqYT3wHwpOY6JDnQ=="],
"ejs": ["ejs@3.1.10", "", { "dependencies": { "jake": "^10.8.5" }, "bin": { "ejs": "bin/cli.js" } }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="], "ejs": ["ejs@3.1.10", "", { "dependencies": { "jake": "^10.8.5" }, "bin": { "ejs": "bin/cli.js" } }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="],
@@ -3877,6 +3938,8 @@
"es-abstract-get": ["es-abstract-get@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "es-object-atoms": "^1.1.2", "is-callable": "^1.2.7", "object-inspect": "^1.13.4" } }, "sha512-6PMWXpdhshVvFp+FoWYs1EvG1Nj0tvk0dZM+XcK0xMEM1czRVcP6ohqPWHy6qPagSpC8j4+p89WXlT+xXJs/fg=="], "es-abstract-get": ["es-abstract-get@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "es-object-atoms": "^1.1.2", "is-callable": "^1.2.7", "object-inspect": "^1.13.4" } }, "sha512-6PMWXpdhshVvFp+FoWYs1EvG1Nj0tvk0dZM+XcK0xMEM1czRVcP6ohqPWHy6qPagSpC8j4+p89WXlT+xXJs/fg=="],
"es-aggregate-error": ["es-aggregate-error@1.0.14", "", { "dependencies": { "define-data-property": "^1.1.4", "define-properties": "^1.2.1", "es-abstract": "^1.24.0", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "globalthis": "^1.0.4", "has-property-descriptors": "^1.0.2", "set-function-name": "^2.0.2" } }, "sha512-3YxX6rVb07B5TV11AV5wsL7nQCHXNwoHPsQC8S4AmBiqYhyNCJ5BRKXkXyDJvs8QzXN20NgRtxe3dEEQD9NLHA=="],
"es-array-method-boxes-properly": ["es-array-method-boxes-properly@1.0.0", "", {}, "sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA=="], "es-array-method-boxes-properly": ["es-array-method-boxes-properly@1.0.0", "", {}, "sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA=="],
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
@@ -3951,6 +4014,8 @@
"exponential-backoff": ["exponential-backoff@3.1.3", "", {}, "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA=="], "exponential-backoff": ["exponential-backoff@3.1.3", "", {}, "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA=="],
"expr-eval-fork": ["expr-eval-fork@3.0.3", "", {}, "sha512-BhC+hbc5lIVjygr840n5DEkW3MQq7H9o+mc1/N7Z5uIiCFVyESLL5DIE7LNq4CYUNxy+XjA+3jRrL/h0Kt2xcg=="],
"express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], "express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="],
"express-rate-limit": ["express-rate-limit@8.6.2", "", { "dependencies": { "debug": "^4.4.3", "ip-address": "^10.2.0" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-YH4ru+eOJxQABscKFfRCy9R7x9QFGdezclVMwwgFFndzS2Xnm0uo6B0ABZsLhcpeptGv2qvuJVWlQr9gQZoC3A=="], "express-rate-limit": ["express-rate-limit@8.6.2", "", { "dependencies": { "debug": "^4.4.3", "ip-address": "^10.2.0" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-YH4ru+eOJxQABscKFfRCy9R7x9QFGdezclVMwwgFFndzS2Xnm0uo6B0ABZsLhcpeptGv2qvuJVWlQr9gQZoC3A=="],
@@ -3977,6 +4042,8 @@
"fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
"fast-memoize": ["fast-memoize@2.5.2", "", {}, "sha512-Ue0LwpDYErFbmNnZSF0UH6eImUwDmogUO1jyE+JbN2gsQz/jICm1Ve7t9QT0rNSsfJt+Hs4/S3GnsDVjL4HVrw=="],
"fast-string-truncated-width": ["fast-string-truncated-width@3.0.3", "", {}, "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g=="], "fast-string-truncated-width": ["fast-string-truncated-width@3.0.3", "", {}, "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g=="],
"fast-string-width": ["fast-string-width@3.0.2", "", { "dependencies": { "fast-string-truncated-width": "^3.0.2" } }, "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg=="], "fast-string-width": ["fast-string-width@3.0.2", "", { "dependencies": { "fast-string-truncated-width": "^3.0.2" } }, "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg=="],
@@ -3987,7 +4054,7 @@
"fast-xml-builder": ["fast-xml-builder@1.3.0", "", { "dependencies": { "path-expression-matcher": "^1.6.2", "xml-naming": "^0.3.0" } }, "sha512-F74cZEdCvuw9P41GAC3rod4X04jjWGM1JPEv/GWSqFTWLsdyMSBMBMlm9Hk3GLBgLBbdBNY8yee0pQh2RBVESQ=="], "fast-xml-builder": ["fast-xml-builder@1.3.0", "", { "dependencies": { "path-expression-matcher": "^1.6.2", "xml-naming": "^0.3.0" } }, "sha512-F74cZEdCvuw9P41GAC3rod4X04jjWGM1JPEv/GWSqFTWLsdyMSBMBMlm9Hk3GLBgLBbdBNY8yee0pQh2RBVESQ=="],
"fast-xml-parser": ["fast-xml-parser@4.4.1", "", { "dependencies": { "strnum": "^1.0.5" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw=="], "fast-xml-parser": ["fast-xml-parser@5.10.1", "", { "dependencies": { "@nodable/entities": "^3.0.0", "fast-xml-builder": "^1.2.0", "is-unsafe": "^2.0.0", "path-expression-matcher": "^1.6.2", "strnum": "^2.4.1", "xml-naming": "^0.3.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-IEMIf7298kXuZSRFoGfMYrl7is8LpavODgbNz1cwIudv7KwVFnuU+UsMporfq6PD6aXSlawZlARiA3UywCTfMw=="],
"fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="], "fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="],
@@ -4009,8 +4076,6 @@
"find-babel-config": ["find-babel-config@2.1.2", "", { "dependencies": { "json5": "^2.2.3" } }, "sha512-ZfZp1rQyp4gyuxqt1ZqjFGVeVBvmpURMqdIWXbPRfB97Bf6BzdK/xSIbylEINzQ0kB5tlDQfn9HkNXXWsqTqLg=="], "find-babel-config": ["find-babel-config@2.1.2", "", { "dependencies": { "json5": "^2.2.3" } }, "sha512-ZfZp1rQyp4gyuxqt1ZqjFGVeVBvmpURMqdIWXbPRfB97Bf6BzdK/xSIbylEINzQ0kB5tlDQfn9HkNXXWsqTqLg=="],
"find-my-way-ts": ["find-my-way-ts@0.1.6", "", {}, "sha512-a85L9ZoXtNAey3Y6Z+eBWW658kO/MwR7zIafkIUPUMf3isZG0NCs2pjW2wtjxAKuJPxMAsHUIP4ZPGv0o5gyTA=="],
"find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="],
"finity": ["finity@0.5.4", "", {}, "sha512-3l+5/1tuw616Lgb0QBimxfdd2TqaDGpfCBpfX6EqtFmqUV3FtQnVEX4Aa62DagYEqnsTIjZcTfbq9msDbXYgyA=="], "finity": ["finity@0.5.4", "", {}, "sha512-3l+5/1tuw616Lgb0QBimxfdd2TqaDGpfCBpfX6EqtFmqUV3FtQnVEX4Aa62DagYEqnsTIjZcTfbq9msDbXYgyA=="],
@@ -4027,6 +4092,8 @@
"for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="], "for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="],
"foreach": ["foreach@2.0.6", "", {}, "sha512-k6GAGDyqLe9JaebCsFCoudPPWfihKu8pylYXRlqP1J7ms39iPoTtk2fviNglIeQEwdh0bQeKJ01ZPyuyQvKzwg=="],
"foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="], "foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="],
"form-data": ["form-data@4.0.6", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.4", "mime-types": "^2.1.35" } }, "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ=="], "form-data": ["form-data@4.0.6", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.4", "mime-types": "^2.1.35" } }, "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ=="],
@@ -4243,6 +4310,8 @@
"ignore-walk": ["ignore-walk@8.0.0", "", { "dependencies": { "minimatch": "^10.0.3" } }, "sha512-FCeMZT4NiRQGh+YkeKMtWrOmBgWjHjMJ26WQWrRQyoyzqevdaGSakUaJW5xQYmjLlUVk2qUnCjYVBax9EKKg8A=="], "ignore-walk": ["ignore-walk@8.0.0", "", { "dependencies": { "minimatch": "^10.0.3" } }, "sha512-FCeMZT4NiRQGh+YkeKMtWrOmBgWjHjMJ26WQWrRQyoyzqevdaGSakUaJW5xQYmjLlUVk2qUnCjYVBax9EKKg8A=="],
"image-size": ["image-size@2.0.2", "", { "bin": { "image-size": "bin/image-size.js" } }, "sha512-IRqXKlaXwgSMAMtpNzZa1ZAe8m+Sa1770Dhk8VkSsP9LS+iHD62Zd8FQKs8fbPiagBE7BzoFX23cxFnwshpV6w=="],
"immediate": ["immediate@3.0.6", "", {}, "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="], "immediate": ["immediate@3.0.6", "", {}, "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="],
"immer": ["immer@11.1.4", "", {}, "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw=="], "immer": ["immer@11.1.4", "", {}, "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw=="],
@@ -4257,7 +4326,7 @@
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
"ini": ["ini@7.0.0", "", {}, "sha512-ifK0CgjALofS5bkrcTy4RaQ9Vx2Knf/eLeIO+NaswQEpH1UblrtTSCIvN71qQDMq0PeQ/SSPojvEJp9vvvfr+w=="], "ini": ["ini@6.0.0", "", {}, "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ=="],
"inline-style-parser": ["inline-style-parser@0.2.7", "", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="], "inline-style-parser": ["inline-style-parser@0.2.7", "", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="],
@@ -4329,6 +4398,8 @@
"is-negative-zero": ["is-negative-zero@2.0.3", "", {}, "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw=="], "is-negative-zero": ["is-negative-zero@2.0.3", "", {}, "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw=="],
"is-network-error": ["is-network-error@1.3.2", "", {}, "sha512-PhBY86zaxNZUuWP6h13Vu5oFe0XY6/UlKzQnYFELzGVHygP3MxmvTfYSG7GN3aIab/iWudSMgjSnG9Dq+nHrgA=="],
"is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],
"is-number-object": ["is-number-object@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw=="], "is-number-object": ["is-number-object@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw=="],
@@ -4401,6 +4472,8 @@
"jsbi": ["jsbi@4.3.2", "", {}, "sha512-9fqMSQbhJykSeii05nxKl4m6Eqn2P6rOlYiS+C5Dr/HPIU/7yZxu5qzbs40tgaFORiw2Amd0mirjxatXYMkIew=="], "jsbi": ["jsbi@4.3.2", "", {}, "sha512-9fqMSQbhJykSeii05nxKl4m6Eqn2P6rOlYiS+C5Dr/HPIU/7yZxu5qzbs40tgaFORiw2Amd0mirjxatXYMkIew=="],
"jsep": ["jsep@1.4.0", "", {}, "sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw=="],
"jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
"json-bigint": ["json-bigint@1.0.0", "", { "dependencies": { "bignumber.js": "^9.0.0" } }, "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ=="], "json-bigint": ["json-bigint@1.0.0", "", { "dependencies": { "bignumber.js": "^9.0.0" } }, "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ=="],
@@ -4409,6 +4482,8 @@
"json-parse-even-better-errors": ["json-parse-even-better-errors@5.0.0", "", {}, "sha512-ZF1nxZ28VhQouRWhUcVlUIN3qwSgPuswK05s/HIaoetAoE/9tngVmCHjSxmSQPav1nd+lPtTL0YZ/2AFdR/iYQ=="], "json-parse-even-better-errors": ["json-parse-even-better-errors@5.0.0", "", {}, "sha512-ZF1nxZ28VhQouRWhUcVlUIN3qwSgPuswK05s/HIaoetAoE/9tngVmCHjSxmSQPav1nd+lPtTL0YZ/2AFdR/iYQ=="],
"json-pointer": ["json-pointer@0.6.2", "", { "dependencies": { "foreach": "^2.0.4" } }, "sha512-vLWcKbOaXlO+jvRy4qNd+TI1QUPZzfJj1tpJ3vAXDych5XJf93ftpUKe5pKCrzyIIwgBJcOcCVRUfqQP25afBw=="],
"json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="], "json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="],
"json-schema-to-ts": ["json-schema-to-ts@3.1.1", "", { "dependencies": { "@babel/runtime": "^7.18.3", "ts-algebra": "^2.0.0" } }, "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g=="], "json-schema-to-ts": ["json-schema-to-ts@3.1.1", "", { "dependencies": { "@babel/runtime": "^7.18.3", "ts-algebra": "^2.0.0" } }, "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g=="],
@@ -4431,6 +4506,8 @@
"jsonparse": ["jsonparse@1.3.1", "", {}, "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg=="], "jsonparse": ["jsonparse@1.3.1", "", {}, "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg=="],
"jsonpath-plus": ["jsonpath-plus@10.4.0", "", { "dependencies": { "@jsep-plugin/assignment": "^1.3.0", "@jsep-plugin/regex": "^1.0.4", "jsep": "^1.4.0" }, "bin": { "jsonpath": "bin/jsonpath-cli.js", "jsonpath-plus": "bin/jsonpath-cli.js" } }, "sha512-T92WWatJXmhBbKsgH/0hl+jxjdXrifi5IKeMY02DWggRxX0UElcbVzPlmgLTbvsPeW1PasQ6xE2Q75stkhGbsA=="],
"jsonpointer": ["jsonpointer@5.0.1", "", {}, "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ=="], "jsonpointer": ["jsonpointer@5.0.1", "", {}, "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ=="],
"jsonwebtoken": ["jsonwebtoken@9.0.3", "", { "dependencies": { "jws": "^4.0.1", "lodash.includes": "^4.3.0", "lodash.isboolean": "^3.0.3", "lodash.isinteger": "^4.0.4", "lodash.isnumber": "^3.0.3", "lodash.isplainobject": "^4.0.6", "lodash.isstring": "^4.0.1", "lodash.once": "^4.0.0", "ms": "^2.1.1", "semver": "^7.5.4" } }, "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g=="], "jsonwebtoken": ["jsonwebtoken@9.0.3", "", { "dependencies": { "jws": "^4.0.1", "lodash.includes": "^4.3.0", "lodash.isboolean": "^3.0.3", "lodash.isinteger": "^4.0.4", "lodash.isnumber": "^3.0.3", "lodash.isplainobject": "^4.0.6", "lodash.isstring": "^4.0.1", "lodash.once": "^4.0.0", "ms": "^2.1.1", "semver": "^7.5.4" } }, "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g=="],
@@ -4447,7 +4524,7 @@
"jwt-decode": ["jwt-decode@3.1.2", "", {}, "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A=="], "jwt-decode": ["jwt-decode@3.1.2", "", {}, "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A=="],
"katex": ["katex@0.16.27", "", { "dependencies": { "commander": "^8.3.0" }, "bin": { "katex": "cli.js" } }, "sha512-aeQoDkuRWSqQN6nSvVCEFvfXdqo1OQiCmmW1kc9xSdjutPv7BGO7pqY9sQRJpMOGrEdfDgF2TfRXe5eUAD2Waw=="], "katex": ["katex@0.16.47", "", { "dependencies": { "commander": "^8.3.0" }, "bin": { "katex": "cli.js" } }, "sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg=="],
"keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
@@ -4529,6 +4606,8 @@
"lodash.once": ["lodash.once@4.1.1", "", {}, "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="], "lodash.once": ["lodash.once@4.1.1", "", {}, "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="],
"lodash.topath": ["lodash.topath@4.5.2", "", {}, "sha512-1/W4dM+35DwvE/iEd1M9ekewOSTlpFekhw9mhAtrwjVqUr83/ilQiyAvmg4tVX7Unkcfl1KC+i9WdaT4B6aQcg=="],
"loglevelnext": ["loglevelnext@6.0.0", "", {}, "sha512-FDl1AI2sJGjHHG3XKJd6sG3/6ncgiGCQ0YkW46nxe7SfqQq6hujd9CvFXIXtkGBUN83KPZ2KSOJK8q5P0bSSRQ=="], "loglevelnext": ["loglevelnext@6.0.0", "", {}, "sha512-FDl1AI2sJGjHHG3XKJd6sG3/6ncgiGCQ0YkW46nxe7SfqQq6hujd9CvFXIXtkGBUN83KPZ2KSOJK8q5P0bSSRQ=="],
"long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="], "long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="],
@@ -4613,6 +4692,8 @@
"media-typer": ["media-typer@1.1.1", "", {}, "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ=="], "media-typer": ["media-typer@1.1.1", "", {}, "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ=="],
"medium-zoom": ["medium-zoom@1.1.0", "", {}, "sha512-ewyDsp7k4InCUp3jRmwHBRFGyjBimKps/AJLjRSox+2q/2H4p/PNpQf+pwONWlJiOudkBXtbdmVbFjqyybfTmQ=="],
"merge-anything": ["merge-anything@5.1.7", "", { "dependencies": { "is-what": "^4.1.8" } }, "sha512-eRtbOb1N5iyH0tkQDAoQ4Ipsp/5qSR79Dzrz8hEPxRX10RWWR/iQXdoKmBSRCThY1Fh5EhISDtpSc93fpxUniQ=="], "merge-anything": ["merge-anything@5.1.7", "", { "dependencies": { "is-what": "^4.1.8" } }, "sha512-eRtbOb1N5iyH0tkQDAoQ4Ipsp/5qSR79Dzrz8hEPxRX10RWWR/iQXdoKmBSRCThY1Fh5EhISDtpSc93fpxUniQ=="],
"merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], "merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="],
@@ -4753,8 +4834,6 @@
"muggle-string": ["muggle-string@0.4.1", "", {}, "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ=="], "muggle-string": ["muggle-string@0.4.1", "", {}, "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ=="],
"multipasta": ["multipasta@0.2.8", "", {}, "sha512-ZPWuMKyv0cSO29f7hozp+k6+crZbQijV8ipMvxNxRf2SwtYGTX1ZX89Kd20VV4H9Znonx+EQn+iy1wGQsJ+b+Q=="],
"mustache": ["mustache@4.2.0", "", { "bin": { "mustache": "bin/mustache" } }, "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ=="], "mustache": ["mustache@4.2.0", "", { "bin": { "mustache": "bin/mustache" } }, "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ=="],
"mysql2": ["mysql2@3.14.4", "", { "dependencies": { "aws-ssl-profiles": "^1.1.1", "denque": "^2.1.0", "generate-function": "^2.3.1", "iconv-lite": "^0.7.0", "long": "^5.2.1", "lru.min": "^1.0.0", "named-placeholders": "^1.1.3", "seq-queue": "^0.0.5", "sqlstring": "^2.3.2" } }, "sha512-Cs/jx3WZPNrYHVz+Iunp9ziahaG5uFMvD2R8Zlmc194AqXNxt9HBNu7ZsPYrUtmJsF0egETCWIdMIYAwOGjL1w=="], "mysql2": ["mysql2@3.14.4", "", { "dependencies": { "aws-ssl-profiles": "^1.1.1", "denque": "^2.1.0", "generate-function": "^2.3.1", "iconv-lite": "^0.7.0", "long": "^5.2.1", "lru.min": "^1.0.0", "named-placeholders": "^1.1.3", "seq-queue": "^0.0.5", "sqlstring": "^2.3.2" } }, "sha512-Cs/jx3WZPNrYHVz+Iunp9ziahaG5uFMvD2R8Zlmc194AqXNxt9HBNu7ZsPYrUtmJsF0egETCWIdMIYAwOGjL1w=="],
@@ -4767,12 +4846,16 @@
"nanoid": ["nanoid@3.3.18", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w=="], "nanoid": ["nanoid@3.3.18", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w=="],
"nanotar": ["nanotar@0.3.0", "", {}, "sha512-Kv2JYYiCzt16Kt5QwAc9BFG89xfPNBx+oQL4GQXD9nLqPkZBiNaqaCWtwnbk/q7UVsTYevvM1b0UF8zmEI4pCg=="],
"negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
"neotraverse": ["neotraverse@0.6.18", "", {}, "sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA=="], "neotraverse": ["neotraverse@0.6.18", "", {}, "sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA=="],
"nf3": ["nf3@0.1.12", "", {}, "sha512-qbMXT7RTGh74MYWPeqTIED8nDW70NXOULVHpdWcdZ7IVHVnAsMV9fNugSNnvooipDc1FMOzpis7T9nXJEbJhvQ=="], "nf3": ["nf3@0.1.12", "", {}, "sha512-qbMXT7RTGh74MYWPeqTIED8nDW70NXOULVHpdWcdZ7IVHVnAsMV9fNugSNnvooipDc1FMOzpis7T9nXJEbJhvQ=="],
"nimma": ["nimma@0.2.3", "", { "dependencies": { "@jsep-plugin/regex": "^1.0.1", "@jsep-plugin/ternary": "^1.0.2", "astring": "^1.8.1", "jsep": "^1.2.0" }, "optionalDependencies": { "jsonpath-plus": "^6.0.1 || ^10.1.0", "lodash.topath": "^4.5.2" } }, "sha512-1ZOI8J+1PKKGceo/5CT5GfQOG6H8I2BencSK06YarZ2wXwH37BSSUWldqJmMJYA5JfqDqffxDXynt6f11AyKcA=="],
"nitro": ["nitro@3.0.1-alpha.1", "", { "dependencies": { "consola": "^3.4.2", "crossws": "^0.4.1", "db0": "^0.3.4", "h3": "2.0.1-rc.5", "jiti": "^2.6.1", "nf3": "^0.1.10", "ofetch": "^2.0.0-alpha.3", "ohash": "^2.0.11", "oxc-minify": "^0.96.0", "oxc-transform": "^0.96.0", "srvx": "^0.9.5", "undici": "^7.16.0", "unenv": "^2.0.0-rc.24", "unstorage": "^2.0.0-alpha.4" }, "peerDependencies": { "rolldown": "*", "rollup": "^4", "vite": "^7", "xml2js": "^0.6.2" }, "optionalPeers": ["rolldown", "rollup", "vite", "xml2js"], "bin": { "nitro": "dist/cli/index.mjs" } }, "sha512-U4AxIsXxdkxzkFrK0XAw0e5Qbojk8jQ50MjjRBtBakC4HurTtQoiZvF+lSe382jhuQZCfAyywGWOFa9QzXLFaw=="], "nitro": ["nitro@3.0.1-alpha.1", "", { "dependencies": { "consola": "^3.4.2", "crossws": "^0.4.1", "db0": "^0.3.4", "h3": "2.0.1-rc.5", "jiti": "^2.6.1", "nf3": "^0.1.10", "ofetch": "^2.0.0-alpha.3", "ohash": "^2.0.11", "oxc-minify": "^0.96.0", "oxc-transform": "^0.96.0", "srvx": "^0.9.5", "undici": "^7.16.0", "unenv": "^2.0.0-rc.24", "unstorage": "^2.0.0-alpha.4" }, "peerDependencies": { "rolldown": "*", "rollup": "^4", "vite": "^7", "xml2js": "^0.6.2" }, "optionalPeers": ["rolldown", "rollup", "vite", "xml2js"], "bin": { "nitro": "dist/cli/index.mjs" } }, "sha512-U4AxIsXxdkxzkFrK0XAw0e5Qbojk8jQ50MjjRBtBakC4HurTtQoiZvF+lSe382jhuQZCfAyywGWOFa9QzXLFaw=="],
"nlcst-to-string": ["nlcst-to-string@4.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0" } }, "sha512-YKLBCcUYKAg0FNlOBT6aI91qFmSiFKiluk655WzPF+DDMA02qIyy8uiRqI8QXtcFpEvll12LpL5MXqEmAZ+dcA=="], "nlcst-to-string": ["nlcst-to-string@4.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0" } }, "sha512-YKLBCcUYKAg0FNlOBT6aI91qFmSiFKiluk655WzPF+DDMA02qIyy8uiRqI8QXtcFpEvll12LpL5MXqEmAZ+dcA=="],
@@ -4861,6 +4944,8 @@
"openai": ["openai@6.49.0", "", { "peerDependencies": { "@aws-sdk/credential-provider-node": ">=3.972.0 <4", "@smithy/hash-node": ">=4.3.0 <5", "@smithy/signature-v4": ">=5.4.0 <6", "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["@aws-sdk/credential-provider-node", "@smithy/hash-node", "@smithy/signature-v4", "ws", "zod"] }, "sha512-aYCc0C6L864eR6WSYIwQGyXriw/nIyZx0ObvhzOEVuk0zoBDpynjSbrionWI7q65B5H8jJX0DXR9snEzM6bfPg=="], "openai": ["openai@6.49.0", "", { "peerDependencies": { "@aws-sdk/credential-provider-node": ">=3.972.0 <4", "@smithy/hash-node": ">=4.3.0 <5", "@smithy/signature-v4": ">=5.4.0 <6", "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["@aws-sdk/credential-provider-node", "@smithy/hash-node", "@smithy/signature-v4", "ws", "zod"] }, "sha512-aYCc0C6L864eR6WSYIwQGyXriw/nIyZx0ObvhzOEVuk0zoBDpynjSbrionWI7q65B5H8jJX0DXR9snEzM6bfPg=="],
"openapi-sampler": ["openapi-sampler@1.7.4", "", { "dependencies": { "@types/json-schema": "^7.0.7", "fast-xml-parser": "^5.5.1", "json-pointer": "0.6.2" } }, "sha512-CKS/rd5ucPCuEDbJnjGDXZTsuGWcmv53aCmQx7soZlPEONUGN4af0/dY5+THRFZraSEjeA78nlfzdFswC/N5SA=="],
"openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="], "openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="],
"openid-client": ["openid-client@5.6.4", "", { "dependencies": { "jose": "^4.15.4", "lru-cache": "^6.0.0", "object-hash": "^2.2.0", "oidc-token-hash": "^5.0.3" } }, "sha512-T1h3B10BRPKfcObdBklX639tVz+xh34O7GjofqrqiAQdm7eHsQ00ih18x6wuJ/E6FxdtS2u3FmUGPDeEcMwzNA=="], "openid-client": ["openid-client@5.6.4", "", { "dependencies": { "jose": "^4.15.4", "lru-cache": "^6.0.0", "object-hash": "^2.2.0", "oidc-token-hash": "^5.0.3" } }, "sha512-T1h3B10BRPKfcObdBklX639tVz+xh34O7GjofqrqiAQdm7eHsQ00ih18x6wuJ/E6FxdtS2u3FmUGPDeEcMwzNA=="],
@@ -4899,7 +4984,7 @@
"p-queue": ["p-queue@8.1.1", "", { "dependencies": { "eventemitter3": "^5.0.1", "p-timeout": "^6.1.2" } }, "sha512-aNZ+VfjobsWryoiPnEApGGmf5WmNsCo9xu8dfaYamG5qaLP7ClhLN6NgsFe6SwJ2UbLEBK5dv9x8Mn5+RVhMWQ=="], "p-queue": ["p-queue@8.1.1", "", { "dependencies": { "eventemitter3": "^5.0.1", "p-timeout": "^6.1.2" } }, "sha512-aNZ+VfjobsWryoiPnEApGGmf5WmNsCo9xu8dfaYamG5qaLP7ClhLN6NgsFe6SwJ2UbLEBK5dv9x8Mn5+RVhMWQ=="],
"p-retry": ["p-retry@4.6.2", "", { "dependencies": { "@types/retry": "0.12.0", "retry": "^0.13.1" } }, "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ=="], "p-retry": ["p-retry@8.0.0", "", { "dependencies": { "is-network-error": "^1.3.0" } }, "sha512-kFVqH1HxOHp8LupNsOys7bSV09VYTRLxarH/mokO4Rqhk6wGi70E0jh4VzvVGXfEVNggHoHLAMWsQqHyU1Ey9A=="],
"p-timeout": ["p-timeout@6.1.4", "", {}, "sha512-MyIV3ZA/PmyBN/ud8vV9XzwTrNtR4jFrObymZYnZqMmW0zA8Z17vnT0rBgFE/TlohB+YCHqXMgZzb3Csp49vqg=="], "p-timeout": ["p-timeout@6.1.4", "", {}, "sha512-MyIV3ZA/PmyBN/ud8vV9XzwTrNtR4jFrObymZYnZqMmW0zA8Z17vnT0rBgFE/TlohB+YCHqXMgZzb3Csp49vqg=="],
@@ -4933,6 +5018,8 @@
"pascal-case": ["pascal-case@3.1.2", "", { "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3" } }, "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g=="], "pascal-case": ["pascal-case@3.1.2", "", { "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3" } }, "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g=="],
"path": ["path@0.12.7", "", { "dependencies": { "process": "^0.11.1", "util": "^0.10.3" } }, "sha512-aXXC6s+1w7otVF9UletFkFcDsJeO7lSZBPUQhtb5O0xJe8LtYhj/GxldoL09bBj9+ZmE2hNoHqQSFMN5fikh4Q=="],
"path-browserify": ["path-browserify@1.0.1", "", {}, "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="], "path-browserify": ["path-browserify@1.0.1", "", {}, "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="],
"path-data-parser": ["path-data-parser@0.1.0", "", {}, "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w=="], "path-data-parser": ["path-data-parser@0.1.0", "", {}, "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w=="],
@@ -4965,6 +5052,8 @@
"pend": ["pend@1.2.0", "", {}, "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg=="], "pend": ["pend@1.2.0", "", {}, "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg=="],
"perfect-debounce": ["perfect-debounce@2.1.0", "", {}, "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g=="],
"piccolore": ["piccolore@0.1.3", "", {}, "sha512-o8bTeDWjE086iwKrROaDf31K0qC/BENdm15/uH9usSC/uZjJOKb2YGiVHfLY4GhwsERiPI1jmwI2XrA7ACOxVw=="], "piccolore": ["piccolore@0.1.3", "", {}, "sha512-o8bTeDWjE086iwKrROaDf31K0qC/BENdm15/uH9usSC/uZjJOKb2YGiVHfLY4GhwsERiPI1jmwI2XrA7ACOxVw=="],
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
@@ -4993,6 +5082,8 @@
"points-on-path": ["points-on-path@0.2.1", "", { "dependencies": { "path-data-parser": "0.1.0", "points-on-curve": "0.2.0" } }, "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g=="], "points-on-path": ["points-on-path@0.2.1", "", { "dependencies": { "path-data-parser": "0.1.0", "points-on-curve": "0.2.0" } }, "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g=="],
"pony-cause": ["pony-cause@1.1.1", "", {}, "sha512-PxkIc/2ZpLiEzQXu5YRDOUgBlfGYBY8156HY5ZcRAwwonMk5W/MrJP2LLkG/hF7GEQzaHo2aS7ho6ZLCOvf+6g=="],
"possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="], "possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="],
"postcss": ["postcss@8.5.26", "", { "dependencies": { "nanoid": "^3.3.17", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ=="], "postcss": ["postcss@8.5.26", "", { "dependencies": { "nanoid": "^3.3.17", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ=="],
@@ -5221,6 +5312,8 @@
"roarr": ["roarr@2.15.4", "", { "dependencies": { "boolean": "^3.0.1", "detect-node": "^2.0.4", "globalthis": "^1.0.1", "json-stringify-safe": "^5.0.1", "semver-compare": "^1.0.0", "sprintf-js": "^1.1.2" } }, "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A=="], "roarr": ["roarr@2.15.4", "", { "dependencies": { "boolean": "^3.0.1", "detect-node": "^2.0.4", "globalthis": "^1.0.1", "json-stringify-safe": "^5.0.1", "semver-compare": "^1.0.0", "sprintf-js": "^1.1.2" } }, "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A=="],
"robots-parser": ["robots-parser@3.0.1", "", {}, "sha512-s+pyvQeIKIZ0dx5iJiQk1tPLJAWln39+MI5jtM8wnyws+G5azk+dMnMX0qfbqNetKKNgcWWOdi0sfm+FbQbgdQ=="],
"robust-predicates": ["robust-predicates@3.0.3", "", {}, "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA=="], "robust-predicates": ["robust-predicates@3.0.3", "", {}, "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA=="],
"rolldown": ["rolldown@1.2.3", "", { "dependencies": { "@oxc-project/types": "=0.143.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.2.3", "@rolldown/binding-darwin-arm64": "1.2.3", "@rolldown/binding-darwin-x64": "1.2.3", "@rolldown/binding-freebsd-x64": "1.2.3", "@rolldown/binding-linux-arm-gnueabihf": "1.2.3", "@rolldown/binding-linux-arm64-gnu": "1.2.3", "@rolldown/binding-linux-arm64-musl": "1.2.3", "@rolldown/binding-linux-ppc64-gnu": "1.2.3", "@rolldown/binding-linux-s390x-gnu": "1.2.3", "@rolldown/binding-linux-x64-gnu": "1.2.3", "@rolldown/binding-linux-x64-musl": "1.2.3", "@rolldown/binding-openharmony-arm64": "1.2.3", "@rolldown/binding-win32-arm64-msvc": "1.2.3", "@rolldown/binding-win32-x64-msvc": "1.2.3" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-rn9wpmxplLf7NLNyCk9FyWh3FM43DbY8jOzCdEPzH7uflhTftRbCEpqi6Ly2osgoU8OwObtmavMbWLaWy4LX7A=="], "rolldown": ["rolldown@1.2.3", "", { "dependencies": { "@oxc-project/types": "=0.143.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.2.3", "@rolldown/binding-darwin-arm64": "1.2.3", "@rolldown/binding-darwin-x64": "1.2.3", "@rolldown/binding-freebsd-x64": "1.2.3", "@rolldown/binding-linux-arm-gnueabihf": "1.2.3", "@rolldown/binding-linux-arm64-gnu": "1.2.3", "@rolldown/binding-linux-arm64-musl": "1.2.3", "@rolldown/binding-linux-ppc64-gnu": "1.2.3", "@rolldown/binding-linux-s390x-gnu": "1.2.3", "@rolldown/binding-linux-x64-gnu": "1.2.3", "@rolldown/binding-linux-x64-musl": "1.2.3", "@rolldown/binding-openharmony-arm64": "1.2.3", "@rolldown/binding-win32-arm64-msvc": "1.2.3", "@rolldown/binding-win32-x64-msvc": "1.2.3" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-rn9wpmxplLf7NLNyCk9FyWh3FM43DbY8jOzCdEPzH7uflhTftRbCEpqi6Ly2osgoU8OwObtmavMbWLaWy4LX7A=="],
@@ -5249,6 +5342,8 @@
"safe-regex-test": ["safe-regex-test@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-regex": "^1.2.1" } }, "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw=="], "safe-regex-test": ["safe-regex-test@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-regex": "^1.2.1" } }, "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw=="],
"safe-stable-stringify": ["safe-stable-stringify@1.1.1", "", {}, "sha512-ERq4hUjKDbJfE4+XtZLFPCDi8Vb1JqaxAPTxWFLBx8XcAlf9Bda/ZJdVezs/NAfsMQScyIlUMx+Yeu7P7rx5jw=="],
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
"sanitize-filename": ["sanitize-filename@1.6.4", "", { "dependencies": { "truncate-utf8-bytes": "^1.0.0" } }, "sha512-9ZyI08PsvdQl2r/bBIGubpVdR3RR9sY6RDiWFPreA21C/EFlQhmgo20UZlNjZMMZNubusLhAQozkA0Od5J21Eg=="], "sanitize-filename": ["sanitize-filename@1.6.4", "", { "dependencies": { "truncate-utf8-bytes": "^1.0.0" } }, "sha512-9ZyI08PsvdQl2r/bBIGubpVdR3RR9sY6RDiWFPreA21C/EFlQhmgo20UZlNjZMMZNubusLhAQozkA0Od5J21Eg=="],
@@ -5459,7 +5554,7 @@
"stripe": ["stripe@18.0.0", "", { "dependencies": { "@types/node": ">=8.1.0", "qs": "^6.11.0" } }, "sha512-3Fs33IzKUby//9kCkCa1uRpinAoTvj6rJgQ2jrBEysoxEvfsclvXdna1amyEYbA2EKkjynuB4+L/kleCCaWTpA=="], "stripe": ["stripe@18.0.0", "", { "dependencies": { "@types/node": ">=8.1.0", "qs": "^6.11.0" } }, "sha512-3Fs33IzKUby//9kCkCa1uRpinAoTvj6rJgQ2jrBEysoxEvfsclvXdna1amyEYbA2EKkjynuB4+L/kleCCaWTpA=="],
"strnum": ["strnum@1.1.2", "", {}, "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA=="], "strnum": ["strnum@2.4.1", "", { "dependencies": { "anynum": "^1.0.1" } }, "sha512-M9eUSMT2dCB2cTNPG7UYj6KuK7RJR2SN2+yCV/fTW3xzTCS6EaGZ5pSMgDIjB7r8zSfTGk+dvvn9rTjpVS9Mwg=="],
"stubborn-fs": ["stubborn-fs@2.0.0", "", { "dependencies": { "stubborn-utils": "^1.0.1" } }, "sha512-Y0AvSwDw8y+nlSNFXMm2g6L51rBGdAQT20J3YSOqxC53Lo3bjWRtr2BKcfYoAf352WYpsZSTURrA0tqhfgudPA=="], "stubborn-fs": ["stubborn-fs@2.0.0", "", { "dependencies": { "stubborn-utils": "^1.0.1" } }, "sha512-Y0AvSwDw8y+nlSNFXMm2g6L51rBGdAQT20J3YSOqxC53Lo3bjWRtr2BKcfYoAf352WYpsZSTURrA0tqhfgudPA=="],
@@ -5545,8 +5640,6 @@
"toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
"toml": ["toml@4.3.0", "", {}, "sha512-lVb8X9BsPVuH0M4BKeS91tXAmJvCjQ5UIyAbQFaxkKGyUFK2RPkhwaFSQH8vbpl1d23eu/IBH+dwVMHWaq9A5A=="],
"toolbeam-docs-theme": ["toolbeam-docs-theme@0.4.8", "", { "peerDependencies": { "@astrojs/starlight": "^0.34.3", "astro": "^5.7.13" } }, "sha512-b+5ynEFp4Woe5a22hzNQm42lD23t13ZMihVxHbzjA50zdcM9aOSJTIjdJ0PDSd4/50HbBXcpHiQsz6rM4N88ww=="], "toolbeam-docs-theme": ["toolbeam-docs-theme@0.4.8", "", { "peerDependencies": { "@astrojs/starlight": "^0.34.3", "astro": "^5.7.13" } }, "sha512-b+5ynEFp4Woe5a22hzNQm42lD23t13ZMihVxHbzjA50zdcM9aOSJTIjdJ0PDSd4/50HbBXcpHiQsz6rM4N88ww=="],
"topojson-client": ["topojson-client@3.1.0", "", { "dependencies": { "commander": "2" }, "bin": { "topo2geo": "bin/topo2geo", "topomerge": "bin/topomerge", "topoquantize": "bin/topoquantize" } }, "sha512-605uxS6bcYxGXw9qi62XyrV6Q3xwbndjachmNxu8HWTtVPxZfEJN9fd/SZS1Q54Sn2y0TMyMxFj/cJINqGHrKw=="], "topojson-client": ["topojson-client@3.1.0", "", { "dependencies": { "commander": "2" }, "bin": { "topo2geo": "bin/topo2geo", "topomerge": "bin/topomerge", "topoquantize": "bin/topoquantize" } }, "sha512-605uxS6bcYxGXw9qi62XyrV6Q3xwbndjachmNxu8HWTtVPxZfEJN9fd/SZS1Q54Sn2y0TMyMxFj/cJINqGHrKw=="],
@@ -5681,6 +5774,8 @@
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
"urijs": ["urijs@1.19.11", "", {}, "sha512-HXgFDgDommxn5/bIv0cnQZsPhHDA90NPHD6+c/v21U5+Sx5hoP8+dP9IZXBU1gIfvdRfhG8cel9QNPeionfcCQ=="],
"use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="], "use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="],
"use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="], "use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="],
@@ -5689,8 +5784,12 @@
"utf8-byte-length": ["utf8-byte-length@1.0.5", "", {}, "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA=="], "utf8-byte-length": ["utf8-byte-length@1.0.5", "", {}, "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA=="],
"util": ["util@0.10.4", "", { "dependencies": { "inherits": "2.0.3" } }, "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A=="],
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
"utility-types": ["utility-types@3.11.0", "", {}, "sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw=="],
"utils-merge": ["utils-merge@1.0.1", "", {}, "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="], "utils-merge": ["utils-merge@1.0.1", "", {}, "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="],
"uuid": ["uuid@14.0.1", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew=="], "uuid": ["uuid@14.0.1", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew=="],
@@ -5807,7 +5906,7 @@
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
"write-file-atomic": ["write-file-atomic@7.0.1", "", { "dependencies": { "signal-exit": "^4.0.1" } }, "sha512-OTIk8iR8/aCRWBqvxrzxR0hgxWpnYBblY1S5hDWBQfk/VFmJwzmJgQFN3WsoUKHISv2eAwe+PpbUzyL1CKTLXg=="], "write-file-atomic": ["write-file-atomic@8.0.0", "", { "dependencies": { "signal-exit": "^4.0.1" } }, "sha512-dYwyZredl67GyLLIHJnRM3h2PcOmN5SkcgC7eM5DPDEOEl6dLFqVrMg3F1Ea32usj4VSVZtd2H4MtKTNOf6nPg=="],
"ws": ["ws@8.21.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g=="], "ws": ["ws@8.21.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g=="],
@@ -5999,6 +6098,12 @@
"@astrojs/vercel/esbuild": ["esbuild@0.28.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.1", "@esbuild/android-arm": "0.28.1", "@esbuild/android-arm64": "0.28.1", "@esbuild/android-x64": "0.28.1", "@esbuild/darwin-arm64": "0.28.1", "@esbuild/darwin-x64": "0.28.1", "@esbuild/freebsd-arm64": "0.28.1", "@esbuild/freebsd-x64": "0.28.1", "@esbuild/linux-arm": "0.28.1", "@esbuild/linux-arm64": "0.28.1", "@esbuild/linux-ia32": "0.28.1", "@esbuild/linux-loong64": "0.28.1", "@esbuild/linux-mips64el": "0.28.1", "@esbuild/linux-ppc64": "0.28.1", "@esbuild/linux-riscv64": "0.28.1", "@esbuild/linux-s390x": "0.28.1", "@esbuild/linux-x64": "0.28.1", "@esbuild/netbsd-arm64": "0.28.1", "@esbuild/netbsd-x64": "0.28.1", "@esbuild/openbsd-arm64": "0.28.1", "@esbuild/openbsd-x64": "0.28.1", "@esbuild/openharmony-arm64": "0.28.1", "@esbuild/sunos-x64": "0.28.1", "@esbuild/win32-arm64": "0.28.1", "@esbuild/win32-ia32": "0.28.1", "@esbuild/win32-x64": "0.28.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw=="], "@astrojs/vercel/esbuild": ["esbuild@0.28.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.1", "@esbuild/android-arm": "0.28.1", "@esbuild/android-arm64": "0.28.1", "@esbuild/android-x64": "0.28.1", "@esbuild/darwin-arm64": "0.28.1", "@esbuild/darwin-x64": "0.28.1", "@esbuild/freebsd-arm64": "0.28.1", "@esbuild/freebsd-x64": "0.28.1", "@esbuild/linux-arm": "0.28.1", "@esbuild/linux-arm64": "0.28.1", "@esbuild/linux-ia32": "0.28.1", "@esbuild/linux-loong64": "0.28.1", "@esbuild/linux-mips64el": "0.28.1", "@esbuild/linux-ppc64": "0.28.1", "@esbuild/linux-riscv64": "0.28.1", "@esbuild/linux-s390x": "0.28.1", "@esbuild/linux-x64": "0.28.1", "@esbuild/netbsd-arm64": "0.28.1", "@esbuild/netbsd-x64": "0.28.1", "@esbuild/openbsd-arm64": "0.28.1", "@esbuild/openbsd-x64": "0.28.1", "@esbuild/openharmony-arm64": "0.28.1", "@esbuild/sunos-x64": "0.28.1", "@esbuild/win32-arm64": "0.28.1", "@esbuild/win32-ia32": "0.28.1", "@esbuild/win32-x64": "0.28.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw=="],
"@asyncapi/parser/ajv-formats": ["ajv-formats@2.1.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA=="],
"@asyncapi/parser/js-yaml": ["js-yaml@4.3.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ=="],
"@asyncapi/parser/node-fetch": ["node-fetch@2.6.7", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ=="],
"@aws-crypto/crc32/@aws-sdk/types": ["@aws-sdk/types@3.974.2", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-3W6IUtSxFbH6X7Wb7DzGCV5QiFQsd0g8bOfntpmDxQlzBoKWUMBu/JPQR0DwkE+Hpnxd6db1tXbOwdeHddG6cA=="], "@aws-crypto/crc32/@aws-sdk/types": ["@aws-sdk/types@3.974.2", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-3W6IUtSxFbH6X7Wb7DzGCV5QiFQsd0g8bOfntpmDxQlzBoKWUMBu/JPQR0DwkE+Hpnxd6db1tXbOwdeHddG6cA=="],
"@aws-crypto/crc32c/@aws-sdk/types": ["@aws-sdk/types@3.974.2", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-3W6IUtSxFbH6X7Wb7DzGCV5QiFQsd0g8bOfntpmDxQlzBoKWUMBu/JPQR0DwkE+Hpnxd6db1tXbOwdeHddG6cA=="], "@aws-crypto/crc32c/@aws-sdk/types": ["@aws-sdk/types@3.974.2", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-3W6IUtSxFbH6X7Wb7DzGCV5QiFQsd0g8bOfntpmDxQlzBoKWUMBu/JPQR0DwkE+Hpnxd6db1tXbOwdeHddG6cA=="],
@@ -6117,8 +6222,6 @@
"@azure/core-http/uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="], "@azure/core-http/uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="],
"@azure/core-xml/fast-xml-parser": ["fast-xml-parser@5.10.1", "", { "dependencies": { "@nodable/entities": "^3.0.0", "fast-xml-builder": "^1.2.0", "is-unsafe": "^2.0.0", "path-expression-matcher": "^1.6.2", "strnum": "^2.4.1", "xml-naming": "^0.3.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-IEMIf7298kXuZSRFoGfMYrl7is8LpavODgbNz1cwIudv7KwVFnuU+UsMporfq6PD6aXSlawZlARiA3UywCTfMw=="],
"@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], "@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
@@ -6141,6 +6244,8 @@
"@dot/log/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], "@dot/log/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
"@effect/platform-node-shared/ws": ["ws@8.21.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw=="],
"@electron/asar/commander": ["commander@5.1.0", "", {}, "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg=="], "@electron/asar/commander": ["commander@5.1.0", "", {}, "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg=="],
"@electron/asar/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], "@electron/asar/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
@@ -6195,10 +6300,6 @@
"@modelcontextprotocol/sdk/jose": ["jose@6.2.8", "", {}, "sha512-Bsdjwm3Qsd/P0jR+BHDe3LytDfY7WBq2HmCCLIwuVRHMuEC9ae7/R474GIUdF1NgCyZjzVo/A9DOiOBtXq8ZoQ=="], "@modelcontextprotocol/sdk/jose": ["jose@6.2.8", "", {}, "sha512-Bsdjwm3Qsd/P0jR+BHDe3LytDfY7WBq2HmCCLIwuVRHMuEC9ae7/R474GIUdF1NgCyZjzVo/A9DOiOBtXq8ZoQ=="],
"@npmcli/config/ini": ["ini@6.0.0", "", {}, "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ=="],
"@npmcli/git/ini": ["ini@6.0.0", "", {}, "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ=="],
"@npmcli/query/postcss-selector-parser": ["postcss-selector-parser@7.1.5", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-KvvtD7SrlBP7dlgkBghEE3r84CABm5SmV2aNcG4oCA+qDnJ/tvKonFVvwWAyyWUEwxuNawdfEAZKP9zM3oZ2Uw=="], "@npmcli/query/postcss-selector-parser": ["postcss-selector-parser@7.1.5", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-KvvtD7SrlBP7dlgkBghEE3r84CABm5SmV2aNcG4oCA+qDnJ/tvKonFVvwWAyyWUEwxuNawdfEAZKP9zM3oZ2Uw=="],
"@octokit/auth-app/@octokit/request": ["@octokit/request@10.0.13", "", { "dependencies": { "@octokit/endpoint": "^11.0.3", "@octokit/request-error": "^7.1.1", "@octokit/types": "^17.0.0", "content-type": "^2.0.0", "json-with-bigint": "^3.5.3", "universal-user-agent": "^7.0.2" } }, "sha512-v2269YxL9Yf+x3d+gRI63FP0vFQEiWgLyBzxe/Y+0yFDg2B/Tzf5dhh9VNfccVAQnfcfwQWyk/y6Bn7rUXXs7A=="], "@octokit/auth-app/@octokit/request": ["@octokit/request@10.0.13", "", { "dependencies": { "@octokit/endpoint": "^11.0.3", "@octokit/request-error": "^7.1.1", "@octokit/types": "^17.0.0", "content-type": "^2.0.0", "json-with-bigint": "^3.5.3", "universal-user-agent": "^7.0.2" } }, "sha512-v2269YxL9Yf+x3d+gRI63FP0vFQEiWgLyBzxe/Y+0yFDg2B/Tzf5dhh9VNfccVAQnfcfwQWyk/y6Bn7rUXXs7A=="],
@@ -6325,6 +6426,8 @@
"@scalar/types/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], "@scalar/types/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="],
"@sentry/bundler-plugin-core/dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="],
"@sentry/bundler-plugin-core/glob": ["glob@9.3.5", "", { "dependencies": { "fs.realpath": "^1.0.0", "minimatch": "^8.0.2", "minipass": "^4.2.4", "path-scurry": "^1.6.1" } }, "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q=="], "@sentry/bundler-plugin-core/glob": ["glob@9.3.5", "", { "dependencies": { "fs.realpath": "^1.0.0", "minimatch": "^8.0.2", "minipass": "^4.2.4", "path-scurry": "^1.6.1" } }, "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q=="],
"@sentry/bundler-plugin-core/magic-string": ["magic-string@0.30.8", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" } }, "sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ=="], "@sentry/bundler-plugin-core/magic-string": ["magic-string@0.30.8", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" } }, "sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ=="],
@@ -6365,6 +6468,8 @@
"@slack/web-api/p-queue": ["p-queue@6.6.2", "", { "dependencies": { "eventemitter3": "^4.0.4", "p-timeout": "^3.2.0" } }, "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ=="], "@slack/web-api/p-queue": ["p-queue@6.6.2", "", { "dependencies": { "eventemitter3": "^4.0.4", "p-timeout": "^3.2.0" } }, "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ=="],
"@slack/web-api/p-retry": ["p-retry@4.6.2", "", { "dependencies": { "@types/retry": "0.12.0", "retry": "^0.13.1" } }, "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ=="],
"@solidjs/start/es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="], "@solidjs/start/es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="],
"@solidjs/start/path-to-regexp": ["path-to-regexp@8.4.2", "", {}, "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA=="], "@solidjs/start/path-to-regexp": ["path-to-regexp@8.4.2", "", {}, "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA=="],
@@ -6373,6 +6478,26 @@
"@solidjs/start/vite": ["vite@7.1.10", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-CmuvUBzVJ/e3HGxhg6cYk88NGgTnBoOo7ogtfJJ0fefUWAxN/WDSUa50o+oVBxuIhO8FoEZW0j2eW7sfjs5EtA=="], "@solidjs/start/vite": ["vite@7.1.10", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-CmuvUBzVJ/e3HGxhg6cYk88NGgTnBoOo7ogtfJJ0fefUWAxN/WDSUa50o+oVBxuIhO8FoEZW0j2eW7sfjs5EtA=="],
"@stoplight/better-ajv-errors/leven": ["leven@3.1.0", "", {}, "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A=="],
"@stoplight/json/jsonc-parser": ["jsonc-parser@2.2.1", "", {}, "sha512-o6/yDBYccGvTz1+QFevz6l6OBZ2+fMVu2JZ9CIhzsYRX4mjaK5IyX9eldUdCmga16zlgQxyrj5pt9kzuj2C02w=="],
"@stoplight/json-ref-readers/tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="],
"@stoplight/json-ref-resolver/immer": ["immer@9.0.21", "", {}, "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA=="],
"@stoplight/spectral-core/@stoplight/types": ["@stoplight/types@13.6.0", "", { "dependencies": { "@types/json-schema": "^7.0.4", "utility-types": "^3.10.0" } }, "sha512-dzyuzvUjv3m1wmhPfq82lCVYGcXG0xUYgqnWfCq3PCVR4BKFhjdkHrnJ+jIDoMKvXb05AZP/ObQF6+NpDo29IQ=="],
"@stoplight/spectral-core/ajv-formats": ["ajv-formats@2.1.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA=="],
"@stoplight/spectral-core/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="],
"@stoplight/spectral-functions/ajv-formats": ["ajv-formats@2.1.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA=="],
"@stoplight/spectral-parsers/@stoplight/types": ["@stoplight/types@14.1.1", "", { "dependencies": { "@types/json-schema": "^7.0.4", "utility-types": "^3.10.0" } }, "sha512-/kjtr+0t0tjKr+heVfviO9FrU/uGLc+QNX3fHJc19xsCNYqU7lVhaXxDmEID9BZTjG+/r9pK9xP/xU02XGg65g=="],
"@stoplight/yaml/@stoplight/types": ["@stoplight/types@14.1.1", "", { "dependencies": { "@types/json-schema": "^7.0.4", "utility-types": "^3.10.0" } }, "sha512-/kjtr+0t0tjKr+heVfviO9FrU/uGLc+QNX3fHJc19xsCNYqU7lVhaXxDmEID9BZTjG+/r9pK9xP/xU02XGg65g=="],
"@storybook/addon-docs/react": ["react@19.2.8", "", {}, "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw=="], "@storybook/addon-docs/react": ["react@19.2.8", "", {}, "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw=="],
"@storybook/addon-docs/react-dom": ["react-dom@19.2.8", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.8" } }, "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ=="], "@storybook/addon-docs/react-dom": ["react-dom@19.2.8", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.8" } }, "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ=="],
@@ -6435,6 +6560,8 @@
"app-builder-lib/ci-info": ["ci-info@4.3.1", "", {}, "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA=="], "app-builder-lib/ci-info": ["ci-info@4.3.1", "", {}, "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA=="],
"app-builder-lib/dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="],
"app-builder-lib/hosted-git-info": ["hosted-git-info@4.1.0", "", { "dependencies": { "lru-cache": "^6.0.0" } }, "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA=="], "app-builder-lib/hosted-git-info": ["hosted-git-info@4.1.0", "", { "dependencies": { "lru-cache": "^6.0.0" } }, "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA=="],
"app-builder-lib/js-yaml": ["js-yaml@4.3.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ=="], "app-builder-lib/js-yaml": ["js-yaml@4.3.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ=="],
@@ -6473,6 +6600,8 @@
"babel-plugin-module-resolver/glob": ["glob@9.3.5", "", { "dependencies": { "fs.realpath": "^1.0.0", "minimatch": "^8.0.2", "minipass": "^4.2.4", "path-scurry": "^1.6.1" } }, "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q=="], "babel-plugin-module-resolver/glob": ["glob@9.3.5", "", { "dependencies": { "fs.realpath": "^1.0.0", "minimatch": "^8.0.2", "minipass": "^4.2.4", "path-scurry": "^1.6.1" } }, "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q=="],
"bin-links/write-file-atomic": ["write-file-atomic@7.0.1", "", { "dependencies": { "signal-exit": "^4.0.1" } }, "sha512-OTIk8iR8/aCRWBqvxrzxR0hgxWpnYBblY1S5hDWBQfk/VFmJwzmJgQFN3WsoUKHISv2eAwe+PpbUzyL1CKTLXg=="],
"blume/@astrojs/mdx": ["@astrojs/mdx@7.0.5", "", { "dependencies": { "@astrojs/internal-helpers": "0.10.2", "@astrojs/markdown-remark": "7.2.2", "@mdx-js/mdx": "^3.1.1", "acorn": "^8.16.0", "es-module-lexer": "^2.0.0", "estree-util-visit": "^2.0.0", "hast-util-to-html": "^9.0.5", "piccolore": "^0.1.3", "rehype-raw": "^7.0.0", "remark-gfm": "^4.0.1", "remark-smartypants": "^3.0.2", "source-map": "^0.7.6", "unist-util-visit": "^5.1.0", "vfile": "^6.0.3" }, "peerDependencies": { "@astrojs/markdown-satteri": "^0.3.1", "astro": "^7.0.0" }, "optionalPeers": ["@astrojs/markdown-satteri"] }, "sha512-wEM/HH1RiEntyPVagdiF+yArzfcYLKBB0C1RZspVidKZ97rRMbaqP1Nbl/GR0sJs8zwaceqxRymw8aOKKJRdYw=="], "blume/@astrojs/mdx": ["@astrojs/mdx@7.0.5", "", { "dependencies": { "@astrojs/internal-helpers": "0.10.2", "@astrojs/markdown-remark": "7.2.2", "@mdx-js/mdx": "^3.1.1", "acorn": "^8.16.0", "es-module-lexer": "^2.0.0", "estree-util-visit": "^2.0.0", "hast-util-to-html": "^9.0.5", "piccolore": "^0.1.3", "rehype-raw": "^7.0.0", "remark-gfm": "^4.0.1", "remark-smartypants": "^3.0.2", "source-map": "^0.7.6", "unist-util-visit": "^5.1.0", "vfile": "^6.0.3" }, "peerDependencies": { "@astrojs/markdown-satteri": "^0.3.1", "astro": "^7.0.0" }, "optionalPeers": ["@astrojs/markdown-satteri"] }, "sha512-wEM/HH1RiEntyPVagdiF+yArzfcYLKBB0C1RZspVidKZ97rRMbaqP1Nbl/GR0sJs8zwaceqxRymw8aOKKJRdYw=="],
"blume/@clack/prompts": ["@clack/prompts@1.7.0", "", { "dependencies": { "@clack/core": "1.4.3", "fast-string-width": "^3.0.2", "fast-wrap-ansi": "^0.2.0", "sisteransi": "^1.0.5" } }, "sha512-y7/yvZ2TPAnR9+jnc00klvNNLkJiXFFrQA/hlLCcxA9a2A4zQIOimyFQ9XfwYKiGD1fb5GY8vbKIIgO8d5Tb2A=="], "blume/@clack/prompts": ["@clack/prompts@1.7.0", "", { "dependencies": { "@clack/core": "1.4.3", "fast-string-width": "^3.0.2", "fast-wrap-ansi": "^0.2.0", "sisteransi": "^1.0.5" } }, "sha512-y7/yvZ2TPAnR9+jnc00klvNNLkJiXFFrQA/hlLCcxA9a2A4zQIOimyFQ9XfwYKiGD1fb5GY8vbKIIgO8d5Tb2A=="],
@@ -6481,23 +6610,35 @@
"blume/@shikijs/transformers": ["@shikijs/transformers@4.4.2", "", { "dependencies": { "@shikijs/core": "4.4.2", "@shikijs/types": "4.4.2" } }, "sha512-d81PJ9KkR1tVP95FH/9296HTtDo0mh76wv10u9T1YmsZq/UcXgt0OLdBszfUQ1i+umkRMCjDnFbFZU7/tCODTQ=="], "blume/@shikijs/transformers": ["@shikijs/transformers@4.4.2", "", { "dependencies": { "@shikijs/core": "4.4.2", "@shikijs/types": "4.4.2" } }, "sha512-d81PJ9KkR1tVP95FH/9296HTtDo0mh76wv10u9T1YmsZq/UcXgt0OLdBszfUQ1i+umkRMCjDnFbFZU7/tCODTQ=="],
"blume/ai": ["ai@7.0.64", "", { "dependencies": { "@ai-sdk/gateway": "4.0.51", "@ai-sdk/provider": "4.0.7", "@ai-sdk/provider-utils": "5.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-29Ufm56e53pRzIPueWzZiwJsgxmZStaBFpwEkVM89sqyIIax/pzDm+ez/ksJ/CcY9UCuYzvCQpE2zkFHICvS3w=="],
"blume/astro": ["astro@7.1.3", "", { "dependencies": { "@astrojs/compiler-rs": "^0.3.1", "@astrojs/internal-helpers": "0.10.1", "@astrojs/markdown-satteri": "0.3.4", "@astrojs/telemetry": "3.3.3", "@capsizecss/unpack": "^4.0.0", "@clack/prompts": "^1.1.0", "@oslojs/encoding": "^1.1.0", "@rollup/pluginutils": "^5.3.0", "am-i-vibing": "^0.4.0", "aria-query": "^5.3.2", "axobject-query": "^4.1.0", "ci-info": "^4.4.0", "clsx": "^2.1.1", "common-ancestor-path": "^2.0.0", "cookie": "^2.0.1", "devalue": "^5.8.1", "diff": "^8.0.3", "dset": "^3.1.4", "es-module-lexer": "^2.0.0", "esbuild": "^0.28.0", "flattie": "^1.1.1", "fontace": "~0.4.1", "get-tsconfig": "5.0.0-beta.4", "github-slugger": "^2.0.0", "html-escaper": "3.0.3", "http-cache-semantics": "^4.2.0", "js-yaml": "^4.1.1", "jsonc-parser": "^3.3.1", "magic-string": "^0.30.21", "magicast": "^0.5.2", "mrmime": "^2.0.1", "neotraverse": "^1.0.1", "obug": "^2.1.1", "p-limit": "^7.3.0", "p-queue": "^9.1.0", "package-manager-detector": "^1.6.0", "piccolore": "^0.1.3", "picomatch": "^4.0.4", "semver": "^7.7.4", "shiki": "^4.0.2", "smol-toml": "^1.6.0", "svgo": "^4.0.1", "tinyclip": "^0.1.12", "tinyexec": "^1.0.4", "tinyglobby": "^0.2.15", "ultrahtml": "^1.6.0", "unifont": "~0.7.4", "unstorage": "^1.17.5", "vite": "^8.0.13", "vitefu": "^1.1.2", "xxhash-wasm": "^1.1.0", "yargs-parser": "^22.0.0", "zod": "^4.3.6" }, "optionalDependencies": { "sharp": "^0.34.0 || ^0.35.0" }, "peerDependencies": { "@astrojs/markdown-remark": "7.2.1" }, "optionalPeers": ["@astrojs/markdown-remark"], "bin": { "astro": "./bin/astro.mjs" } }, "sha512-4dhPyAAXthf3xLEYnG8SeL7yr/nTPPABfY7e9YF0yuO+vK9Xp+8Q5j4xzsmL3GueukQv4oNwGNTBepLOiDGeJA=="], "blume/astro": ["astro@7.1.3", "", { "dependencies": { "@astrojs/compiler-rs": "^0.3.1", "@astrojs/internal-helpers": "0.10.1", "@astrojs/markdown-satteri": "0.3.4", "@astrojs/telemetry": "3.3.3", "@capsizecss/unpack": "^4.0.0", "@clack/prompts": "^1.1.0", "@oslojs/encoding": "^1.1.0", "@rollup/pluginutils": "^5.3.0", "am-i-vibing": "^0.4.0", "aria-query": "^5.3.2", "axobject-query": "^4.1.0", "ci-info": "^4.4.0", "clsx": "^2.1.1", "common-ancestor-path": "^2.0.0", "cookie": "^2.0.1", "devalue": "^5.8.1", "diff": "^8.0.3", "dset": "^3.1.4", "es-module-lexer": "^2.0.0", "esbuild": "^0.28.0", "flattie": "^1.1.1", "fontace": "~0.4.1", "get-tsconfig": "5.0.0-beta.4", "github-slugger": "^2.0.0", "html-escaper": "3.0.3", "http-cache-semantics": "^4.2.0", "js-yaml": "^4.1.1", "jsonc-parser": "^3.3.1", "magic-string": "^0.30.21", "magicast": "^0.5.2", "mrmime": "^2.0.1", "neotraverse": "^1.0.1", "obug": "^2.1.1", "p-limit": "^7.3.0", "p-queue": "^9.1.0", "package-manager-detector": "^1.6.0", "piccolore": "^0.1.3", "picomatch": "^4.0.4", "semver": "^7.7.4", "shiki": "^4.0.2", "smol-toml": "^1.6.0", "svgo": "^4.0.1", "tinyclip": "^0.1.12", "tinyexec": "^1.0.4", "tinyglobby": "^0.2.15", "ultrahtml": "^1.6.0", "unifont": "~0.7.4", "unstorage": "^1.17.5", "vite": "^8.0.13", "vitefu": "^1.1.2", "xxhash-wasm": "^1.1.0", "yargs-parser": "^22.0.0", "zod": "^4.3.6" }, "optionalDependencies": { "sharp": "^0.34.0 || ^0.35.0" }, "peerDependencies": { "@astrojs/markdown-remark": "7.2.1" }, "optionalPeers": ["@astrojs/markdown-remark"], "bin": { "astro": "./bin/astro.mjs" } }, "sha512-4dhPyAAXthf3xLEYnG8SeL7yr/nTPPABfY7e9YF0yuO+vK9Xp+8Q5j4xzsmL3GueukQv4oNwGNTBepLOiDGeJA=="],
"blume/chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="],
"blume/dompurify": ["dompurify@3.4.13", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-2vmYIoqjze2d+kakP8S/nS5shfsl587kzwEjcGlTdiksUVgFHnFCsLYDVj/JNqJVOQZGSYBTmuycv0PodwmnMQ=="], "blume/dompurify": ["dompurify@3.4.13", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-2vmYIoqjze2d+kakP8S/nS5shfsl587kzwEjcGlTdiksUVgFHnFCsLYDVj/JNqJVOQZGSYBTmuycv0PodwmnMQ=="],
"blume/js-yaml": ["js-yaml@4.3.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ=="], "blume/js-yaml": ["js-yaml@4.3.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ=="],
"blume/katex": ["katex@0.17.0", "", { "dependencies": { "commander": "^8.3.0" }, "bin": { "katex": "cli.js" } }, "sha512-Vdw0ATsQ9V+LuegM/BTwQqV/6cTl5lbGcIrU+BCgLxyf6bo38ybOr372tuSIxir3CN720flu1meYR6XzNMwQnw=="], "blume/katex": ["katex@0.18.4", "", { "dependencies": { "commander": "^8.3.0" }, "bin": { "katex": "cli.js" } }, "sha512-IMPntbRLOU+eu88XDiFKqQ8Akhr9Tv7jDMXqPhjG9SI1JMA4DIgXk4x9k4skJz2NZJXBRbC+2pYBLj9olqcZow=="],
"blume/node-html-parser": ["node-html-parser@9.0.1", "", { "dependencies": { "css-select": "^5.1.0", "entities": "^8.0.0" } }, "sha512-QrdiYYm1NnLRXsMXThUgVcF/syWfWIgHFmy8hylWMGFbHFtnRuXLBxxGQvIm3xaIJMUDJ0ayOmT/FGJYT3pZIw=="], "blume/node-html-parser": ["node-html-parser@9.0.1", "", { "dependencies": { "css-select": "^5.1.0", "entities": "^8.0.0" } }, "sha512-QrdiYYm1NnLRXsMXThUgVcF/syWfWIgHFmy8hylWMGFbHFtnRuXLBxxGQvIm3xaIJMUDJ0ayOmT/FGJYT3pZIw=="],
"blume/p-limit": ["p-limit@7.3.1", "", { "dependencies": { "yocto-queue": "^1.2.1" } }, "sha512-0trZaiG7Y7kN/Egy9a8j47t9osC0Tch4PaIWd9yGF6bvmlk7muExRvGNYb8sXBwEKMoNKsbNN9P8EefuQekE4Q=="],
"blume/react": ["react@19.2.8", "", {}, "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw=="], "blume/react": ["react@19.2.8", "", {}, "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw=="],
"blume/react-dom": ["react-dom@19.2.8", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.8" } }, "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ=="], "blume/react-dom": ["react-dom@19.2.8", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.8" } }, "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ=="],
"blume/sharp": ["sharp@0.35.3", "", { "dependencies": { "@img/colour": "^1.1.0", "detect-libc": "^2.1.2", "semver": "^7.8.5" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.35.3", "@img/sharp-darwin-x64": "0.35.3", "@img/sharp-freebsd-wasm32": "0.35.3", "@img/sharp-libvips-darwin-arm64": "1.3.2", "@img/sharp-libvips-darwin-x64": "1.3.2", "@img/sharp-libvips-linux-arm": "1.3.2", "@img/sharp-libvips-linux-arm64": "1.3.2", "@img/sharp-libvips-linux-ppc64": "1.3.2", "@img/sharp-libvips-linux-riscv64": "1.3.2", "@img/sharp-libvips-linux-s390x": "1.3.2", "@img/sharp-libvips-linux-x64": "1.3.2", "@img/sharp-libvips-linuxmusl-arm64": "1.3.2", "@img/sharp-libvips-linuxmusl-x64": "1.3.2", "@img/sharp-linux-arm": "0.35.3", "@img/sharp-linux-arm64": "0.35.3", "@img/sharp-linux-ppc64": "0.35.3", "@img/sharp-linux-riscv64": "0.35.3", "@img/sharp-linux-s390x": "0.35.3", "@img/sharp-linux-x64": "0.35.3", "@img/sharp-linuxmusl-arm64": "0.35.3", "@img/sharp-linuxmusl-x64": "0.35.3", "@img/sharp-webcontainers-wasm32": "0.35.3", "@img/sharp-win32-arm64": "0.35.3", "@img/sharp-win32-ia32": "0.35.3", "@img/sharp-win32-x64": "0.35.3" }, "peerDependencies": { "@types/node": "*" }, "optionalPeers": ["@types/node"] }, "sha512-ej0zVHuZGHCiABXcNxeYhpRnPNPAcvbG8RMdBAhDAxLKkCRVSpK3Iyu7qbqw3JMzoj0REeM6f3tJLtVwl0023Q=="],
"blume/string-width": ["string-width@8.2.2", "", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg=="],
"blume/tailwindcss": ["tailwindcss@4.3.3", "", {}, "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ=="],
"blume/typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="], "blume/typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="],
"blume/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], "blume/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="],
"body-parser/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="], "body-parser/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="],
@@ -6545,6 +6686,8 @@
"dot-prop/type-fest": ["type-fest@3.13.1", "", {}, "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g=="], "dot-prop/type-fest": ["type-fest@3.13.1", "", {}, "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g=="],
"dotenv-expand/dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="],
"duplexer2/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], "duplexer2/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
"editorconfig/commander": ["commander@10.0.1", "", {}, "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug=="], "editorconfig/commander": ["commander@10.0.1", "", {}, "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug=="],
@@ -6645,8 +6788,6 @@
"mermaid/dompurify": ["dompurify@3.4.13", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-2vmYIoqjze2d+kakP8S/nS5shfsl587kzwEjcGlTdiksUVgFHnFCsLYDVj/JNqJVOQZGSYBTmuycv0PodwmnMQ=="], "mermaid/dompurify": ["dompurify@3.4.13", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-2vmYIoqjze2d+kakP8S/nS5shfsl587kzwEjcGlTdiksUVgFHnFCsLYDVj/JNqJVOQZGSYBTmuycv0PodwmnMQ=="],
"mermaid/katex": ["katex@0.16.47", "", { "dependencies": { "commander": "^8.3.0" }, "bin": { "katex": "cli.js" } }, "sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg=="],
"mermaid/marked": ["marked@16.4.2", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA=="], "mermaid/marked": ["marked@16.4.2", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA=="],
"micromark-extension-mdxjs/acorn": ["acorn@8.18.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ=="], "micromark-extension-mdxjs/acorn": ["acorn@8.18.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ=="],
@@ -6685,8 +6826,6 @@
"p-locate/p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], "p-locate/p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],
"p-retry/retry": ["retry@0.13.1", "", {}, "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg=="],
"parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], "parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
"parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], "parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
@@ -6785,6 +6924,8 @@
"unzipper/fs-extra": ["fs-extra@11.3.1", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-eXvGGwZ5CL17ZSwHWd3bbgk7UUpF6IFHtP57NYYakPvHOs8GDgDe5KJI36jIJzDkJ6eJjuzRA8eBQb6SkKue0g=="], "unzipper/fs-extra": ["fs-extra@11.3.1", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-eXvGGwZ5CL17ZSwHWd3bbgk7UUpF6IFHtP57NYYakPvHOs8GDgDe5KJI36jIJzDkJ6eJjuzRA8eBQb6SkKue0g=="],
"util/inherits": ["inherits@2.0.3", "", {}, "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw=="],
"venice-ai-sdk-provider/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.64", "", { "dependencies": { "@ai-sdk/provider": "3.0.14", "@ai-sdk/provider-utils": "4.0.42" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-oMh5/bhu2ibsXqzHeLbJ+9Y7UQdOSrfOLPI+UA1vEcqXGYVBQfWxLe5jBUpsaL8GBBj/nmSeYGX6QPan8RfHiQ=="], "venice-ai-sdk-provider/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.64", "", { "dependencies": { "@ai-sdk/provider": "3.0.14", "@ai-sdk/provider-utils": "4.0.42" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-oMh5/bhu2ibsXqzHeLbJ+9Y7UQdOSrfOLPI+UA1vEcqXGYVBQfWxLe5jBUpsaL8GBBj/nmSeYGX6QPan8RfHiQ=="],
"venice-ai-sdk-provider/@ai-sdk/provider": ["@ai-sdk/provider@3.0.14", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-5X1k57JBJ4H7H1QjX7CnJYAB1I19r/trVZTMcSms7/kLNZ8RaU4Nt2agcwZzv82Hfx6Q7/TOLU7agAKeFfc8cA=="], "venice-ai-sdk-provider/@ai-sdk/provider": ["@ai-sdk/provider@3.0.14", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-5X1k57JBJ4H7H1QjX7CnJYAB1I19r/trVZTMcSms7/kLNZ8RaU4Nt2agcwZzv82Hfx6Q7/TOLU7agAKeFfc8cA=="],
@@ -7019,6 +7160,8 @@
"@astrojs/vercel/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.1", "", { "os": "win32", "cpu": "x64" }, "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A=="], "@astrojs/vercel/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.1", "", { "os": "win32", "cpu": "x64" }, "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A=="],
"@asyncapi/parser/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
"@aws-crypto/sha1-browser/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], "@aws-crypto/sha1-browser/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="],
"@aws-crypto/sha256-browser/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], "@aws-crypto/sha256-browser/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="],
@@ -7033,6 +7176,8 @@
"@aws-sdk/client-lambda/@aws-sdk/core/@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.3.0", "", {}, "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ=="], "@aws-sdk/client-lambda/@aws-sdk/core/@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.3.0", "", {}, "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ=="],
"@aws-sdk/client-sts/@aws-sdk/core/fast-xml-parser": ["fast-xml-parser@4.4.1", "", { "dependencies": { "strnum": "^1.0.5" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw=="],
"@aws-sdk/client-sts/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.775.0", "", { "dependencies": { "@aws-sdk/core": "3.775.0", "@aws-sdk/types": "3.775.0", "@smithy/property-provider": "^4.0.2", "@smithy/types": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-6ESVxwCbGm7WZ17kY1fjmxQud43vzJFoLd4bmlR+idQSWdqlzGDYdcfzpjDKTcivdtNrVYmFvcH1JBUwCRAZhw=="], "@aws-sdk/client-sts/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.775.0", "", { "dependencies": { "@aws-sdk/core": "3.775.0", "@aws-sdk/types": "3.775.0", "@smithy/property-provider": "^4.0.2", "@smithy/types": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-6ESVxwCbGm7WZ17kY1fjmxQud43vzJFoLd4bmlR+idQSWdqlzGDYdcfzpjDKTcivdtNrVYmFvcH1JBUwCRAZhw=="],
"@aws-sdk/client-sts/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.775.0", "", { "dependencies": { "@aws-sdk/core": "3.775.0", "@aws-sdk/types": "3.775.0", "@smithy/fetch-http-handler": "^5.0.2", "@smithy/node-http-handler": "^4.0.4", "@smithy/property-provider": "^4.0.2", "@smithy/protocol-http": "^5.1.0", "@smithy/smithy-client": "^4.2.0", "@smithy/types": "^4.2.0", "@smithy/util-stream": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-PjDQeDH/J1S0yWV32wCj2k5liRo0ssXMseCBEkCsD3SqsU8o5cU82b0hMX4sAib/RkglCSZqGO0xMiN0/7ndww=="], "@aws-sdk/client-sts/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.775.0", "", { "dependencies": { "@aws-sdk/core": "3.775.0", "@aws-sdk/types": "3.775.0", "@smithy/fetch-http-handler": "^5.0.2", "@smithy/node-http-handler": "^4.0.4", "@smithy/property-provider": "^4.0.2", "@smithy/protocol-http": "^5.1.0", "@smithy/smithy-client": "^4.2.0", "@smithy/types": "^4.2.0", "@smithy/util-stream": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-PjDQeDH/J1S0yWV32wCj2k5liRo0ssXMseCBEkCsD3SqsU8o5cU82b0hMX4sAib/RkglCSZqGO0xMiN0/7ndww=="],
@@ -7091,10 +7236,6 @@
"@aws-sdk/token-providers/@aws-sdk/core/@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.3.0", "", {}, "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ=="], "@aws-sdk/token-providers/@aws-sdk/core/@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.3.0", "", {}, "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ=="],
"@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.4.1", "", { "dependencies": { "anynum": "^1.0.1" } }, "sha512-M9eUSMT2dCB2cTNPG7UYj6KuK7RJR2SN2+yCV/fTW3xzTCS6EaGZ5pSMgDIjB7r8zSfTGk+dvvn9rTjpVS9Mwg=="],
"@azure/core-xml/fast-xml-parser/strnum": ["strnum@2.4.1", "", { "dependencies": { "anynum": "^1.0.1" } }, "sha512-M9eUSMT2dCB2cTNPG7UYj6KuK7RJR2SN2+yCV/fTW3xzTCS6EaGZ5pSMgDIjB7r8zSfTGk+dvvn9rTjpVS9Mwg=="],
"@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], "@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
"@bruits/satteri-wasm32-wasi/@emnapi/core/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA=="], "@bruits/satteri-wasm32-wasi/@emnapi/core/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA=="],
@@ -7413,6 +7554,8 @@
"@slack/web-api/p-queue/p-timeout": ["p-timeout@3.2.0", "", { "dependencies": { "p-finally": "^1.0.0" } }, "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg=="], "@slack/web-api/p-queue/p-timeout": ["p-timeout@3.2.0", "", { "dependencies": { "p-finally": "^1.0.0" } }, "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg=="],
"@slack/web-api/p-retry/retry": ["retry@0.13.1", "", {}, "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg=="],
"@solidjs/start/shiki/@shikijs/core": ["@shikijs/core@1.29.2", "", { "dependencies": { "@shikijs/engine-javascript": "1.29.2", "@shikijs/engine-oniguruma": "1.29.2", "@shikijs/types": "1.29.2", "@shikijs/vscode-textmate": "^10.0.1", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.4" } }, "sha512-vju0lY9r27jJfOY4Z7+Rt/nIOjzJpZ3y+nYpqtUZInVoXQ/TJZcfGnNOGnKjFdVZb8qexiCuSlZRKcGfhhTTZQ=="], "@solidjs/start/shiki/@shikijs/core": ["@shikijs/core@1.29.2", "", { "dependencies": { "@shikijs/engine-javascript": "1.29.2", "@shikijs/engine-oniguruma": "1.29.2", "@shikijs/types": "1.29.2", "@shikijs/vscode-textmate": "^10.0.1", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.4" } }, "sha512-vju0lY9r27jJfOY4Z7+Rt/nIOjzJpZ3y+nYpqtUZInVoXQ/TJZcfGnNOGnKjFdVZb8qexiCuSlZRKcGfhhTTZQ=="],
"@solidjs/start/shiki/@shikijs/engine-javascript": ["@shikijs/engine-javascript@1.29.2", "", { "dependencies": { "@shikijs/types": "1.29.2", "@shikijs/vscode-textmate": "^10.0.1", "oniguruma-to-es": "^2.2.0" } }, "sha512-iNEZv4IrLYPv64Q6k7EPpOCE/nuvGiKl7zxdq0WFuRPF5PAE9PRo2JGq/d8crLusM59BRemJ4eOqrFrC4wiQ+A=="], "@solidjs/start/shiki/@shikijs/engine-javascript": ["@shikijs/engine-javascript@1.29.2", "", { "dependencies": { "@shikijs/types": "1.29.2", "@shikijs/vscode-textmate": "^10.0.1", "oniguruma-to-es": "^2.2.0" } }, "sha512-iNEZv4IrLYPv64Q6k7EPpOCE/nuvGiKl7zxdq0WFuRPF5PAE9PRo2JGq/d8crLusM59BRemJ4eOqrFrC4wiQ+A=="],
@@ -7425,6 +7568,8 @@
"@solidjs/start/shiki/@shikijs/types": ["@shikijs/types@1.29.2", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.1", "@types/hast": "^3.0.4" } }, "sha512-VJjK0eIijTZf0QSTODEXCqinjBn0joAHQ+aPSBzrv4O2d/QSbsMw+ZeSRx03kV34Hy7NzUvV/7NqfYGRLrASmw=="], "@solidjs/start/shiki/@shikijs/types": ["@shikijs/types@1.29.2", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.1", "@types/hast": "^3.0.4" } }, "sha512-VJjK0eIijTZf0QSTODEXCqinjBn0joAHQ+aPSBzrv4O2d/QSbsMw+ZeSRx03kV34Hy7NzUvV/7NqfYGRLrASmw=="],
"@stoplight/spectral-core/minimatch/brace-expansion": ["brace-expansion@1.1.18", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw=="],
"@storybook/addon-docs/react-dom/scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], "@storybook/addon-docs/react-dom/scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
"@storybook/csf-plugin/unplugin/acorn": ["acorn@8.18.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ=="], "@storybook/csf-plugin/unplugin/acorn": ["acorn@8.18.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ=="],
@@ -7515,6 +7660,12 @@
"blume/@shikijs/transformers/@shikijs/types": ["@shikijs/types@4.4.2", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.5" } }, "sha512-PFYitV4vpDr/iPCIhnHp+Q4ftic5N5VeNJ3KQ1O8gn3h2ar8qgwMAXF7tq4m1CWaMS60fV4VqF6vfnWH4F7vqQ=="], "blume/@shikijs/transformers/@shikijs/types": ["@shikijs/types@4.4.2", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.5" } }, "sha512-PFYitV4vpDr/iPCIhnHp+Q4ftic5N5VeNJ3KQ1O8gn3h2ar8qgwMAXF7tq4m1CWaMS60fV4VqF6vfnWH4F7vqQ=="],
"blume/ai/@ai-sdk/gateway": ["@ai-sdk/gateway@4.0.51", "", { "dependencies": { "@ai-sdk/provider": "4.0.7", "@ai-sdk/provider-utils": "5.0.27", "@vercel/oidc": "3.2.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-tQxWGUbg/O3A5EgekJo/C2byLVQ1r+vL6QdEVWmxUSnPhdCupcOeDbQO1tv9Um40VrdwYuSWlYzcbWvtlG5bOA=="],
"blume/ai/@ai-sdk/provider": ["@ai-sdk/provider@4.0.7", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-6or44XprPzKbr8zkmzosowSE0pxkvJcoojBL+mCZvPUt3kvXp3XSNqeVun9golb1acEfSo6yaEBRT18h2VU+1Q=="],
"blume/ai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@5.0.27", "", { "dependencies": { "@ai-sdk/provider": "4.0.7", "@standard-schema/spec": "^1.1.0", "@workflow/serde": "4.1.0", "eventsource-parser": "^3.0.8", "undici": "^7.28.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-EzAn4pdgG5g0xXtH6lE2zyNmfjDQIDjATkfqzuidEI35g++hh4+07vnjzkT/RmGmIClPZiRj/Q2GMPV2V7mkHw=="],
"blume/astro/@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.10.1", "", { "dependencies": { "@types/hast": "^3.0.4", "@types/mdast": "^4.0.4", "js-yaml": "^4.1.1", "picomatch": "^4.0.4", "retext-smartypants": "^6.2.0", "shiki": "^4.0.2", "smol-toml": "^1.6.0", "unified": "^11.0.5" } }, "sha512-5phcroT/vmOOrYuuAxtkbPixy5hePtlz9i8K4OeDv3dNK6/UQRuXPOSRTxIOBbUY5Sonw2UaxjbuVc43Mcir6Q=="], "blume/astro/@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.10.1", "", { "dependencies": { "@types/hast": "^3.0.4", "@types/mdast": "^4.0.4", "js-yaml": "^4.1.1", "picomatch": "^4.0.4", "retext-smartypants": "^6.2.0", "shiki": "^4.0.2", "smol-toml": "^1.6.0", "unified": "^11.0.5" } }, "sha512-5phcroT/vmOOrYuuAxtkbPixy5hePtlz9i8K4OeDv3dNK6/UQRuXPOSRTxIOBbUY5Sonw2UaxjbuVc43Mcir6Q=="],
"blume/astro/@astrojs/telemetry": ["@astrojs/telemetry@3.3.3", "", { "dependencies": { "ci-info": "^4.4.0", "dset": "^3.1.4", "is-docker": "^4.0.0", "package-manager-detector": "^1.6.0" } }, "sha512-C1TLn5sPJr0x4vk56piHWKbnqlEB8BKyte5Y45V02U+D7BGO5eMqZDH5aPjnkXQWJggvmsTXxH03QMZ9NgWLzQ=="], "blume/astro/@astrojs/telemetry": ["@astrojs/telemetry@3.3.3", "", { "dependencies": { "ci-info": "^4.4.0", "dset": "^3.1.4", "is-docker": "^4.0.0", "package-manager-detector": "^1.6.0" } }, "sha512-C1TLn5sPJr0x4vk56piHWKbnqlEB8BKyte5Y45V02U+D7BGO5eMqZDH5aPjnkXQWJggvmsTXxH03QMZ9NgWLzQ=="],
@@ -7535,12 +7686,8 @@
"blume/astro/neotraverse": ["neotraverse@1.0.1", "", {}, "sha512-WmmLty1YWwJl9yZi77v2dVIV6X2kuYV8YYBI/G3LWGKdGHmHUvL1z7FW0iDvEvGAwNEoc5x1tOOOyDnf5jJw/w=="], "blume/astro/neotraverse": ["neotraverse@1.0.1", "", {}, "sha512-WmmLty1YWwJl9yZi77v2dVIV6X2kuYV8YYBI/G3LWGKdGHmHUvL1z7FW0iDvEvGAwNEoc5x1tOOOyDnf5jJw/w=="],
"blume/astro/p-limit": ["p-limit@7.3.1", "", { "dependencies": { "yocto-queue": "^1.2.1" } }, "sha512-0trZaiG7Y7kN/Egy9a8j47t9osC0Tch4PaIWd9yGF6bvmlk7muExRvGNYb8sXBwEKMoNKsbNN9P8EefuQekE4Q=="],
"blume/astro/p-queue": ["p-queue@9.3.3", "", { "dependencies": { "eventemitter3": "^5.0.4", "p-timeout": "^7.0.0" } }, "sha512-NXAOdnEe5FsZJfT4oK84lE1Y5cFFdWlRuOo5tww8DyNMxyRXwn39fIkUtNLKppcPC+UYU/bXujNCUGDv01y7CA=="], "blume/astro/p-queue": ["p-queue@9.3.3", "", { "dependencies": { "eventemitter3": "^5.0.4", "p-timeout": "^7.0.0" } }, "sha512-NXAOdnEe5FsZJfT4oK84lE1Y5cFFdWlRuOo5tww8DyNMxyRXwn39fIkUtNLKppcPC+UYU/bXujNCUGDv01y7CA=="],
"blume/astro/sharp": ["sharp@0.35.3", "", { "dependencies": { "@img/colour": "^1.1.0", "detect-libc": "^2.1.2", "semver": "^7.8.5" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.35.3", "@img/sharp-darwin-x64": "0.35.3", "@img/sharp-freebsd-wasm32": "0.35.3", "@img/sharp-libvips-darwin-arm64": "1.3.2", "@img/sharp-libvips-darwin-x64": "1.3.2", "@img/sharp-libvips-linux-arm": "1.3.2", "@img/sharp-libvips-linux-arm64": "1.3.2", "@img/sharp-libvips-linux-ppc64": "1.3.2", "@img/sharp-libvips-linux-riscv64": "1.3.2", "@img/sharp-libvips-linux-s390x": "1.3.2", "@img/sharp-libvips-linux-x64": "1.3.2", "@img/sharp-libvips-linuxmusl-arm64": "1.3.2", "@img/sharp-libvips-linuxmusl-x64": "1.3.2", "@img/sharp-linux-arm": "0.35.3", "@img/sharp-linux-arm64": "0.35.3", "@img/sharp-linux-ppc64": "0.35.3", "@img/sharp-linux-riscv64": "0.35.3", "@img/sharp-linux-s390x": "0.35.3", "@img/sharp-linux-x64": "0.35.3", "@img/sharp-linuxmusl-arm64": "0.35.3", "@img/sharp-linuxmusl-x64": "0.35.3", "@img/sharp-webcontainers-wasm32": "0.35.3", "@img/sharp-win32-arm64": "0.35.3", "@img/sharp-win32-ia32": "0.35.3", "@img/sharp-win32-x64": "0.35.3" }, "peerDependencies": { "@types/node": "*" }, "optionalPeers": ["@types/node"] }, "sha512-ej0zVHuZGHCiABXcNxeYhpRnPNPAcvbG8RMdBAhDAxLKkCRVSpK3Iyu7qbqw3JMzoj0REeM6f3tJLtVwl0023Q=="],
"blume/astro/unifont": ["unifont@0.7.4", "", { "dependencies": { "css-tree": "^3.1.0", "ofetch": "^1.5.1", "ohash": "^2.0.11" } }, "sha512-oHeis4/xl42HUIeHuNZRGEvxj5AaIKR+bHPNegRq5LV1gdc3jundpONbjglKpihmJf+dswygdMJn3eftGIMemg=="], "blume/astro/unifont": ["unifont@0.7.4", "", { "dependencies": { "css-tree": "^3.1.0", "ofetch": "^1.5.1", "ohash": "^2.0.11" } }, "sha512-oHeis4/xl42HUIeHuNZRGEvxj5AaIKR+bHPNegRq5LV1gdc3jundpONbjglKpihmJf+dswygdMJn3eftGIMemg=="],
"blume/astro/unstorage": ["unstorage@1.17.5", "", { "dependencies": { "anymatch": "^3.1.3", "chokidar": "^5.0.0", "destr": "^2.0.5", "h3": "^1.15.10", "lru-cache": "^11.2.7", "node-fetch-native": "^1.6.7", "ofetch": "^1.5.1", "ufo": "^1.6.3" }, "peerDependencies": { "@azure/app-configuration": "^1.8.0", "@azure/cosmos": "^4.2.0", "@azure/data-tables": "^13.3.0", "@azure/identity": "^4.6.0", "@azure/keyvault-secrets": "^4.9.0", "@azure/storage-blob": "^12.26.0", "@capacitor/preferences": "^6 || ^7 || ^8", "@deno/kv": ">=0.9.0", "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", "@planetscale/database": "^1.19.0", "@upstash/redis": "^1.34.3", "@vercel/blob": ">=0.27.1", "@vercel/functions": "^2.2.12 || ^3.0.0", "@vercel/kv": "^1 || ^2 || ^3", "aws4fetch": "^1.0.20", "db0": ">=0.2.1", "idb-keyval": "^6.2.1", "ioredis": "^5.4.2", "uploadthing": "^7.4.4" }, "optionalPeers": ["@azure/app-configuration", "@azure/cosmos", "@azure/data-tables", "@azure/identity", "@azure/keyvault-secrets", "@azure/storage-blob", "@capacitor/preferences", "@deno/kv", "@netlify/blobs", "@planetscale/database", "@upstash/redis", "@vercel/blob", "@vercel/functions", "@vercel/kv", "aws4fetch", "db0", "idb-keyval", "ioredis", "uploadthing"] }, "sha512-0i3iqvRfx29hkNntHyQvJTpf5W9dQ9ZadSoRU8+xVlhVtT7jAX57fazYO9EHvcRCfBCyi5YRya7XCDOsbTgkPg=="], "blume/astro/unstorage": ["unstorage@1.17.5", "", { "dependencies": { "anymatch": "^3.1.3", "chokidar": "^5.0.0", "destr": "^2.0.5", "h3": "^1.15.10", "lru-cache": "^11.2.7", "node-fetch-native": "^1.6.7", "ofetch": "^1.5.1", "ufo": "^1.6.3" }, "peerDependencies": { "@azure/app-configuration": "^1.8.0", "@azure/cosmos": "^4.2.0", "@azure/data-tables": "^13.3.0", "@azure/identity": "^4.6.0", "@azure/keyvault-secrets": "^4.9.0", "@azure/storage-blob": "^12.26.0", "@capacitor/preferences": "^6 || ^7 || ^8", "@deno/kv": ">=0.9.0", "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", "@planetscale/database": "^1.19.0", "@upstash/redis": "^1.34.3", "@vercel/blob": ">=0.27.1", "@vercel/functions": "^2.2.12 || ^3.0.0", "@vercel/kv": "^1 || ^2 || ^3", "aws4fetch": "^1.0.20", "db0": ">=0.2.1", "idb-keyval": "^6.2.1", "ioredis": "^5.4.2", "uploadthing": "^7.4.4" }, "optionalPeers": ["@azure/app-configuration", "@azure/cosmos", "@azure/data-tables", "@azure/identity", "@azure/keyvault-secrets", "@azure/storage-blob", "@capacitor/preferences", "@deno/kv", "@netlify/blobs", "@planetscale/database", "@upstash/redis", "@vercel/blob", "@vercel/functions", "@vercel/kv", "aws4fetch", "db0", "idb-keyval", "ioredis", "uploadthing"] }, "sha512-0i3iqvRfx29hkNntHyQvJTpf5W9dQ9ZadSoRU8+xVlhVtT7jAX57fazYO9EHvcRCfBCyi5YRya7XCDOsbTgkPg=="],
@@ -7549,7 +7696,7 @@
"blume/astro/yargs-parser": ["yargs-parser@22.0.0", "", {}, "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw=="], "blume/astro/yargs-parser": ["yargs-parser@22.0.0", "", {}, "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw=="],
"blume/astro/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], "blume/chokidar/readdirp": ["readdirp@5.1.1", "", {}, "sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA=="],
"blume/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], "blume/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
@@ -7559,6 +7706,42 @@
"blume/react-dom/scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], "blume/react-dom/scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
"blume/sharp/@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.3.2" }, "os": "darwin", "cpu": "arm64" }, "sha512-RMnFX7YQsMoh7lWfcM4NEHHymBX/rLuKNPVM84XE9ONPcaSCDgE7CHIHpSgPcO2xcRthgBy1HfNO319mwhIAkg=="],
"blume/sharp/@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.3.2" }, "os": "darwin", "cpu": "x64" }, "sha512-Xo+5uFBtLN0BKqieTxiFzFPQAUlBbbH5iBKyRX/z1JrbnYsHTfKJnUfL8+p2TPXr1pXqao4eeL4Rl144uDpK9w=="],
"blume/sharp/@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.3.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-9J6ypZFpQBj4YnePGoq/S38w6nz+vqg5WZLrLGY4YuSemdMq47GMLBPO42MzwdGwpg/agZ7xzZcFHa48xlywfg=="],
"blume/sharp/@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.3.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-m2pW1n6cns9VaubNwsZ+c3CRYjxNQWgJ5gPlnL1nbBcpkBvFm6SCFN5o0psFHI8w9n11NKhFkeEDns98tiqbEw=="],
"blume/sharp/@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.3.2", "", { "os": "linux", "cpu": "arm" }, "sha512-1eMLzy92I4J6rmi4mAT8yC3HxOtniyGELlzGbNMLLeqe052ahFQ0h6LFq+lh5DsDIdYViIDst08abvSbcEdLXQ=="],
"blume/sharp/@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.3.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-dqVSFynCox4C/J8kT16V7SIFAns0IjgLwkvYT7p8LQVmJ5OS5b6tI9IGflxTeuBS//zXeFIUbwt5dwxyZ17cnA=="],
"blume/sharp/@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.3.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-/ABshyj8gCpyIrNXnHn4LorDJ0HHm1VhXPBlxZ8zAtfVPAaSafXPGn+sUSIRiwaSBy0mmFjSjiXI5mkcwdChKQ=="],
"blume/sharp/@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-ITPEtgffGJ0S6G9dRyw/366tJQqFRcHWPHhC+Stpg3Z8AEMrDrTr2lhdz4f/Y/HMbRh//7Z5mBzEpVdi62Oc3w=="],
"blume/sharp/@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.3.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-zE9EdiUzUmg5mDT5a1rk5fYJ6GWPloTwWBYDS14naqHsL+EaMpDj1AWnpLgh3u0YCORv2Tt50wrcrpYqkP97Kw=="],
"blume/sharp/@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-m0lrLiUt+lBYnCFr8qV/65yMR4E/c7/wf78I5eKTdkEakFAlZ9QlzEM3QIhhAwVeUhLAHLcCq7a7Vszq/oFNZQ=="],
"blume/sharp/@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.3.2" }, "os": "linux", "cpu": "arm" }, "sha512-affVWCTLooy8TSxbDx2qkzuDeaWLNVBA+P//FNBirHsXpP2fuBhk5AuboYUnrDnzoXes8GFjpTx0SBFOCRg+FA=="],
"blume/sharp/@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.3.2" }, "os": "linux", "cpu": "arm64" }, "sha512-QgKDspHPnrU+GQ55XPhGwyhC8acLVOOSyAvo1oVfFmrIXLkDNmGWzAfDZ4xK8oSA1qBQrALcHX0G5UZni/SuFQ=="],
"blume/sharp/@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.3.2" }, "os": "linux", "cpu": "s390x" }, "sha512-KgAxQ0DxpNOq1rG2t5cgTgShJFGSuU7XO45cqC+1NVOuZnP6tlgZRuSYOfNupGkHID0o3cJOsw4DVeJpMovcGw=="],
"blume/sharp/@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.3.2" }, "os": "linux", "cpu": "x64" }, "sha512-8pqvxubL2PGdhlPy6GLqzDYMUjyRmKAwKHYKixpdJYBUK7PJ0C029XdsnpFIdgRZG68fZiGdHVWcKPvtiPB4cA=="],
"blume/sharp/@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.3.2" }, "os": "linux", "cpu": "arm64" }, "sha512-Vz0iQjzzcSX3HCbfwFfCSG/9SCIqyO0mH2sXyiHaAYfBk0cRsCWXRyQYX0ovCK/PAQBbTzQ0dsPQHh5MAFL59w=="],
"blume/sharp/@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.3.2" }, "os": "linux", "cpu": "x64" }, "sha512-6O1NPKcDVj9QEdg7Hx549EX8U0rp6yXQERqru6yRN7fGBn32UvIRJUlWnk+8xDCiG76hXVBbX82NZ/ZKr0euIg=="],
"blume/sharp/@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.35.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-r53mXsBN6lFUDiST764SvgwUdHAqM4rPAiDzAmf4fLoB6X/rkfyTrLCg6+g17wJJiCmB3JYgHuUldCWUIRFSXw=="],
"blume/sharp/@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.35.3", "", { "os": "win32", "cpu": "x64" }, "sha512-D4y1vNeZrIIJCN+uHaWVtH86B+aCrdMYYjicy9pXHvbGZeGYLLSd3wdVuC37FxVXlU1ARsk84eKWfWMXGYEqvA=="],
"builder-util/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], "builder-util/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
"cliui/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], "cliui/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
@@ -7627,8 +7810,6 @@
"lazystream/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], "lazystream/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
"mermaid/katex/commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="],
"p-locate/p-limit/yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], "p-locate/p-limit/yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
"pkg-dir/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], "pkg-dir/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="],
@@ -8115,6 +8296,8 @@
"@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], "@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="],
"@aws-sdk/client-sts/@aws-sdk/core/fast-xml-parser/strnum": ["strnum@1.1.2", "", {}, "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA=="],
"@aws-sdk/client-sts/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.782.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.775.0", "@aws-sdk/middleware-host-header": "3.775.0", "@aws-sdk/middleware-logger": "3.775.0", "@aws-sdk/middleware-recursion-detection": "3.775.0", "@aws-sdk/middleware-user-agent": "3.782.0", "@aws-sdk/region-config-resolver": "3.775.0", "@aws-sdk/types": "3.775.0", "@aws-sdk/util-endpoints": "3.782.0", "@aws-sdk/util-user-agent-browser": "3.775.0", "@aws-sdk/util-user-agent-node": "3.782.0", "@smithy/config-resolver": "^4.1.0", "@smithy/core": "^3.2.0", "@smithy/fetch-http-handler": "^5.0.2", "@smithy/hash-node": "^4.0.2", "@smithy/invalid-dependency": "^4.0.2", "@smithy/middleware-content-length": "^4.0.2", "@smithy/middleware-endpoint": "^4.1.0", "@smithy/middleware-retry": "^4.1.0", "@smithy/middleware-serde": "^4.0.3", "@smithy/middleware-stack": "^4.0.2", "@smithy/node-config-provider": "^4.0.2", "@smithy/node-http-handler": "^4.0.4", "@smithy/protocol-http": "^5.1.0", "@smithy/smithy-client": "^4.2.0", "@smithy/types": "^4.2.0", "@smithy/url-parser": "^4.0.2", "@smithy/util-base64": "^4.0.0", "@smithy/util-body-length-browser": "^4.0.0", "@smithy/util-body-length-node": "^4.0.0", "@smithy/util-defaults-mode-browser": "^4.0.8", "@smithy/util-defaults-mode-node": "^4.0.8", "@smithy/util-endpoints": "^3.0.2", "@smithy/util-middleware": "^4.0.2", "@smithy/util-retry": "^4.0.2", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-QOYC8q7luzHFXrP0xYAqBctoPkynjfV0r9dqntFu4/IWMTyC1vlo1UTxFAjIPyclYw92XJyEkVCVg9v/nQnsUA=="], "@aws-sdk/client-sts/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.782.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.775.0", "@aws-sdk/middleware-host-header": "3.775.0", "@aws-sdk/middleware-logger": "3.775.0", "@aws-sdk/middleware-recursion-detection": "3.775.0", "@aws-sdk/middleware-user-agent": "3.782.0", "@aws-sdk/region-config-resolver": "3.775.0", "@aws-sdk/types": "3.775.0", "@aws-sdk/util-endpoints": "3.782.0", "@aws-sdk/util-user-agent-browser": "3.775.0", "@aws-sdk/util-user-agent-node": "3.782.0", "@smithy/config-resolver": "^4.1.0", "@smithy/core": "^3.2.0", "@smithy/fetch-http-handler": "^5.0.2", "@smithy/hash-node": "^4.0.2", "@smithy/invalid-dependency": "^4.0.2", "@smithy/middleware-content-length": "^4.0.2", "@smithy/middleware-endpoint": "^4.1.0", "@smithy/middleware-retry": "^4.1.0", "@smithy/middleware-serde": "^4.0.3", "@smithy/middleware-stack": "^4.0.2", "@smithy/node-config-provider": "^4.0.2", "@smithy/node-http-handler": "^4.0.4", "@smithy/protocol-http": "^5.1.0", "@smithy/smithy-client": "^4.2.0", "@smithy/types": "^4.2.0", "@smithy/url-parser": "^4.0.2", "@smithy/util-base64": "^4.0.0", "@smithy/util-body-length-browser": "^4.0.0", "@smithy/util-body-length-node": "^4.0.0", "@smithy/util-defaults-mode-browser": "^4.0.8", "@smithy/util-defaults-mode-node": "^4.0.8", "@smithy/util-endpoints": "^3.0.2", "@smithy/util-middleware": "^4.0.2", "@smithy/util-retry": "^4.0.2", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-QOYC8q7luzHFXrP0xYAqBctoPkynjfV0r9dqntFu4/IWMTyC1vlo1UTxFAjIPyclYw92XJyEkVCVg9v/nQnsUA=="],
"@aws-sdk/client-sts/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/client-sso": ["@aws-sdk/client-sso@3.782.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.775.0", "@aws-sdk/middleware-host-header": "3.775.0", "@aws-sdk/middleware-logger": "3.775.0", "@aws-sdk/middleware-recursion-detection": "3.775.0", "@aws-sdk/middleware-user-agent": "3.782.0", "@aws-sdk/region-config-resolver": "3.775.0", "@aws-sdk/types": "3.775.0", "@aws-sdk/util-endpoints": "3.782.0", "@aws-sdk/util-user-agent-browser": "3.775.0", "@aws-sdk/util-user-agent-node": "3.782.0", "@smithy/config-resolver": "^4.1.0", "@smithy/core": "^3.2.0", "@smithy/fetch-http-handler": "^5.0.2", "@smithy/hash-node": "^4.0.2", "@smithy/invalid-dependency": "^4.0.2", "@smithy/middleware-content-length": "^4.0.2", "@smithy/middleware-endpoint": "^4.1.0", "@smithy/middleware-retry": "^4.1.0", "@smithy/middleware-serde": "^4.0.3", "@smithy/middleware-stack": "^4.0.2", "@smithy/node-config-provider": "^4.0.2", "@smithy/node-http-handler": "^4.0.4", "@smithy/protocol-http": "^5.1.0", "@smithy/smithy-client": "^4.2.0", "@smithy/types": "^4.2.0", "@smithy/url-parser": "^4.0.2", "@smithy/util-base64": "^4.0.0", "@smithy/util-body-length-browser": "^4.0.0", "@smithy/util-body-length-node": "^4.0.0", "@smithy/util-defaults-mode-browser": "^4.0.8", "@smithy/util-defaults-mode-node": "^4.0.8", "@smithy/util-endpoints": "^3.0.2", "@smithy/util-middleware": "^4.0.2", "@smithy/util-retry": "^4.0.2", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-5GlJBejo8wqMpSSEKb45WE82YxI2k73YuebjLH/eWDNQeE6VI5Bh9lA1YQ7xNkLLH8hIsb0pSfKVuwh0VEzVrg=="], "@aws-sdk/client-sts/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/client-sso": ["@aws-sdk/client-sso@3.782.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.775.0", "@aws-sdk/middleware-host-header": "3.775.0", "@aws-sdk/middleware-logger": "3.775.0", "@aws-sdk/middleware-recursion-detection": "3.775.0", "@aws-sdk/middleware-user-agent": "3.782.0", "@aws-sdk/region-config-resolver": "3.775.0", "@aws-sdk/types": "3.775.0", "@aws-sdk/util-endpoints": "3.782.0", "@aws-sdk/util-user-agent-browser": "3.775.0", "@aws-sdk/util-user-agent-node": "3.782.0", "@smithy/config-resolver": "^4.1.0", "@smithy/core": "^3.2.0", "@smithy/fetch-http-handler": "^5.0.2", "@smithy/hash-node": "^4.0.2", "@smithy/invalid-dependency": "^4.0.2", "@smithy/middleware-content-length": "^4.0.2", "@smithy/middleware-endpoint": "^4.1.0", "@smithy/middleware-retry": "^4.1.0", "@smithy/middleware-serde": "^4.0.3", "@smithy/middleware-stack": "^4.0.2", "@smithy/node-config-provider": "^4.0.2", "@smithy/node-http-handler": "^4.0.4", "@smithy/protocol-http": "^5.1.0", "@smithy/smithy-client": "^4.2.0", "@smithy/types": "^4.2.0", "@smithy/url-parser": "^4.0.2", "@smithy/util-base64": "^4.0.0", "@smithy/util-body-length-browser": "^4.0.0", "@smithy/util-body-length-node": "^4.0.0", "@smithy/util-defaults-mode-browser": "^4.0.8", "@smithy/util-defaults-mode-node": "^4.0.8", "@smithy/util-endpoints": "^3.0.2", "@smithy/util-middleware": "^4.0.2", "@smithy/util-retry": "^4.0.2", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-5GlJBejo8wqMpSSEKb45WE82YxI2k73YuebjLH/eWDNQeE6VI5Bh9lA1YQ7xNkLLH8hIsb0pSfKVuwh0VEzVrg=="],
@@ -8479,6 +8662,8 @@
"@solidjs/start/shiki/@shikijs/engine-javascript/oniguruma-to-es": ["oniguruma-to-es@2.3.0", "", { "dependencies": { "emoji-regex-xs": "^1.0.0", "regex": "^5.1.1", "regex-recursion": "^5.1.1" } }, "sha512-bwALDxriqfKGfUufKGGepCzu9x7nJQuoRoAFp4AnwehhC2crqrDIAP/uN2qdlsAvSMpeRC3+Yzhqc7hLmle5+g=="], "@solidjs/start/shiki/@shikijs/engine-javascript/oniguruma-to-es": ["oniguruma-to-es@2.3.0", "", { "dependencies": { "emoji-regex-xs": "^1.0.0", "regex": "^5.1.1", "regex-recursion": "^5.1.1" } }, "sha512-bwALDxriqfKGfUufKGGepCzu9x7nJQuoRoAFp4AnwehhC2crqrDIAP/uN2qdlsAvSMpeRC3+Yzhqc7hLmle5+g=="],
"@stoplight/spectral-core/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
"ansi-align/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "ansi-align/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"app-builder-lib/@electron/get/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="], "app-builder-lib/@electron/get/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="],
@@ -8505,6 +8690,8 @@
"blume/@shikijs/transformers/@shikijs/core/@shikijs/primitive": ["@shikijs/primitive@4.4.2", "", { "dependencies": { "@shikijs/types": "4.4.2", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.5" } }, "sha512-l6fQQKsOMlz72n38fztmSgZ76MO6KSWuw8o+GJ+FhmqrpC9pIOJNQNXGgbb5yX2AwpzlEHwsaLPnk/8o4Fm+rA=="], "blume/@shikijs/transformers/@shikijs/core/@shikijs/primitive": ["@shikijs/primitive@4.4.2", "", { "dependencies": { "@shikijs/types": "4.4.2", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.5" } }, "sha512-l6fQQKsOMlz72n38fztmSgZ76MO6KSWuw8o+GJ+FhmqrpC9pIOJNQNXGgbb5yX2AwpzlEHwsaLPnk/8o4Fm+rA=="],
"blume/ai/@ai-sdk/provider-utils/undici": ["undici@7.29.0", "", {}, "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw=="],
"blume/astro/@astrojs/telemetry/is-docker": ["is-docker@4.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-LHE+wROyG/Y/0ZnbktRCoTix2c1RhgWaZraMZ8o1Q7zCh0VSrICJQO5oqIIISrcSBtrXv0o233w1IYwsWCjTzA=="], "blume/astro/@astrojs/telemetry/is-docker": ["is-docker@4.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-LHE+wROyG/Y/0ZnbktRCoTix2c1RhgWaZraMZ8o1Q7zCh0VSrICJQO5oqIIISrcSBtrXv0o233w1IYwsWCjTzA=="],
"blume/astro/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ=="], "blume/astro/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ=="],
@@ -8561,46 +8748,8 @@
"blume/astro/p-queue/p-timeout": ["p-timeout@7.0.1", "", {}, "sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg=="], "blume/astro/p-queue/p-timeout": ["p-timeout@7.0.1", "", {}, "sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg=="],
"blume/astro/sharp/@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.3.2" }, "os": "darwin", "cpu": "arm64" }, "sha512-RMnFX7YQsMoh7lWfcM4NEHHymBX/rLuKNPVM84XE9ONPcaSCDgE7CHIHpSgPcO2xcRthgBy1HfNO319mwhIAkg=="],
"blume/astro/sharp/@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.3.2" }, "os": "darwin", "cpu": "x64" }, "sha512-Xo+5uFBtLN0BKqieTxiFzFPQAUlBbbH5iBKyRX/z1JrbnYsHTfKJnUfL8+p2TPXr1pXqao4eeL4Rl144uDpK9w=="],
"blume/astro/sharp/@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.3.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-9J6ypZFpQBj4YnePGoq/S38w6nz+vqg5WZLrLGY4YuSemdMq47GMLBPO42MzwdGwpg/agZ7xzZcFHa48xlywfg=="],
"blume/astro/sharp/@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.3.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-m2pW1n6cns9VaubNwsZ+c3CRYjxNQWgJ5gPlnL1nbBcpkBvFm6SCFN5o0psFHI8w9n11NKhFkeEDns98tiqbEw=="],
"blume/astro/sharp/@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.3.2", "", { "os": "linux", "cpu": "arm" }, "sha512-1eMLzy92I4J6rmi4mAT8yC3HxOtniyGELlzGbNMLLeqe052ahFQ0h6LFq+lh5DsDIdYViIDst08abvSbcEdLXQ=="],
"blume/astro/sharp/@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.3.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-dqVSFynCox4C/J8kT16V7SIFAns0IjgLwkvYT7p8LQVmJ5OS5b6tI9IGflxTeuBS//zXeFIUbwt5dwxyZ17cnA=="],
"blume/astro/sharp/@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.3.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-/ABshyj8gCpyIrNXnHn4LorDJ0HHm1VhXPBlxZ8zAtfVPAaSafXPGn+sUSIRiwaSBy0mmFjSjiXI5mkcwdChKQ=="],
"blume/astro/sharp/@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-ITPEtgffGJ0S6G9dRyw/366tJQqFRcHWPHhC+Stpg3Z8AEMrDrTr2lhdz4f/Y/HMbRh//7Z5mBzEpVdi62Oc3w=="],
"blume/astro/sharp/@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.3.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-zE9EdiUzUmg5mDT5a1rk5fYJ6GWPloTwWBYDS14naqHsL+EaMpDj1AWnpLgh3u0YCORv2Tt50wrcrpYqkP97Kw=="],
"blume/astro/sharp/@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-m0lrLiUt+lBYnCFr8qV/65yMR4E/c7/wf78I5eKTdkEakFAlZ9QlzEM3QIhhAwVeUhLAHLcCq7a7Vszq/oFNZQ=="],
"blume/astro/sharp/@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.3.2" }, "os": "linux", "cpu": "arm" }, "sha512-affVWCTLooy8TSxbDx2qkzuDeaWLNVBA+P//FNBirHsXpP2fuBhk5AuboYUnrDnzoXes8GFjpTx0SBFOCRg+FA=="],
"blume/astro/sharp/@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.3.2" }, "os": "linux", "cpu": "arm64" }, "sha512-QgKDspHPnrU+GQ55XPhGwyhC8acLVOOSyAvo1oVfFmrIXLkDNmGWzAfDZ4xK8oSA1qBQrALcHX0G5UZni/SuFQ=="],
"blume/astro/sharp/@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.3.2" }, "os": "linux", "cpu": "s390x" }, "sha512-KgAxQ0DxpNOq1rG2t5cgTgShJFGSuU7XO45cqC+1NVOuZnP6tlgZRuSYOfNupGkHID0o3cJOsw4DVeJpMovcGw=="],
"blume/astro/sharp/@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.3.2" }, "os": "linux", "cpu": "x64" }, "sha512-8pqvxubL2PGdhlPy6GLqzDYMUjyRmKAwKHYKixpdJYBUK7PJ0C029XdsnpFIdgRZG68fZiGdHVWcKPvtiPB4cA=="],
"blume/astro/sharp/@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.3.2" }, "os": "linux", "cpu": "arm64" }, "sha512-Vz0iQjzzcSX3HCbfwFfCSG/9SCIqyO0mH2sXyiHaAYfBk0cRsCWXRyQYX0ovCK/PAQBbTzQ0dsPQHh5MAFL59w=="],
"blume/astro/sharp/@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.3.2" }, "os": "linux", "cpu": "x64" }, "sha512-6O1NPKcDVj9QEdg7Hx549EX8U0rp6yXQERqru6yRN7fGBn32UvIRJUlWnk+8xDCiG76hXVBbX82NZ/ZKr0euIg=="],
"blume/astro/sharp/@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.35.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-r53mXsBN6lFUDiST764SvgwUdHAqM4rPAiDzAmf4fLoB6X/rkfyTrLCg6+g17wJJiCmB3JYgHuUldCWUIRFSXw=="],
"blume/astro/sharp/@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.35.3", "", { "os": "win32", "cpu": "x64" }, "sha512-D4y1vNeZrIIJCN+uHaWVtH86B+aCrdMYYjicy9pXHvbGZeGYLLSd3wdVuC37FxVXlU1ARsk84eKWfWMXGYEqvA=="],
"blume/astro/unifont/ofetch": ["ofetch@1.5.1", "", { "dependencies": { "destr": "^2.0.5", "node-fetch-native": "^1.6.7", "ufo": "^1.6.1" } }, "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA=="], "blume/astro/unifont/ofetch": ["ofetch@1.5.1", "", { "dependencies": { "destr": "^2.0.5", "node-fetch-native": "^1.6.7", "ufo": "^1.6.1" } }, "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA=="],
"blume/astro/unstorage/chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="],
"blume/astro/unstorage/h3": ["h3@1.15.11", "", { "dependencies": { "cookie-es": "^1.2.3", "crossws": "^0.3.5", "defu": "^6.1.6", "destr": "^2.0.5", "iron-webcrypto": "^1.2.1", "node-mock-http": "^1.0.4", "radix3": "^1.1.2", "ufo": "^1.6.3", "uncrypto": "^0.1.3" } }, "sha512-L3THSe2MPeBwgIZVSH5zLdBBU90TOxarvhK9d04IDY2AmVS8j2Jz2LIWtwsGOU3lu2I5jCN7FNvVfY2+XyF+mg=="], "blume/astro/unstorage/h3": ["h3@1.15.11", "", { "dependencies": { "cookie-es": "^1.2.3", "crossws": "^0.3.5", "defu": "^6.1.6", "destr": "^2.0.5", "iron-webcrypto": "^1.2.1", "node-mock-http": "^1.0.4", "radix3": "^1.1.2", "ufo": "^1.6.3", "uncrypto": "^0.1.3" } }, "sha512-L3THSe2MPeBwgIZVSH5zLdBBU90TOxarvhK9d04IDY2AmVS8j2Jz2LIWtwsGOU3lu2I5jCN7FNvVfY2+XyF+mg=="],
"blume/astro/unstorage/ofetch": ["ofetch@1.5.1", "", { "dependencies": { "destr": "^2.0.5", "node-fetch-native": "^1.6.7", "ufo": "^1.6.1" } }, "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA=="], "blume/astro/unstorage/ofetch": ["ofetch@1.5.1", "", { "dependencies": { "destr": "^2.0.5", "node-fetch-native": "^1.6.7", "ufo": "^1.6.1" } }, "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA=="],
@@ -9059,8 +9208,6 @@
"babel-plugin-module-resolver/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], "babel-plugin-module-resolver/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
"blume/astro/unstorage/chokidar/readdirp": ["readdirp@5.1.1", "", {}, "sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA=="],
"blume/astro/unstorage/h3/cookie-es": ["cookie-es@1.2.3", "", {}, "sha512-lXVyvUvrNXblMqzIRrxHb57UUVmqsSWlxqt3XIjCkUP0wDAf6uicO6KMbEgYrMNtEvWgWHwe42CKxPu9MYAnWw=="], "blume/astro/unstorage/h3/cookie-es": ["cookie-es@1.2.3", "", {}, "sha512-lXVyvUvrNXblMqzIRrxHb57UUVmqsSWlxqt3XIjCkUP0wDAf6uicO6KMbEgYrMNtEvWgWHwe42CKxPu9MYAnWw=="],
"blume/astro/unstorage/h3/crossws": ["crossws@0.3.5", "", { "dependencies": { "uncrypto": "^0.1.3" } }, "sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA=="], "blume/astro/unstorage/h3/crossws": ["crossws@0.3.5", "", { "dependencies": { "uncrypto": "^0.1.3" } }, "sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA=="],
+1 -1
View File
@@ -2,7 +2,7 @@
exact = true exact = true
# Only install newly resolved package versions published at least 3 days ago. # Only install newly resolved package versions published at least 3 days ago.
minimumReleaseAge = 259200 minimumReleaseAge = 259200
minimumReleaseAgeExcludes = ["@ai-sdk/amazon-bedrock", "@ai-sdk/anthropic", "@opencode-ai/sdk", "@opentui/core", "@opentui/core-darwin-arm64", "@opentui/core-darwin-x64", "@opentui/core-linux-arm64", "@opentui/core-linux-arm64-musl", "@opentui/core-linux-x64", "@opentui/core-linux-x64-musl", "@opentui/core-win32-arm64", "@opentui/core-win32-x64", "@opentui/keymap", "@opentui/solid", "opentui-spinner", "gitlab-ai-provider", "opencode-gitlab-auth", "@ff-labs/fff-node", "@ff-labs/fff-bun", "@ff-labs/fff-bin-darwin-arm64", "@ff-labs/fff-bin-darwin-x64", "@ff-labs/fff-bin-linux-arm64-gnu", "@ff-labs/fff-bin-linux-arm64-musl", "@ff-labs/fff-bin-linux-x64-gnu", "@ff-labs/fff-bin-linux-x64-musl", "@ff-labs/fff-bin-win32-arm64", "@ff-labs/fff-bin-win32-x64", "@pierre/diffs", "@pierre/theming", "app-builder-lib", "dmg-builder", "electron-builder", "electron-publish"] minimumReleaseAgeExcludes = ["@ai-sdk/amazon-bedrock", "@ai-sdk/anthropic", "@opencode-ai/sdk", "@opentui/core", "@opentui/core-darwin-arm64", "@opentui/core-darwin-x64", "@opentui/core-linux-arm64", "@opentui/core-linux-arm64-musl", "@opentui/core-linux-x64", "@opentui/core-linux-x64-musl", "@opentui/core-win32-arm64", "@opentui/core-win32-x64", "@opentui/keymap", "@opentui/solid", "opentui-spinner", "gitlab-ai-provider", "opencode-gitlab-auth", "@ff-labs/fff-node", "@ff-labs/fff-bun", "@ff-labs/fff-bin-darwin-arm64", "@ff-labs/fff-bin-darwin-x64", "@ff-labs/fff-bin-linux-arm64-gnu", "@ff-labs/fff-bin-linux-arm64-musl", "@ff-labs/fff-bin-linux-x64-gnu", "@ff-labs/fff-bin-linux-x64-musl", "@ff-labs/fff-bin-win32-arm64", "@ff-labs/fff-bin-win32-x64", "@pierre/diffs", "@pierre/theming", "app-builder-lib", "dmg-builder", "electron-builder", "electron-publish", "blume"]
[test] [test]
root = "./do-not-run-tests-from-root" root = "./do-not-run-tests-from-root"
+4 -4
View File
@@ -15,13 +15,13 @@ Usage: install.sh [options]
Options: Options:
-h, --help Display this help message -h, --help Display this help message
-v, --version <version> Install a specific version (e.g., 0.0.0-next-17236) -v, --version <version> Install a specific version (e.g., 0.0.0-beta-17236)
-b, --binary <path> Install from a local binary instead of downloading -b, --binary <path> Install from a local binary instead of downloading
--no-modify-path Don't modify shell config files (.zshrc, .bashrc, etc.) --no-modify-path Don't modify shell config files (.zshrc, .bashrc, etc.)
Examples: Examples:
curl -fsSL https://raw.githubusercontent.com/anomalyco/opencode/v2/install | bash curl -fsSL https://opencode.ai/v2/install | bash
curl -fsSL https://raw.githubusercontent.com/anomalyco/opencode/v2/install | bash -s -- --version 0.0.0-next-17236 curl -fsSL https://opencode.ai/v2/install | bash -s -- --version 0.0.0-beta-17236
./install --binary /path/to/opencode2 ./install --binary /path/to/opencode2
EOF EOF
} }
@@ -166,7 +166,7 @@ else
fi fi
if [ -z "$requested_version" ]; then if [ -z "$requested_version" ]; then
metadata=$(curl -fsSL https://registry.npmjs.org/@opencode-ai%2fcli/next || true) metadata=$(curl -fsSL https://registry.npmjs.org/@opencode-ai%2fcli/beta || true)
specific_version=$(echo "$metadata" | sed -n 's/.*"version":"\([^"]*\)".*/\1/p') specific_version=$(echo "$metadata" | sed -n 's/.*"version":"\([^"]*\)".*/\1/p')
if [ -z "$specific_version" ]; then if [ -z "$specific_version" ]; then
+8 -6
View File
@@ -8,7 +8,7 @@
"packageManager": "bun@1.3.14", "packageManager": "bun@1.3.14",
"scripts": { "scripts": {
"dev": "bun run --cwd packages/cli --conditions=browser src/index.ts", "dev": "bun run --cwd packages/cli --conditions=browser src/index.ts",
"dev:live": "OPENCODE_TUI_CHANNEL=next OPENCODE_PASSWORD=\"$(opencode2 service get password)\" bun run dev --server \"$(opencode2 service status)\"", "dev:live": "OPENCODE_TUI_CHANNEL=dev OPENCODE_PASSWORD=\"$(opencode2 service get password)\" bun run dev --server \"$(opencode2 service status)\"",
"dev:desktop": "bun --cwd packages/desktop dev", "dev:desktop": "bun --cwd packages/desktop dev",
"dev:web": "bun --cwd packages/app dev", "dev:web": "bun --cwd packages/app dev",
"dev:console": "ulimit -n 10240 2>/dev/null; bun run --cwd packages/console/app dev", "dev:console": "ulimit -n 10240 2>/dev/null; bun run --cwd packages/console/app dev",
@@ -37,9 +37,10 @@
"packages/slack" "packages/slack"
], ],
"catalog": { "catalog": {
"@effect/opentelemetry": "4.0.0-beta.101", "@effect/opentelemetry": "4.0.0-beta.107",
"@effect/platform-node": "4.0.0-beta.101", "@effect/platform-node": "4.0.0-beta.107",
"@effect/sql-sqlite-bun": "4.0.0-beta.101", "@effect/platform-node-shared": "4.0.0-beta.107",
"@effect/sql-sqlite-bun": "4.0.0-beta.107",
"@npmcli/arborist": "9.4.0", "@npmcli/arborist": "9.4.0",
"@types/bun": "1.3.13", "@types/bun": "1.3.13",
"@types/cross-spawn": "6.0.6", "@types/cross-spawn": "6.0.6",
@@ -70,7 +71,7 @@
"dompurify": "3.3.1", "dompurify": "3.3.1",
"drizzle-kit": "1.0.0-rc.2", "drizzle-kit": "1.0.0-rc.2",
"drizzle-orm": "1.0.0-rc.2", "drizzle-orm": "1.0.0-rc.2",
"effect": "4.0.0-beta.101", "effect": "4.0.0-beta.107",
"ai": "6.0.168", "ai": "6.0.168",
"cross-spawn": "7.0.6", "cross-spawn": "7.0.6",
"hono": "4.10.7", "hono": "4.10.7",
@@ -154,6 +155,7 @@
"@opentui/core": "catalog:", "@opentui/core": "catalog:",
"@opentui/keymap": "catalog:", "@opentui/keymap": "catalog:",
"@opentui/solid": "catalog:", "@opentui/solid": "catalog:",
"@effect/platform-node-shared": "catalog:",
"@types/bun": "catalog:", "@types/bun": "catalog:",
"@types/node": "catalog:", "@types/node": "catalog:",
"effect": "catalog:" "effect": "catalog:"
@@ -164,6 +166,7 @@
"@npmcli/agent@4.0.2": "patches/@npmcli%2Fagent@4.0.2.patch", "@npmcli/agent@4.0.2": "patches/@npmcli%2Fagent@4.0.2.patch",
"@silvia-odwyer/photon-node@0.3.4": "patches/@silvia-odwyer%2Fphoton-node@0.3.4.patch", "@silvia-odwyer/photon-node@0.3.4": "patches/@silvia-odwyer%2Fphoton-node@0.3.4.patch",
"@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch", "@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch",
"drizzle-orm@1.0.0-rc.2": "patches/drizzle-orm@1.0.0-rc.2.patch",
"solid-js@1.9.10": "patches/solid-js@1.9.10.patch", "solid-js@1.9.10": "patches/solid-js@1.9.10.patch",
"@ai-sdk/mistral@3.0.51": "patches/@ai-sdk%2Fmistral@3.0.51.patch", "@ai-sdk/mistral@3.0.51": "patches/@ai-sdk%2Fmistral@3.0.51.patch",
"gcp-metadata@8.1.2": "patches/gcp-metadata@8.1.2.patch", "gcp-metadata@8.1.2": "patches/gcp-metadata@8.1.2.patch",
@@ -171,7 +174,6 @@
"@ai-sdk/google@3.0.73": "patches/@ai-sdk%2Fgoogle@3.0.73.patch", "@ai-sdk/google@3.0.73": "patches/@ai-sdk%2Fgoogle@3.0.73.patch",
"@pierre/trees@1.0.0-beta.4": "patches/@pierre%2Ftrees@1.0.0-beta.4.patch", "@pierre/trees@1.0.0-beta.4": "patches/@pierre%2Ftrees@1.0.0-beta.4.patch",
"@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch", "@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch",
"effect@4.0.0-beta.101": "patches/effect@4.0.0-beta.101.patch",
"@tanstack/virtual-core@3.17.3": "patches/@tanstack%2Fvirtual-core@3.17.3.patch", "@tanstack/virtual-core@3.17.3": "patches/@tanstack%2Fvirtual-core@3.17.3.patch",
"@ff-labs/fff-bun@0.10.1": "patches/@ff-labs%2Ffff-bun@0.10.1.patch" "@ff-labs/fff-bun@0.10.1": "patches/@ff-labs%2Ffff-bun@0.10.1.patch"
} }
@@ -30,7 +30,6 @@ import { ToolSchemaProjection } from "./utils/tool-schema.js"
import { ToolStream } from "./utils/tool-stream.js" import { ToolStream } from "./utils/tool-stream.js"
const ADAPTER = "anthropic-messages" const ADAPTER = "anthropic-messages"
const MEDIA_MIMES = new Set<string>([...ProviderShared.IMAGE_MIMES, ...ProviderShared.PDF_MIMES])
export const DEFAULT_BASE_URL = "https://api.anthropic.com/v1" export const DEFAULT_BASE_URL = "https://api.anthropic.com/v1"
export const PATH = "/messages" export const PATH = "/messages"
@@ -400,7 +399,7 @@ const lowerServerToolResult = Effect.fn("AnthropicMessages.lowerServerToolResult
}) })
const lowerMedia = Effect.fn("AnthropicMessages.lowerMedia")(function* (part: MediaPart) { const lowerMedia = Effect.fn("AnthropicMessages.lowerMedia")(function* (part: MediaPart) {
const media = yield* ProviderShared.validateMedia("Anthropic Messages", part, MEDIA_MIMES) const media = ProviderShared.normalizeMedia(part)
if (media.mime === "application/pdf") if (media.mime === "application/pdf")
return { return {
type: "document" as const, type: "document" as const,
@@ -410,6 +409,8 @@ const lowerMedia = Effect.fn("AnthropicMessages.lowerMedia")(function* (part: Me
data: media.base64, data: media.base64,
}, },
} satisfies AnthropicDocumentBlock } satisfies AnthropicDocumentBlock
if (!media.mime.startsWith("image/"))
return yield* invalid(`Anthropic Messages does not support media type ${part.mediaType}`)
return { return {
type: "image" as const, type: "image" as const,
source: { source: {
+44 -27
View File
@@ -24,7 +24,6 @@ import { Lifecycle } from "./utils/lifecycle.js"
import { ToolSchemaProjection } from "./utils/tool-schema.js" import { ToolSchemaProjection } from "./utils/tool-schema.js"
const ADAPTER = "gemini" const ADAPTER = "gemini"
const MEDIA_MIMES = new Set<string>(ProviderShared.MEDIA_MIMES)
// Google documents this sentinel for replaying Gemini 3 function calls after their original signature was lost. // Google documents this sentinel for replaying Gemini 3 function calls after their original signature was lost.
const SKIP_THOUGHT_SIGNATURE_VALIDATOR = "skip_thought_signature_validator" const SKIP_THOUGHT_SIGNATURE_VALIDATOR = "skip_thought_signature_validator"
export const DEFAULT_BASE_URL = "https://generativelanguage.googleapis.com/v1beta" export const DEFAULT_BASE_URL = "https://generativelanguage.googleapis.com/v1beta"
@@ -93,7 +92,7 @@ const GeminiFunctionCallPart = Schema.Struct({
functionCall: Schema.Struct({ functionCall: Schema.Struct({
id: Schema.optional(Schema.String), id: Schema.optional(Schema.String),
name: Schema.String, name: Schema.String,
args: Schema.Unknown, args: Schema.optional(Schema.Unknown),
}), }),
thoughtSignature: Schema.optional(Schema.String), thoughtSignature: Schema.optional(Schema.String),
}) })
@@ -167,6 +166,7 @@ const GeminiGenerationConfig = Schema.Struct({
const GeminiBodyFields = { const GeminiBodyFields = {
cachedContent: Schema.optional(Schema.String), cachedContent: Schema.optional(Schema.String),
contents: Schema.Array(GeminiContent), contents: Schema.Array(GeminiContent),
labels: Schema.optional(Schema.Record(Schema.String, Schema.String)),
safetySettings: optionalArray(GeminiSafetySetting), safetySettings: optionalArray(GeminiSafetySetting),
serviceTier: Schema.optional(Schema.String), serviceTier: Schema.optional(Schema.String),
systemInstruction: Schema.optional(GeminiSystemInstruction), systemInstruction: Schema.optional(GeminiSystemInstruction),
@@ -191,8 +191,19 @@ const GeminiCandidate = Schema.Struct({
finishReason: Schema.optional(Schema.String), finishReason: Schema.optional(Schema.String),
}) })
const GeminiPromptFeedback = Schema.StructWithRest(
Schema.Struct({
blockReason: Schema.optional(Schema.String),
blockReasonMessage: Schema.optional(Schema.String),
safetyRatings: Schema.optional(Schema.Unknown),
}),
[Schema.Record(Schema.String, Schema.Unknown)],
)
type GeminiPromptFeedback = Schema.Schema.Type<typeof GeminiPromptFeedback>
const GeminiEvent = Schema.Struct({ const GeminiEvent = Schema.Struct({
candidates: optionalArray(GeminiCandidate), candidates: optionalArray(GeminiCandidate),
promptFeedback: Schema.optional(GeminiPromptFeedback),
usageMetadata: Schema.optional(GeminiUsage), usageMetadata: Schema.optional(GeminiUsage),
}) })
type GeminiEvent = Schema.Schema.Type<typeof GeminiEvent> type GeminiEvent = Schema.Schema.Type<typeof GeminiEvent>
@@ -201,6 +212,7 @@ interface ParserState {
readonly finishReason?: string readonly finishReason?: string
readonly hasToolCalls: boolean readonly hasToolCalls: boolean
readonly nextToolCallId: number readonly nextToolCallId: number
readonly promptFeedback?: GeminiPromptFeedback
readonly usage?: Usage readonly usage?: Usage
readonly lifecycle: Lifecycle.State readonly lifecycle: Lifecycle.State
readonly reasoningSignature?: string readonly reasoningSignature?: string
@@ -248,7 +260,7 @@ const lowerToolConfig = (toolChoice: NonNullable<LLMRequest["toolChoice"]>) =>
const lowerUserPart = Effect.fn("Gemini.lowerUserPart")(function* (part: TextPart | MediaPart) { const lowerUserPart = Effect.fn("Gemini.lowerUserPart")(function* (part: TextPart | MediaPart) {
if (part.type === "text") return { text: part.text } if (part.type === "text") return { text: part.text }
const media = yield* ProviderShared.validateMedia("Gemini", part, MEDIA_MIMES) const media = ProviderShared.normalizeMedia(part)
return { inlineData: { mimeType: media.mime, data: media.base64 } } return { inlineData: { mimeType: media.mime, data: media.base64 } }
}) })
@@ -353,7 +365,7 @@ const lowerMessages = Effect.fn("Gemini.lowerMessages")(function* (request: LLMR
const media: GeminiInlineDataPart[] = [] const media: GeminiInlineDataPart[] = []
for (const item of content) { for (const item of content) {
if (item.type === "text") continue if (item.type === "text") continue
const value = yield* ProviderShared.validateToolFile("Gemini", item, MEDIA_MIMES) const value = ProviderShared.normalizeToolFile(item)
media.push({ inlineData: { mimeType: value.mime, data: value.base64 } }) media.push({ inlineData: { mimeType: value.mime, data: value.base64 } })
} }
parts.push({ parts.push({
@@ -504,32 +516,37 @@ const mapFinishReason = (finishReason: string | undefined, hasToolCalls: boolean
return "unknown" return "unknown"
} }
const finish = (state: ParserState): ReadonlyArray<LLMEvent> => const finish = (state: ParserState): ReadonlyArray<LLMEvent> => {
state.finishReason || state.usage const promptBlockReason = state.finishReason === undefined ? state.promptFeedback?.blockReason : undefined
? (() => { const finishReason = state.finishReason ?? promptBlockReason
const events: LLMEvent[] = [] if (finishReason === undefined && state.usage === undefined) return []
const lifecycle = state.reasoningSignature
? Lifecycle.reasoningEnd( const events: LLMEvent[] = []
state.lifecycle, const lifecycle = state.reasoningSignature
events, ? Lifecycle.reasoningEnd(
"reasoning-0", state.lifecycle,
googleMetadata({ thoughtSignature: state.reasoningSignature }), events,
) "reasoning-0",
: state.lifecycle googleMetadata({ thoughtSignature: state.reasoningSignature }),
Lifecycle.finish(lifecycle, events, { )
reason: { : state.lifecycle
normalized: mapFinishReason(state.finishReason, state.hasToolCalls), Lifecycle.finish(lifecycle, events, {
raw: state.finishReason, reason: {
}, normalized:
usage: state.usage, promptBlockReason === undefined ? mapFinishReason(finishReason, state.hasToolCalls) : "content-filter",
}) raw: finishReason,
return events },
})() usage: state.usage,
: [] providerMetadata:
state.promptFeedback === undefined ? undefined : googleMetadata({ promptFeedback: state.promptFeedback }),
})
return events
}
const step = (state: ParserState, event: GeminiEvent) => { const step = (state: ParserState, event: GeminiEvent) => {
const nextState = { const nextState = {
...state, ...state,
promptFeedback: event.promptFeedback ?? state.promptFeedback,
usage: event.usageMetadata ? (mapUsage(event.usageMetadata) ?? state.usage) : state.usage, usage: event.usageMetadata ? (mapUsage(event.usageMetadata) ?? state.usage) : state.usage,
} }
const candidate = event.candidates?.[0] const candidate = event.candidates?.[0]
@@ -570,7 +587,7 @@ const step = (state: ParserState, event: GeminiEvent) => {
} }
if ("functionCall" in part) { if ("functionCall" in part) {
const input = part.functionCall.args const input = part.functionCall.args === undefined ? {} : part.functionCall.args
const id = `tool_${nextToolCallId++}` const id = `tool_${nextToolCallId++}`
const metadata = { const metadata = {
...(part.functionCall.id === undefined ? {} : { functionCallId: part.functionCall.id }), ...(part.functionCall.id === undefined ? {} : { functionCallId: part.functionCall.id }),
+4 -5
View File
@@ -26,7 +26,6 @@ import { ToolStream } from "./utils/tool-stream.js"
const ADAPTER = "open-responses" const ADAPTER = "open-responses"
const NAME = "Open Responses" const NAME = "Open Responses"
const MEDIA_MIMES = new Set<string>([...ProviderShared.IMAGE_MIMES, ...ProviderShared.PDF_MIMES])
export const PATH = "/responses" export const PATH = "/responses"
// ============================================================================= // =============================================================================
@@ -285,7 +284,7 @@ export interface Extension {
readonly name: string readonly name: string
readonly lowerMedia?: (input: { readonly lowerMedia?: (input: {
readonly part: MediaPart readonly part: MediaPart
readonly media: ProviderShared.ValidatedMedia readonly media: ProviderShared.NormalizedMedia
readonly request: LLMRequest readonly request: LLMRequest
}) => MediaInput | undefined }) => MediaInput | undefined
readonly messagePhase?: (value: unknown) => MessagePhase | null | undefined readonly messagePhase?: (value: unknown) => MessagePhase | null | undefined
@@ -380,13 +379,13 @@ const lowerMedia = Effect.fn("OpenResponses.lowerMedia")(function* (
request: LLMRequest, request: LLMRequest,
extension: Extension, extension: Extension,
) { ) {
const media = yield* ProviderShared.validateMedia(extension.name, part, MEDIA_MIMES) const media = ProviderShared.normalizeMedia(part)
const extended = extension.lowerMedia?.({ part, media, request }) const extended = extension.lowerMedia?.({ part, media, request })
if (extended) return extended if (extended) return extended
if (media.mime === "application/pdf") { if (!media.mime.startsWith("image/")) {
return { return {
type: "input_file" as const, type: "input_file" as const,
filename: part.filename ?? "document.pdf", filename: part.filename ?? (media.mime === "application/pdf" ? "document.pdf" : "file"),
file_data: media.dataUrl, file_data: media.dataUrl,
} }
} }
+3 -2
View File
@@ -28,7 +28,6 @@ import { ToolSchemaProjection } from "./utils/tool-schema.js"
import { ToolStream } from "./utils/tool-stream.js" import { ToolStream } from "./utils/tool-stream.js"
const ADAPTER = "openai-chat" const ADAPTER = "openai-chat"
const IMAGE_MIMES = new Set<string>(ProviderShared.IMAGE_MIMES)
const RESERVED_REASONING_FIELDS = new Set(["role", "content", "tool_calls"]) const RESERVED_REASONING_FIELDS = new Set(["role", "content", "tool_calls"])
export const DEFAULT_BASE_URL = "https://api.openai.com/v1" export const DEFAULT_BASE_URL = "https://api.openai.com/v1"
export const PATH = "/chat/completions" export const PATH = "/chat/completions"
@@ -284,7 +283,9 @@ const lowerToolCall = (part: ToolCallPart): OpenAIChatAssistantToolCall => ({
}) })
const lowerMedia = Effect.fn("OpenAIChat.lowerMedia")(function* (part: MediaPart) { const lowerMedia = Effect.fn("OpenAIChat.lowerMedia")(function* (part: MediaPart) {
const media = yield* ProviderShared.validateMedia("OpenAI Chat", part, IMAGE_MIMES) const media = ProviderShared.normalizeMedia(part)
if (!media.mime.startsWith("image/"))
return yield* ProviderShared.invalidRequest(`OpenAI Chat does not support media type ${part.mediaType}`)
return { type: "image_url" as const, image_url: { url: media.dataUrl } } return { type: "image_url" as const, image_url: { url: media.dataUrl } }
}) })
+12 -46
View File
@@ -155,59 +155,24 @@ export const wrappedSystemUpdate = Effect.fn("ProviderShared.wrappedSystemUpdate
export const parseToolInput = (route: string, name: string, raw: string) => export const parseToolInput = (route: string, name: string, raw: string) =>
parseJson(route, raw || "{}", `Invalid JSON input for ${route} tool call ${name}`) parseJson(route, raw || "{}", `Invalid JSON input for ${route} tool call ${name}`)
export const IMAGE_MIMES = ["image/png", "image/jpeg", "image/gif", "image/webp"] as const export interface NormalizedMedia {
export const VIDEO_MIMES = ["video/mp4", "video/webm", "video/quicktime"] as const
export const AUDIO_MIMES = ["audio/wav", "audio/mp3", "audio/aiff", "audio/aac", "audio/ogg", "audio/flac"] as const
export const PDF_MIMES = ["application/pdf"] as const
export const MEDIA_MIMES = [...IMAGE_MIMES, ...VIDEO_MIMES, ...AUDIO_MIMES, ...PDF_MIMES] as const
export const MAX_MEDIA_ENCODED_BYTES = 28 * 1024 * 1024
export const MAX_MEDIA_DECODED_BYTES = 20 * 1024 * 1024
const base64Pattern = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/
export interface ValidatedMedia {
readonly mime: string readonly mime: string
readonly base64: string readonly base64: string
readonly dataUrl: string readonly dataUrl: string
readonly bytes: Uint8Array
} }
export const validateMedia = Effect.fn("ProviderShared.validateMedia")(function* ( export const normalizeMedia = (part: MediaPart): NormalizedMedia => {
route: string,
part: MediaPart,
supportedMimes: ReadonlySet<string>,
) {
const mime = part.mediaType.toLowerCase() const mime = part.mediaType.toLowerCase()
if (!supportedMimes.has(mime)) return yield* invalidRequest(`${route} does not support media type ${part.mediaType}`)
let base64: string
if (typeof part.data !== "string") { if (typeof part.data !== "string") {
if (part.data.byteLength > MAX_MEDIA_DECODED_BYTES) const base64 = Buffer.from(part.data).toString("base64")
return yield* invalidRequest(`${route} media exceeds the ${MAX_MEDIA_DECODED_BYTES} byte decoded limit`) return { mime, base64, dataUrl: `data:${mime};base64,${base64}` }
base64 = Buffer.from(part.data).toString("base64")
} else if (part.data.startsWith("data:")) {
const match = /^data:([^;,]+);base64,([A-Za-z0-9+/]*={0,2})$/s.exec(part.data)
if (!match) return yield* invalidRequest(`${route} media data URL must contain valid base64`)
if (match[1]!.toLowerCase() !== mime)
return yield* invalidRequest(`${route} media type ${part.mediaType} does not match data URL type ${match[1]}`)
base64 = match[2]!
} else {
base64 = part.data
} }
if (!part.data.startsWith("data:")) return { mime, base64: part.data, dataUrl: `data:${mime};base64,${part.data}` }
return { mime, base64: part.data.slice(part.data.indexOf(",") + 1), dataUrl: part.data }
}
if (Buffer.byteLength(base64, "utf8") > MAX_MEDIA_ENCODED_BYTES) export const normalizeToolFile = (part: Tool.FileContent) =>
return yield* invalidRequest(`${route} media exceeds the ${MAX_MEDIA_ENCODED_BYTES} byte encoded limit`) normalizeMedia({ type: "media", mediaType: part.mime, data: part.uri, filename: part.name })
if (!base64 || base64.length % 4 !== 0 || !base64Pattern.test(base64))
return yield* invalidRequest(`${route} media must contain valid base64`)
const bytes = Buffer.from(base64, "base64")
if (bytes.byteLength > MAX_MEDIA_DECODED_BYTES)
return yield* invalidRequest(`${route} media exceeds the ${MAX_MEDIA_DECODED_BYTES} byte decoded limit`)
if (bytes.toString("base64") !== base64) return yield* invalidRequest(`${route} media must contain canonical base64`)
return { mime, base64, dataUrl: `data:${mime};base64,${base64}`, bytes } satisfies ValidatedMedia
})
export const validateToolFile = (route: string, part: Tool.FileContent, supportedMimes: ReadonlySet<string>) =>
validateMedia(route, { type: "media", mediaType: part.mime, data: part.uri, filename: part.name }, supportedMimes)
export const trimBaseUrl = (value: string) => value.replace(/\/+$/, "") export const trimBaseUrl = (value: string) => value.replace(/\/+$/, "")
@@ -237,14 +202,15 @@ export const errorText = (error: unknown) => {
* decoder, and drops empty / `[DONE]` keep-alive events so the downstream * decoder, and drops empty / `[DONE]` keep-alive events so the downstream
* `decodeChunk` sees one JSON string per element. The SSE channel emits a * `decodeChunk` sees one JSON string per element. The SSE channel emits a
* `Retry` control event on its error channel; we drop it here (we don't * `Retry` control event on its error channel; we drop it here (we don't
* implement client-driven retries) so the public error channel stays * implement client-driven retries). Decoder failures become provider output
* `AIError`. * errors so the public error channel stays `AIError`.
*/ */
export const sseFraming = (bytes: Stream.Stream<Uint8Array, AIError>): Stream.Stream<string, AIError> => export const sseFraming = (bytes: Stream.Stream<Uint8Array, AIError>): Stream.Stream<string, AIError> =>
bytes.pipe( bytes.pipe(
Stream.decodeText(), Stream.decodeText(),
Stream.pipeThroughChannel(Sse.decode()), Stream.pipeThroughChannel(Sse.decode()),
Stream.catchTag("Retry", () => Stream.empty), Stream.catchTag("Retry", () => Stream.empty),
Stream.catchTag("SseError", (error) => Stream.fail(eventError("sse", error.message))),
Stream.filter((event) => event.data.length > 0 && event.data !== "[DONE]"), Stream.filter((event) => event.data.length > 0 && event.data !== "[DONE]"),
Stream.map((event) => event.data), Stream.map((event) => event.data),
) )
@@ -66,11 +66,7 @@ export const lower = Effect.fn("BedrockMedia.lower")(function* (part: MediaPart)
const mime = part.mediaType.toLowerCase() const mime = part.mediaType.toLowerCase()
const imageFormat = IMAGE_FORMATS[mime as keyof typeof IMAGE_FORMATS] const imageFormat = IMAGE_FORMATS[mime as keyof typeof IMAGE_FORMATS]
if (imageFormat) { if (imageFormat) {
const media = yield* ProviderShared.validateMedia( const media = ProviderShared.normalizeMedia(part)
"Bedrock Converse",
part,
new Set<string>(Object.keys(IMAGE_FORMATS)),
)
return { image: { format: imageFormat, source: { bytes: media.base64 } } } satisfies ImageBlock return { image: { format: imageFormat, source: { bytes: media.base64 } } } satisfies ImageBlock
} }
if (mime.startsWith("image/")) if (mime.startsWith("image/"))
@@ -79,11 +75,7 @@ export const lower = Effect.fn("BedrockMedia.lower")(function* (part: MediaPart)
if (documentFormat) { if (documentFormat) {
if (!part.filename) if (!part.filename)
return yield* ProviderShared.invalidRequest("Bedrock Converse document media requires a filename") return yield* ProviderShared.invalidRequest("Bedrock Converse document media requires a filename")
const media = yield* ProviderShared.validateMedia( const media = ProviderShared.normalizeMedia(part)
"Bedrock Converse",
part,
new Set<string>(Object.keys(DOCUMENT_FORMATS)),
)
return documentBlock(part.filename, documentFormat, media.base64) return documentBlock(part.filename, documentFormat, media.base64)
} }
return yield* ProviderShared.invalidRequest(`Bedrock Converse does not support media type ${part.mediaType}`) return yield* ProviderShared.invalidRequest(`Bedrock Converse does not support media type ${part.mediaType}`)
+1 -1
View File
@@ -57,7 +57,7 @@ export const isContextOverflowFailure = (failure: unknown) =>
? failure.reason._tag === "InvalidRequest" && failure.reason.classification === "context-overflow" ? failure.reason._tag === "InvalidRequest" && failure.reason.classification === "context-overflow"
: Schema.is(ProviderErrorEvent)(failure) && failure.classification === "context-overflow" : Schema.is(ProviderErrorEvent)(failure) && failure.classification === "context-overflow"
const decodeJson = Schema.decodeUnknownOption(Schema.UnknownFromJsonString) const decodeJson = Schema.decodeUnknownOption(Schema.fromJsonString(Schema.Unknown))
const QUOTA_CODES = new Set(["insufficient_quota", "usage_not_included", "billing_error"]) const QUOTA_CODES = new Set(["insufficient_quota", "usage_not_included", "billing_error"])
const SERVER_CODES = new Set([ const SERVER_CODES = new Set([
"api_error", "api_error",
+34 -11
View File
@@ -1,14 +1,21 @@
import { Effect } from "effect"
import type { ProviderPackage } from "../provider-package.js" import type { ProviderPackage } from "../provider-package.js"
import { Gemini } from "../protocols/gemini.js" import { Gemini } from "../protocols/gemini.js"
import { ProviderShared } from "../protocols/shared.js"
import { Auth } from "../route/auth.js" import { Auth } from "../route/auth.js"
import { Route, type RouteDefaultsInput } from "../route/client.js" import { Route, type RouteDefaultsInput } from "../route/client.js"
import { Endpoint } from "../route/endpoint.js" import { Endpoint } from "../route/endpoint.js"
import { Framing } from "../route/framing.js" import { Framing } from "../route/framing.js"
import { ProviderID, type ModelID } from "../schema/index.js" import { ProviderID, type LLMRequest, type ModelID, type ProviderOptions } from "../schema/index.js"
import { GoogleVertexShared } from "./google-vertex-shared.js" import { GoogleVertexShared } from "./google-vertex-shared.js"
export type GeminiOptionsInput = Gemini.OptionsInput export interface GeminiOptionsInput extends Gemini.OptionsInput {
export type GeminiProviderOptionsInput = Gemini.ProviderOptionsInput readonly labels?: Readonly<Record<string, string>>
}
export type GeminiProviderOptionsInput = ProviderOptions & {
readonly gemini?: GeminiOptionsInput
}
export const id = ProviderID.make("google-vertex") export const id = ProviderID.make("google-vertex")
@@ -17,7 +24,7 @@ export type Config = RouteDefaultsInput &
readonly baseURL?: string readonly baseURL?: string
readonly location?: string readonly location?: string
readonly project?: string readonly project?: string
readonly providerOptions?: Gemini.ProviderOptionsInput readonly providerOptions?: GeminiProviderOptionsInput
} }
export type Settings = ProviderPackage.Settings & export type Settings = ProviderPackage.Settings &
@@ -28,14 +35,33 @@ export type Settings = ProviderPackage.Settings &
readonly baseURL?: string readonly baseURL?: string
readonly location?: string readonly location?: string
readonly project?: string readonly project?: string
readonly providerOptions?: Gemini.ProviderOptionsInput readonly providerOptions?: GeminiProviderOptionsInput
} }
const fromRequest = Effect.fn("GoogleVertex.fromRequest")(function* (request: LLMRequest) {
const body = yield* Gemini.protocol.body.from(request)
const value = request.providerOptions?.gemini?.labels
const labels = ProviderShared.isRecord(value)
? Object.fromEntries(
Object.entries(value).filter((entry): entry is [string, string] => typeof entry[1] === "string"),
)
: undefined
return { ...body, labels }
})
const protocol = {
...Gemini.protocol,
body: {
...Gemini.protocol.body,
from: fromRequest,
},
}
const route = Route.make({ const route = Route.make({
id: "google-vertex-gemini", id: "google-vertex-gemini",
provider: id, provider: id,
providerMetadataKey: "google", providerMetadataKey: "google",
protocol: Gemini.protocol, protocol,
endpoint: Endpoint.path(({ request }) => { endpoint: Endpoint.path(({ request }) => {
const model = String(request.model.id) const model = String(request.model.id)
return `/${model.startsWith("endpoints/") ? model : `models/${model}`}:streamGenerateContent?alt=sse` return `/${model.startsWith("endpoints/") ? model : `models/${model}`}:streamGenerateContent?alt=sse`
@@ -78,7 +104,7 @@ export const configure = (input: Config = {}) => {
return { return {
id, id,
model: (modelID: string | ModelID) => model: (modelID: string | ModelID) =>
configuredRoute(input, modelID).model<Gemini.ProviderOptionsInput>({ id: modelID }), configuredRoute(input, modelID).model<GeminiProviderOptionsInput>({ id: modelID }),
configure, configure,
} }
} }
@@ -87,10 +113,7 @@ export const provider = {
id, id,
configure, configure,
} }
export const model: ProviderPackage.Definition<Settings, Gemini.ProviderOptionsInput>["model"] = ( export const model: ProviderPackage.Definition<Settings, GeminiProviderOptionsInput>["model"] = (modelID, settings) => {
modelID,
settings,
) => {
if (settings.apiKey !== undefined && settings.accessToken !== undefined) if (settings.apiKey !== undefined && settings.accessToken !== undefined)
throw new Error("Google Vertex apiKey cannot be combined with accessToken or auth") throw new Error("Google Vertex apiKey cannot be combined with accessToken or auth")
return configure({ return configure({
+1 -1
View File
@@ -148,7 +148,7 @@ export const AIErrorReason = Schema.Union([
]).pipe(Schema.toTaggedUnion("_tag")) ]).pipe(Schema.toTaggedUnion("_tag"))
export type AIErrorReason = Schema.Schema.Type<typeof AIErrorReason> export type AIErrorReason = Schema.Schema.Type<typeof AIErrorReason>
export class AIError extends Schema.TaggedErrorClass<AIError>()("AI.Error", { export class AIError extends Schema.TaggedError<AIError>()("AI.Error", {
module: Schema.String, module: Schema.String,
method: Schema.String, method: Schema.String,
reason: AIErrorReason, reason: AIErrorReason,
@@ -399,7 +399,7 @@ describe("Anthropic Messages route", () => {
}), }),
) )
it.effect("rejects unsupported media in tool-result content with a clear error", () => it.effect("rejects tool-result media that cannot be lowered", () =>
Effect.gen(function* () { Effect.gen(function* () {
const error = yield* compileRequest( const error = yield* compileRequest(
LLM.request({ LLM.request({
@@ -418,8 +418,7 @@ describe("Anthropic Messages route", () => {
}), }),
).pipe(Effect.flip) ).pipe(Effect.flip)
expect(error.message).toContain("Anthropic Messages") expect(error.message).toContain("Anthropic Messages does not support media type audio/mpeg")
expect(error.message).toContain("audio/mpeg")
}), }),
) )
@@ -75,7 +75,6 @@ describe("Amazon Bedrock Mantle provider", () => {
}), }),
), ),
), ),
Effect.flip,
) )
expect(seen).toEqual([{ url: "https://mantle.test/v1/chat/completions", authorization: "Bearer test-key" }]) expect(seen).toEqual([{ url: "https://mantle.test/v1/chat/completions", authorization: "Bearer test-key" }])
+162 -24
View File
@@ -4,7 +4,6 @@ import { LLM, AIError, LLMRequest, Message, ToolCallPart, ToolDefinition, Usage
import { Auth, LLMClient } from "../../src/route.js" import { Auth, LLMClient } from "../../src/route.js"
import { compileRequest } from "../../src/route/client.js" import { compileRequest } from "../../src/route/client.js"
import * as Gemini from "../../src/protocols/gemini.js" import * as Gemini from "../../src/protocols/gemini.js"
import { ProviderShared } from "../../src/protocols/shared.js"
import { it } from "../lib/effect.js" import { it } from "../lib/effect.js"
import { fixedResponse } from "../lib/http.js" import { fixedResponse } from "../lib/http.js"
import { sseEvents, sseRaw } from "../lib/sse.js" import { sseEvents, sseRaw } from "../lib/sse.js"
@@ -291,35 +290,30 @@ describe("Gemini route", () => {
}), }),
) )
for (const [name, media] of [ it.effect("passes encoded media through without local validation", () =>
["mismatched data URL MIME", { mediaType: "image/png", data: "data:image/jpeg;base64,/9j/" }],
["malformed base64", { mediaType: "image/png", data: "%%%=" }],
["unsupported SVG", { mediaType: "image/svg+xml", data: "PHN2Zz4=" }],
] as const)
it.effect(`rejects ${name}`, () =>
Effect.gen(function* () {
const error = yield* compileRequest(
LLM.request({ model, messages: [Message.user({ type: "media", ...media })] }),
).pipe(Effect.flip)
expect(error.message).toMatch(/does not support|does not match|valid base64/)
}),
)
it.effect("rejects oversized image input", () =>
Effect.gen(function* () { Effect.gen(function* () {
const error = yield* compileRequest( const prepared = yield* compileRequest(
LLM.request({ LLM.request({
model, model,
messages: [ messages: [
Message.user({ Message.user([
type: "media", { type: "media", mediaType: "image/png", data: "%%%=" },
mediaType: "image/png", { type: "media", mediaType: "image/png", data: "data:image/jpeg;base64,/9j/" },
data: "A".repeat(ProviderShared.MAX_MEDIA_ENCODED_BYTES + 4), { type: "media", mediaType: "image/svg+xml", data: "PHN2Zz4=" },
}), ]),
], ],
}), }),
).pipe(Effect.flip) )
expect(error.message).toContain("encoded limit") expect(prepared.body.contents).toEqual([
{
role: "user",
parts: [
{ inlineData: { mimeType: "image/png", data: "%%%=" } },
{ inlineData: { mimeType: "image/png", data: "/9j/" } },
{ inlineData: { mimeType: "image/svg+xml", data: "PHN2Zz4=" } },
],
},
])
}), }),
) )
@@ -708,6 +702,80 @@ describe("Gemini route", () => {
}), }),
) )
it.effect("leaves unsigned parallel calls unchanged after a signed Gemini 3 call", () =>
Effect.gen(function* () {
const prepared = yield* compileRequest(
LLM.request({
model: gemini3,
messages: [
Message.assistant([
ToolCallPart.make({
id: "tool_0",
name: "lookup",
input: { query: "weather" },
providerMetadata: { google: { thoughtSignature: "parallel_signature" } },
}),
ToolCallPart.make({ id: "tool_1", name: "lookup", input: { query: "news" } }),
ToolCallPart.make({ id: "tool_2", name: "lookup", input: { query: "sports" } }),
]),
],
}),
)
expect(prepared.body.contents).toEqual([
{
role: "model",
parts: [
{
functionCall: { id: undefined, name: "lookup", args: { query: "weather" } },
thoughtSignature: "parallel_signature",
},
{
functionCall: { id: undefined, name: "lookup", args: { query: "news" } },
thoughtSignature: undefined,
},
{
functionCall: { id: undefined, name: "lookup", args: { query: "sports" } },
thoughtSignature: undefined,
},
],
},
])
}),
)
it.effect("adds the validator bypass sentinel to every call in an unsigned Gemini 3 batch", () =>
Effect.gen(function* () {
const prepared = yield* compileRequest(
LLM.request({
model: gemini3,
messages: [
Message.assistant([
ToolCallPart.make({ id: "tool_0", name: "lookup", input: { query: "weather" } }),
ToolCallPart.make({ id: "tool_1", name: "lookup", input: { query: "news" } }),
]),
],
}),
)
expect(prepared.body.contents).toEqual([
{
role: "model",
parts: [
{
functionCall: { id: undefined, name: "lookup", args: { query: "weather" } },
thoughtSignature: "skip_thought_signature_validator",
},
{
functionCall: { id: undefined, name: "lookup", args: { query: "news" } },
thoughtSignature: "skip_thought_signature_validator",
},
],
},
])
}),
)
it.effect("emits streamed tool calls and maps finish reason", () => it.effect("emits streamed tool calls and maps finish reason", () =>
Effect.gen(function* () { Effect.gen(function* () {
const body = sseEvents({ const body = sseEvents({
@@ -773,6 +841,31 @@ describe("Gemini route", () => {
}), }),
) )
it.effect("defaults omitted function call args to an empty object", () =>
Effect.gen(function* () {
const response = yield* LLMClient.generate(
LLMRequest.update(request, {
tools: [ToolDefinition.make({ name: "ping", description: "Ping", inputSchema: { type: "object" } })],
}),
).pipe(
Effect.provide(
fixedResponse(
sseEvents({
candidates: [
{
content: { role: "model", parts: [{ functionCall: { name: "ping" } }] },
finishReason: "STOP",
},
],
}),
),
),
)
expect(response.toolCalls).toEqual([{ type: "tool-call", id: "tool_0", name: "ping", input: {} }])
}),
)
it.effect("maps tool calls without a finish reason", () => it.effect("maps tool calls without a finish reason", () =>
Effect.gen(function* () { Effect.gen(function* () {
const response = yield* LLMClient.generate( const response = yield* LLMClient.generate(
@@ -868,6 +961,51 @@ describe("Gemini route", () => {
}), }),
) )
it.effect("preserves candidate-less prompt safety blocks as content-filter outcomes", () =>
Effect.gen(function* () {
const blocked = yield* LLMClient.generate(request).pipe(
Effect.provide(
fixedResponse(
sseEvents({
promptFeedback: {
blockReason: "FUTURE_SAFETY_REASON",
blockReasonMessage: "Prompt blocked",
safetyRatings: [{ category: "HARM_CATEGORY_HARASSMENT", blocked: true }],
},
}),
),
),
)
const blockedWithUsage = yield* LLMClient.generate(request).pipe(
Effect.provide(
fixedResponse(
sseEvents(
{ promptFeedback: { blockReason: "SAFETY" } },
{ usageMetadata: { promptTokenCount: 7, totalTokenCount: 7 } },
),
),
),
)
expect(blocked.events.map((event) => event.type)).toEqual(["step-start", "step-finish", "finish"])
expect(blocked.events.at(-1)).toMatchObject({
type: "finish",
reason: { normalized: "content-filter", raw: "FUTURE_SAFETY_REASON" },
providerMetadata: {
google: {
promptFeedback: {
blockReason: "FUTURE_SAFETY_REASON",
blockReasonMessage: "Prompt blocked",
safetyRatings: [{ category: "HARM_CATEGORY_HARASSMENT", blocked: true }],
},
},
},
})
expect(blockedWithUsage.finishReason).toEqual({ normalized: "content-filter", raw: "SAFETY" })
expect(blockedWithUsage.usage).toMatchObject({ inputTokens: 7, totalTokens: 7 })
}),
)
it.effect("maps current blocking and invalid-output finish reasons", () => it.effect("maps current blocking and invalid-output finish reasons", () =>
Effect.gen(function* () { Effect.gen(function* () {
const reasons = [ const reasons = [
@@ -54,6 +54,27 @@ describe("Google Vertex providers", () => {
}), }),
) )
it.effect("adds billing labels to Vertex Gemini requests", () =>
Effect.gen(function* () {
const prepared = yield* compileRequest(
LLM.request({
model: GoogleVertex.configure({
accessToken: "vertex-token",
project: "vertex-project",
providerOptions: {
gemini: { labels: { component: "opencode", environment: "test" } },
},
}).model("gemini-3.5-flash"),
prompt: "Say hello.",
}),
)
expect(prepared.body).toMatchObject({
labels: { component: "opencode", environment: "test" },
})
}),
)
it.effect("projects Anthropic Messages onto the Vertex raw-predict API", () => it.effect("projects Anthropic Messages onto the Vertex raw-predict API", () =>
Effect.gen(function* () { Effect.gen(function* () {
const model = GoogleVertexMessages.configure({ const model = GoogleVertexMessages.configure({
+29 -22
View File
@@ -527,35 +527,42 @@ describe("OpenAI Chat route", () => {
}), }),
) )
for (const [name, media] of [ it.effect("passes encoded image media through without local validation", () =>
["mismatched data URL MIME", { mediaType: "image/png", data: "data:image/jpeg;base64,/9j/" }], Effect.gen(function* () {
["malformed base64", { mediaType: "image/png", data: "not-base64" }], const prepared = yield* compileRequest(
["unsupported SVG", { mediaType: "image/svg+xml", data: "PHN2Zz4=" }], LLM.request({
] as const) model,
it.effect(`rejects ${name}`, () => messages: [
Effect.gen(function* () { Message.user([
const error = yield* compileRequest( { type: "media", mediaType: "image/png", data: "not-base64" },
LLM.request({ model, messages: [Message.user({ type: "media", ...media })] }), { type: "media", mediaType: "image/png", data: "data:image/jpeg;base64,/9j/" },
).pipe(Effect.flip) { type: "media", mediaType: "image/svg+xml", data: "PHN2Zz4=" },
expect(error.message).toMatch(/does not support|does not match|valid base64/) ]),
}), ],
) }),
)
expect(prepared.body.messages).toEqual([
{
role: "user",
content: [
{ type: "image_url", image_url: { url: "data:image/png;base64,not-base64" } },
{ type: "image_url", image_url: { url: "data:image/jpeg;base64,/9j/" } },
{ type: "image_url", image_url: { url: "data:image/svg+xml;base64,PHN2Zz4=" } },
],
},
])
}),
)
it.effect("rejects oversized image input", () => it.effect("rejects non-image media that cannot be lowered", () =>
Effect.gen(function* () { Effect.gen(function* () {
const error = yield* compileRequest( const error = yield* compileRequest(
LLM.request({ LLM.request({
model, model,
messages: [ messages: [Message.user({ type: "media", mediaType: "audio/mpeg", data: "AAECAw==" })],
Message.user({
type: "media",
mediaType: "image/png",
data: "A".repeat(ProviderShared.MAX_MEDIA_ENCODED_BYTES + 4),
}),
],
}), }),
).pipe(Effect.flip) ).pipe(Effect.flip)
expect(error.message).toContain("encoded limit") expect(error.message).toContain("OpenAI Chat does not support media type audio/mpeg")
}), }),
) )
@@ -1149,6 +1149,32 @@ describe("OpenAI Responses route", () => {
}), }),
) )
it.effect("passes large PDF tool-result content through", () =>
Effect.gen(function* () {
const base64 = "A".repeat(8_125_844)
const dataUrl = `data:application/pdf;base64,${base64}`
const prepared = yield* compileRequest(
LLM.request({
id: "req_tool_result_large_pdf",
model,
messages: [
Message.assistant([ToolCallPart.make({ id: "call_1", name: "read", input: {} })]),
Message.tool({
id: "call_1",
name: "read",
resultType: "content",
result: [{ type: "file", uri: dataUrl, mime: "application/pdf", name: "report.pdf" }],
}),
],
}),
)
expect(expectToolOutput(prepared.body).output).toEqual([
{ type: "input_file", filename: "report.pdf", file_data: dataUrl },
])
}),
)
it.effect("uses xAI inline file encoding for PDF tool results", () => it.effect("uses xAI inline file encoding for PDF tool results", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* compileRequest( const prepared = yield* compileRequest(
@@ -1184,9 +1210,9 @@ describe("OpenAI Responses route", () => {
}), }),
) )
it.effect("rejects unsupported media in tool-result content with a clear error", () => it.effect("passes non-image tool-result content through as an input file", () =>
Effect.gen(function* () { Effect.gen(function* () {
const error = yield* compileRequest( const prepared = yield* compileRequest(
LLM.request({ LLM.request({
id: "req_tool_result_unsupported_media", id: "req_tool_result_unsupported_media",
model, model,
@@ -1200,10 +1226,11 @@ describe("OpenAI Responses route", () => {
}), }),
], ],
}), }),
).pipe(Effect.flip) )
expect(error.message).toContain("OpenAI Responses") expect(expectToolOutput(prepared.body).output).toEqual([
expect(error.message).toContain("audio/mpeg") { type: "input_file", filename: "file", file_data: "data:audio/mpeg;base64,AAECAw==" },
])
}), }),
) )
@@ -2394,17 +2421,28 @@ describe("OpenAI Responses route", () => {
}), }),
) )
it.effect("rejects unsupported user media content", () => it.effect("passes non-image user media through as an input file", () =>
Effect.gen(function* () { Effect.gen(function* () {
const error = yield* compileRequest( const prepared = yield* compileRequest(
LLM.request({ LLM.request({
id: "req_media", id: "req_media",
model, model,
messages: [Message.user({ type: "media", mediaType: "application/x-tar", data: "AAECAw==" })], messages: [Message.user({ type: "media", mediaType: "application/x-tar", data: "AAECAw==" })],
}), }),
).pipe(Effect.flip) )
expect(error.message).toContain("OpenAI Responses does not support media type application/x-tar") expect(prepared.body.input).toEqual([
{
role: "user",
content: [
{
type: "input_file",
filename: "file",
file_data: "data:application/x-tar;base64,AAECAw==",
},
],
},
])
}), }),
) )
+13 -1
View File
@@ -1,5 +1,5 @@
import { describe, expect, test } from "bun:test" import { describe, expect, test } from "bun:test"
import { Effect, Schema } from "effect" import { Effect, Schema, Stream } from "effect"
import * as OpenAIChat from "../src/protocols/openai-chat.js" import * as OpenAIChat from "../src/protocols/openai-chat.js"
import * as OpenAIResponses from "../src/protocols/openai-responses.js" import * as OpenAIResponses from "../src/protocols/openai-responses.js"
import { import {
@@ -90,6 +90,18 @@ describe("AI.Usage", () => {
expect(ProviderShared.sumTokens()).toBeUndefined() expect(ProviderShared.sumTokens()).toBeUndefined()
}) })
test("sseFraming maps decoder failures to AI errors", async () => {
const error = await Effect.runPromise(
ProviderShared.sseFraming(Stream.make(new TextEncoder().encode(`data: ${"x".repeat(10 * 1024 * 1024)}`))).pipe(
Stream.runCollect,
Effect.flip,
),
)
expect(error).toBeInstanceOf(AIError)
expect(error.reason._tag).toBe("InvalidProviderOutput")
})
test("visibleOutputTokens clamps reasoning > output to zero", () => { test("visibleOutputTokens clamps reasoning > output to zero", () => {
expect(new Usage({ outputTokens: 10, reasoningTokens: 4 }).visibleOutputTokens).toBe(6) expect(new Usage({ outputTokens: 10, reasoningTokens: 4 }).visibleOutputTokens).toBe(6)
expect(new Usage({ outputTokens: 10 }).visibleOutputTokens).toBe(10) expect(new Usage({ outputTokens: 10 }).visibleOutputTokens).toBe(10)
+1 -1
View File
@@ -86,7 +86,7 @@ async function writeProtocolStream(session: CDPSession, handle: string, file: st
try { try {
while (true) { while (true) {
const chunk = await session.send("IO.read", { handle }) const chunk = await session.send("IO.read", { handle })
await output.write(chunk.base64Encoded ? Buffer.from(chunk.data, "base64") : chunk.data) await (chunk.base64Encoded ? output.write(Buffer.from(chunk.data, "base64")) : output.write(chunk.data))
if (chunk.eof) break if (chunk.eof) break
} }
} finally { } finally {
@@ -9,6 +9,7 @@ import {
import { import {
assistantMessage, assistantMessage,
partUpdated, partUpdated,
renderedPartID,
setupTimeline, setupTimeline,
shell, shell,
textPart, textPart,
@@ -97,13 +98,15 @@ test.describe("timeline adverse visual stability", () => {
element.scrollTop = 0 element.scrollTop = 0
}) })
await page.waitForTimeout(300) await page.waitForTimeout(300)
const trigger = page.locator(`[data-timeline-part-id="${targetID}"] [data-slot="collapsible-trigger"]`) const trigger = page.locator(
`[data-timeline-part-id="${renderedPartID(targetID)}"] [data-slot="collapsible-trigger"]`,
)
await expect(trigger).toBeVisible() await expect(trigger).toBeVisible()
await trigger.click() await trigger.click()
await expect(trigger).toHaveAttribute("aria-expanded", "true") await expect(trigger).toHaveAttribute("aria-expanded", "true")
await scroller.evaluate((element) => (element.scrollTop = element.scrollHeight)) await scroller.evaluate((element) => (element.scrollTop = element.scrollHeight))
await expect(page.locator(`[data-timeline-part-id="${targetID}"]`)).toHaveCount(0) await expect(page.locator(`[data-timeline-part-id="${renderedPartID(targetID)}"]`)).toHaveCount(0)
await scroller.evaluate((element) => (element.scrollTop = 0)) await scroller.evaluate((element) => (element.scrollTop = 0))
await expect(trigger).toBeVisible() await expect(trigger).toBeVisible()
await expect(trigger).toHaveAttribute("aria-expanded", "true") await expect(trigger).toHaveAttribute("aria-expanded", "true")
@@ -127,13 +130,16 @@ test.describe("timeline adverse visual stability", () => {
cpuRate: 4, cpuRate: 4,
}) })
await waitForVisualSettle(page, [ await waitForVisualSettle(page, [
`[data-timeline-part-id="${shellID}"]`, `[data-timeline-part-id="${renderedPartID(shellID)}"]`,
`[data-timeline-part-id="${followingID}"]`, `[data-timeline-part-id="${renderedPartID(followingID)}"]`,
]) ])
const regions = defineVisualRegions({ const regions = defineVisualRegions({
shell: { selector: `[data-timeline-part-id="${shellID}"]`, closest: '[data-timeline-row="AssistantPart"]' }, shell: {
selector: `[data-timeline-part-id="${renderedPartID(shellID)}"]`,
closest: '[data-timeline-row="AssistantPart"]',
},
following: { following: {
selector: `[data-timeline-part-id="${followingID}"]`, selector: `[data-timeline-part-id="${renderedPartID(followingID)}"]`,
closest: '[data-timeline-row="AssistantPart"]', closest: '[data-timeline-row="AssistantPart"]',
}, },
}) })
@@ -184,10 +190,13 @@ test.describe("timeline adverse visual stability", () => {
}) })
const group = `[data-timeline-part-ids="${contextIDs.join(",")}"]` const group = `[data-timeline-part-ids="${contextIDs.join(",")}"]`
const regions = defineVisualRegions({ const regions = defineVisualRegions({
shell: { selector: `[data-timeline-part-id="${shellID}"]`, closest: '[data-timeline-row="AssistantPart"]' }, shell: {
selector: `[data-timeline-part-id="${renderedPartID(shellID)}"]`,
closest: '[data-timeline-row="AssistantPart"]',
},
context: { selector: group, closest: '[data-timeline-row="AssistantPart"]' }, context: { selector: group, closest: '[data-timeline-row="AssistantPart"]' },
following: { following: {
selector: `[data-timeline-part-id="${followingID}"]`, selector: `[data-timeline-part-id="${renderedPartID(followingID)}"]`,
closest: '[data-timeline-row="AssistantPart"]', closest: '[data-timeline-row="AssistantPart"]',
}, },
}) })
@@ -1,192 +0,0 @@
import { expect, test } from "@playwright/test"
import {
defineVisualRegions,
reportVisualStability,
startVisualProbe,
stopVisualProbe,
visualPlan,
} from "../../utils/visual-stability"
import {
assistantID,
assistantMessage,
event,
partUpdated,
setupTimeline,
textPart,
toolPart,
userMessage,
waitForVisualSettle,
} from "./fixture"
const inputs = {
read: { filePath: "src/a.ts", offset: 0, limit: 120 },
glob: { path: ".", pattern: "**/*.ts" },
grep: { path: ".", pattern: "stable", include: "*.ts" },
list: { path: "src" },
}
test("appends context operations while the group is expanded", async ({ page }, testInfo) => {
const firstID = "prt_append_01_read"
const followingID = "prt_append_99_following"
const timeline = await setupTimeline(page, {
messages: [
userMessage(),
assistantMessage([toolPart(firstID, "read", "running", inputs.read), textPart(followingID, "Following append")], {
completed: false,
}),
],
cpuRate: 4,
})
const initialGroup = `[data-timeline-part-ids="${firstID}"]`
await page.locator(`${initialGroup} [data-slot="collapsible-trigger"]`).click()
await waitForVisualSettle(page, [initialGroup, `[data-timeline-part-id="${followingID}"]`])
const regions = defineVisualRegions({
context: {
selector: '[data-timeline-part-ids^="prt_append_01_read"]',
closest: '[data-timeline-row="AssistantPart"]',
},
following: { selector: `[data-timeline-part-id="${followingID}"]`, closest: '[data-timeline-row="AssistantPart"]' },
})
await startVisualProbe(page, regions)
await timeline.send(partUpdated(toolPart("prt_append_02_glob", "glob", "running", inputs.glob)), 180)
await timeline.send(partUpdated(toolPart("prt_append_03_grep", "grep", "completed", inputs.grep)), 240)
await timeline.send(partUpdated(toolPart("prt_append_04_list", "list", "completed", inputs.list)), 500)
const trace = await stopVisualProbe<keyof typeof regions>(page)
await reportVisualStability(
testInfo,
"context-append",
trace,
visualPlan(
regions,
[
{ type: "required", regions: ["context", "following"] },
{ type: "unique", regions: ["context", "following"] },
{ type: "stable", regions: ["context", "following"] },
{ type: "opacity", regions: "all" },
{ type: "continuity", regions: "all" },
{ type: "motion", regions: ["following"], maxPositionReversals: 0 },
{ type: "label-stability", regions: "all" },
{ type: "preserve-bottom-anchor" },
{ type: "flow", regions: ["context", "following"] },
],
{ perMarker: true },
),
)
await expect(
page.locator(
'[data-timeline-part-ids="prt_append_01_read,prt_append_02_glob,prt_append_03_grep,prt_append_04_list"]',
),
).toBeVisible()
await expect(
page.locator('[data-timeline-part-ids^="prt_append_01_read"] [data-slot="collapsible-trigger"]'),
).toHaveAttribute("aria-expanded", "true")
})
test("splits and merges context groups when a middle text part changes", async ({ page }, testInfo) => {
const textID = "prt_split_02_text"
const followingID = "prt_split_99_following"
const timeline = await setupTimeline(page, {
messages: [
userMessage(),
assistantMessage([
toolPart("prt_split_01_read", "read", "completed", inputs.read),
textPart(textID, "Boundary"),
toolPart("prt_split_03_glob", "glob", "completed", inputs.glob),
textPart(followingID, "Following split groups"),
]),
],
cpuRate: 4,
})
const regions = defineVisualRegions({
following: { selector: `[data-timeline-part-id="${followingID}"]`, closest: '[data-timeline-row="AssistantPart"]' },
})
await startVisualProbe(page, regions)
await timeline.send(
event("message.part.removed", { sessionID: "ses_timeline_stability", messageID: assistantID, partID: textID }),
500,
)
await expect(page.locator('[data-timeline-part-ids="prt_split_01_read,prt_split_03_glob"]')).toBeVisible()
await timeline.send(partUpdated(textPart(textID, "Boundary restored")), 500)
await expect(page.locator('[data-timeline-part-ids="prt_split_01_read"]')).toBeVisible()
await expect(page.locator('[data-timeline-part-ids="prt_split_03_glob"]')).toBeVisible()
const trace = await stopVisualProbe<keyof typeof regions>(page)
await reportVisualStability(
testInfo,
"context-split-merge",
trace,
visualPlan(
regions,
[
{ type: "required", regions: ["following"] },
{ type: "unique", regions: ["following"] },
{ type: "stable", regions: ["following"] },
{ type: "opacity", regions: "all" },
{ type: "continuity", regions: "all" },
{ type: "motion", regions: "all", maxPositionReversals: 1 },
{ type: "label-stability", regions: "all" },
],
{ perMarker: true },
),
)
})
test("removing the first context member replaces the group once without overlapping following content", async ({
page,
}, testInfo) => {
const ids = ["prt_key_01_read", "prt_key_02_glob", "prt_key_03_grep"]
const followingID = "prt_key_99_following"
const timeline = await setupTimeline(page, {
messages: [
userMessage(),
assistantMessage([
toolPart(ids[0]!, "read", "completed", inputs.read),
toolPart(ids[1]!, "glob", "completed", inputs.glob),
toolPart(ids[2]!, "grep", "completed", inputs.grep),
textPart(followingID, "Following replaced group"),
]),
],
cpuRate: 4,
})
const original = page.locator(`[data-timeline-part-ids="${ids.join(",")}"]`)
const originalRowKey = await original.evaluate((element) =>
element.closest("[data-timeline-key]")?.getAttribute("data-timeline-key"),
)
await original.locator('[data-slot="collapsible-trigger"]').click()
await expect(original.locator('[data-slot="collapsible-trigger"]')).toHaveAttribute("aria-expanded", "true")
const regions = defineVisualRegions({
context: {
selector: '[data-timeline-part-ids*="prt_key_02_glob"]',
closest: '[data-timeline-row="AssistantPart"]',
},
following: { selector: `[data-timeline-part-id="${followingID}"]`, closest: '[data-timeline-row="AssistantPart"]' },
})
await startVisualProbe(page, regions)
await timeline.send(
event("message.part.removed", { sessionID: "ses_timeline_stability", messageID: assistantID, partID: ids[0] }),
500,
)
const trace = await stopVisualProbe<keyof typeof regions>(page)
await reportVisualStability(
testInfo,
"context-first-remove",
trace,
visualPlan(regions, [
{ type: "required", regions: ["context", "following"] },
{ type: "unique", regions: ["context", "following"] },
{ type: "opacity", regions: "all" },
{ type: "continuity", regions: "all" },
{ type: "motion", regions: "all", maxPositionReversals: 0 },
{ type: "label-stability", regions: "all" },
{ type: "flow", regions: ["context", "following"] },
]),
)
await expect(page.locator(`[data-timeline-part-ids="${ids.slice(1).join(",")}"]`)).toBeVisible()
expect(
await page
.locator(`[data-timeline-part-ids="${ids.slice(1).join(",")}"]`)
.evaluate((element) => element.closest("[data-timeline-key]")?.getAttribute("data-timeline-key")),
).toBe(originalRowKey)
await expect(
page.locator(`[data-timeline-part-ids="${ids.slice(1).join(",")}"] [data-slot="collapsible-trigger"]`),
).toHaveAttribute("aria-expanded", "true")
})
@@ -9,6 +9,7 @@ import {
import { import {
assistantMessage, assistantMessage,
partUpdated, partUpdated,
renderedPartID,
setupTimeline, setupTimeline,
shell, shell,
textPart, textPart,
@@ -34,13 +35,16 @@ for (const deviceScaleFactor of [1, 1.25]) {
seedHistory: true, seedHistory: true,
}) })
await waitForVisualSettle(page, [ await waitForVisualSettle(page, [
`[data-timeline-part-id="${shellID}"]`, `[data-timeline-part-id="${renderedPartID(shellID)}"]`,
`[data-timeline-part-id="${followingID}"]`, `[data-timeline-part-id="${renderedPartID(followingID)}"]`,
]) ])
const regions = defineVisualRegions({ const regions = defineVisualRegions({
shell: { selector: `[data-timeline-part-id="${shellID}"]`, closest: '[data-timeline-row="AssistantPart"]' }, shell: {
selector: `[data-timeline-part-id="${renderedPartID(shellID)}"]`,
closest: '[data-timeline-row="AssistantPart"]',
},
following: { following: {
selector: `[data-timeline-part-id="${followingID}"]`, selector: `[data-timeline-part-id="${renderedPartID(followingID)}"]`,
closest: '[data-timeline-row="AssistantPart"]', closest: '[data-timeline-row="AssistantPart"]',
}, },
}) })
@@ -71,13 +75,16 @@ for (const reducedMotion of [true]) {
seedHistory: true, seedHistory: true,
}) })
await waitForVisualSettle(page, [ await waitForVisualSettle(page, [
`[data-timeline-part-id="${shellID}"]`, `[data-timeline-part-id="${renderedPartID(shellID)}"]`,
`[data-timeline-part-id="${followingID}"]`, `[data-timeline-part-id="${renderedPartID(followingID)}"]`,
]) ])
const regions = defineVisualRegions({ const regions = defineVisualRegions({
shell: { selector: `[data-timeline-part-id="${shellID}"]`, closest: '[data-timeline-row="AssistantPart"]' }, shell: {
selector: `[data-timeline-part-id="${renderedPartID(shellID)}"]`,
closest: '[data-timeline-row="AssistantPart"]',
},
following: { following: {
selector: `[data-timeline-part-id="${followingID}"]`, selector: `[data-timeline-part-id="${renderedPartID(followingID)}"]`,
closest: '[data-timeline-row="AssistantPart"]', closest: '[data-timeline-row="AssistantPart"]',
}, },
}) })
@@ -9,6 +9,7 @@ import {
import { import {
assistantMessage, assistantMessage,
partUpdated, partUpdated,
renderedPartID,
setupTimeline, setupTimeline,
textPart, textPart,
toolPart, toolPart,
@@ -43,11 +44,17 @@ for (const profile of profiles) {
settings: { editToolPartsExpanded: true }, settings: { editToolPartsExpanded: true },
cpuRate: 4, cpuRate: 4,
}) })
await waitForVisualSettle(page, [`[data-timeline-part-id="${partID}"]`, `[data-timeline-part-id="${followingID}"]`]) await waitForVisualSettle(page, [
`[data-timeline-part-id="${renderedPartID(partID)}"]`,
`[data-timeline-part-id="${renderedPartID(followingID)}"]`,
])
const regions = defineVisualRegions({ const regions = defineVisualRegions({
tool: { selector: `[data-timeline-part-id="${partID}"]`, closest: '[data-timeline-row="AssistantPart"]' }, tool: {
selector: `[data-timeline-part-id="${renderedPartID(partID)}"]`,
closest: '[data-timeline-row="AssistantPart"]',
},
following: { following: {
selector: `[data-timeline-part-id="${followingID}"]`, selector: `[data-timeline-part-id="${renderedPartID(followingID)}"]`,
closest: '[data-timeline-row="AssistantPart"]', closest: '[data-timeline-row="AssistantPart"]',
}, },
}) })
@@ -9,6 +9,7 @@ import {
import { import {
assistantMessage, assistantMessage,
partUpdated, partUpdated,
renderedPartID,
setupTimeline, setupTimeline,
textPart, textPart,
toolPart, toolPart,
@@ -35,12 +36,23 @@ test("adds patch files incrementally without resetting outer expansion", async (
cpuRate: 4, cpuRate: 4,
seedHistory: true, seedHistory: true,
}) })
const trigger = page.locator(`[data-timeline-part-id="${patchID}"] [data-slot="collapsible-trigger"]`).first() const trigger = page
.locator(`[data-timeline-part-id="${renderedPartID(patchID)}"] [data-slot="collapsible-trigger"]`)
.first()
await expect(trigger).toHaveAttribute("aria-expanded", "true") await expect(trigger).toHaveAttribute("aria-expanded", "true")
await waitForVisualSettle(page, [`[data-timeline-part-id="${patchID}"]`, `[data-timeline-part-id="${followingID}"]`]) await waitForVisualSettle(page, [
`[data-timeline-part-id="${renderedPartID(patchID)}"]`,
`[data-timeline-part-id="${renderedPartID(followingID)}"]`,
])
const regions = defineVisualRegions({ const regions = defineVisualRegions({
patch: { selector: `[data-timeline-part-id="${patchID}"]`, closest: '[data-timeline-row="AssistantPart"]' }, patch: {
following: { selector: `[data-timeline-part-id="${followingID}"]`, closest: '[data-timeline-row="AssistantPart"]' }, selector: `[data-timeline-part-id="${renderedPartID(patchID)}"]`,
closest: '[data-timeline-row="AssistantPart"]',
},
following: {
selector: `[data-timeline-part-id="${renderedPartID(followingID)}"]`,
closest: '[data-timeline-row="AssistantPart"]',
},
}) })
await startVisualProbe(page, regions) await startVisualProbe(page, regions)
const second = patchFile("src/b.ts", "add") const second = patchFile("src/b.ts", "add")
@@ -17,19 +17,28 @@ describe("timeline fixture validation", () => {
test("rejects malformed SDK values at runtime", () => { test("rejects malformed SDK values at runtime", () => {
expect(() => expect(() =>
assistantMessage([], { assistantMessage([], {
error: { name: "APIError", data: { message: "failed" } } as never, error: { type: "APIError", message: 1 } as never,
}), }),
).toThrow() ).toThrow()
expect(() => expect(() =>
validateTimelineEvent({ validateTimelineEvent({
directory: "C:/OpenCode/TimelineStability", id: "evt_invalid_status",
payload: { created: 1,
id: "evt_invalid_status", type: "session.status",
type: "session.status", data: { sessionID: "ses_timeline_stability", status: { type: "retry", attempt: 1 } },
properties: { sessionID: "ses_timeline_stability", status: { type: "retry", attempt: 1 } },
},
}), }),
).toThrow() ).toThrow()
expect(() => validateTimelineMessages([{ ...userMessage(), id: "invalid" } as never])).toThrow()
expect(() => validateTimelineMessages([{ ...userMessage(), time: { created: "invalid" } } as never])).toThrow()
expect(() =>
validateTimelineMessages([
userMessage(),
{
...assistantMessage(),
content: [{ type: "tool", id: "call_invalid", name: "bash", state: { status: "completed" } }],
} as never,
]),
).toThrow()
}) })
test("rejects duplicate IDs and orphan assistants", () => { test("rejects duplicate IDs and orphan assistants", () => {
@@ -42,8 +51,8 @@ describe("timeline fixture validation", () => {
test("assigns deterministic event IDs", () => { test("assigns deterministic event IDs", () => {
const first = event("session.status", { sessionID: "ses_timeline_stability", status: { type: "busy" } }) const first = event("session.status", { sessionID: "ses_timeline_stability", status: { type: "busy" } })
const second = event("session.status", { sessionID: "ses_timeline_stability", status: { type: "idle" } }) const second = event("session.status", { sessionID: "ses_timeline_stability", status: { type: "idle" } })
expect(first.payload.id).toMatch(/^evt_timeline_\d{4}$/) expect(first.id).toMatch(/^evt_timeline_\d{4}$/)
expect(Number(second.payload.id.slice(-4))).toBe(Number(first.payload.id.slice(-4)) + 1) expect(Number(second.id.slice(-4))).toBe(Number(first.id.slice(-4)) + 1)
}) })
}) })
@@ -1,9 +1,16 @@
import { base64Encode } from "@opencode-ai/core/util/encode" import { base64Encode } from "@opencode-ai/core/util/encode"
import { Event } from "@opencode-ai/schema/event" import type {
import { SessionStatusEvent } from "@opencode-ai/schema/session-status-event" JsonValue,
import { SessionV1 } from "@opencode-ai/schema/session-v1" OpenCodeEvent,
import type { SessionInfo, SessionMessageInfo, SessionStatus } from "@opencode-ai/client/promise" SessionInfo,
import type { AssistantMessage, Message, Part, ToolPart, ToolState, UserMessage } from "../../../src/types" SessionMessageAssistant,
SessionMessageInfo,
SessionMessageUser,
SessionStatus,
SessionStructuredError,
} from "@opencode-ai/client/promise"
import { EventManifest } from "@opencode-ai/schema/event-manifest"
import { SessionMessage } from "@opencode-ai/schema/session-message"
import { expect, type Page } from "@playwright/test" import { expect, type Page } from "@playwright/test"
import { Schema } from "effect" import { Schema } from "effect"
import { mockOpenCodeServer } from "../../utils/mock-server" import { mockOpenCodeServer } from "../../utils/mock-server"
@@ -18,50 +25,80 @@ export const assistantID = "msg_1001_timeline_assistant"
export const title = "Timeline visual stability" export const title = "Timeline visual stability"
export const model = { providerID: "opencode", modelID: "claude-opus-4-6", variant: "max" } export const model = { providerID: "opencode", modelID: "claude-opus-4-6", variant: "max" }
const server = `http://${process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"}:${process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"}`
const tokens = { input: 100, output: 200, reasoning: 0, cache: { read: 0, write: 0 } }
type Session = SessionInfo type Session = SessionInfo
type GlobalEvent = { type TextSeed = {
directory: string id: string
project?: string type: "text"
workspace?: string text: string
payload: { messageID?: string
id: string }
type: string type FileSeed = {
properties: Record<string, unknown> id: string
} type: "file"
mime: string
filename?: string
url: string
source?: { type: string; path?: string; text?: { value: string; start: number; end: number } }
}
type AgentSeed = {
id: string
type: "agent"
name: string
source?: { value: string; start: number; end: number }
}
type ReasoningSeed = {
id: string
type: "reasoning"
text: string
time?: { start: number; end?: number }
metadata?: Record<string, unknown>
messageID?: string
}
type ToolSeed = {
id: string
type: "tool"
callID: string
tool: string
messageID?: string
executed?: boolean
providerState?: Record<string, unknown>
providerResultState?: Record<string, unknown>
state:
| { status: "pending"; input: Record<string, unknown>; raw: string }
| {
status: "running"
input: Record<string, unknown>
title?: string
metadata: Record<string, unknown>
time: { start: number }
}
| {
status: "completed"
input: Record<string, unknown>
output: string
title: string
metadata: Record<string, unknown>
time: { start: number; end: number }
}
| {
status: "error"
input: Record<string, unknown>
error: string
metadata: Record<string, unknown>
time: { start: number; end: number }
}
} }
type TimelineProperties = { export type TimelineMessage = SessionMessageUser | SessionMessageAssistant
"message.updated": { sessionID: string; info: Message } export type TimelineEvent = OpenCodeEvent | readonly OpenCodeEvent[]
"message.removed": { sessionID: string; messageID: string } export type EventPayload = OpenCodeEvent
"message.part.updated": { sessionID: string; part: Part; time: number } export type ToolStatus = ToolSeed["state"]["status"]
"message.part.removed": { sessionID: string; messageID: string; partID: string } export type PartSeed<Owner extends "user" | "assistant"> = Owner extends "user"
"message.part.delta": { sessionID: string; messageID: string; partID: string; field: string; delta: string } ? TextSeed | FileSeed | AgentSeed
"session.status": { sessionID: string; status: SessionStatus } : TextSeed | ReasoningSeed | ToolSeed
}
type TimelinePayload = {
[Type in keyof TimelineProperties]: { id: string; type: Type; properties: TimelineProperties[Type] }
}[keyof TimelineProperties]
type DeepReadonly<Value> = Value extends readonly unknown[]
? { readonly [Key in keyof Value]: DeepReadonly<Value[Key]> }
: Value extends object
? { readonly [Key in keyof Value]: DeepReadonly<Value[Key]> }
: Value
export type TimelineEvent = DeepReadonly<Omit<GlobalEvent, "payload"> & { payload: TimelinePayload }>
export type EventPayload = TimelineEvent
export type ToolStatus = ToolState["status"]
export type TimelineMessage = { info: UserMessage; parts: Part[] } | { info: AssistantMessage; parts: Part[] }
type UserPart = Extract<Part, { type: "text" | "file" | "agent" | "subtask" }>
type AssistantPart = Exclude<Part, { type: "agent" | "subtask" }>
type OwnedPart<Owner extends Message["role"]> = Owner extends "user" ? UserPart : AssistantPart
export type PartSeed<Owner extends Message["role"]> =
OwnedPart<Owner> extends infer Candidate
? Candidate extends Part
? Omit<Candidate, "sessionID" | "messageID">
: never
: never
type ToolOptions<State extends ToolStatus> = State extends "pending" type ToolOptions<State extends ToolStatus> = State extends "pending"
? { output?: never; title?: never; metadata?: never; error?: never } ? { output?: never; title?: never; metadata?: never; error?: never }
@@ -71,26 +108,19 @@ type ToolOptions<State extends ToolStatus> = State extends "pending"
? { error?: string; metadata?: Record<string, unknown>; output?: never; title?: never } ? { error?: string; metadata?: Record<string, unknown>; output?: never; title?: never }
: { output?: string; title?: string; metadata?: Record<string, unknown>; error?: never } : { output?: string; title?: string; metadata?: Record<string, unknown>; error?: never }
const decodeOptions = { errors: "all", onExcessProperty: "error" } as const type PartRef = { messageID: string; type: "text" | "reasoning" | "tool"; ordinal?: number }
const decodeMessage = Schema.decodeUnknownSync(SessionV1.WithParts) const partRefs = new Map<string, PartRef>()
const decodePart = Schema.decodeUnknownSync(SessionV1.Part) const nextOrdinals = new Map<string, { text: number; reasoning: number }>()
const decodeStatus = Schema.decodeUnknownSync(SessionStatusEvent.Info) const startedParts = new Set<string>()
const timelineEventSchema = Schema.Union([ const toolStates = new Map<string, ToolStatus>()
eventSchema("message.updated", SessionV1.Event.MessageUpdated.data),
eventSchema("message.removed", SessionV1.Event.MessageRemoved.data),
eventSchema("message.part.updated", SessionV1.Event.PartUpdated.data),
eventSchema("message.part.removed", SessionV1.Event.PartRemoved.data),
eventSchema("message.part.delta", SessionV1.Event.PartDelta.data),
eventSchema("session.status", SessionStatusEvent.Status.data),
])
const decodeEvent = Schema.decodeUnknownSync(timelineEventSchema)
let eventSequence = 0 let eventSequence = 0
let durableSequence = -1
export async function setupTimeline( export async function setupTimeline(
page: Page, page: Page,
input: { input: {
messages?: TimelineMessage[] messages?: TimelineMessage[]
currentMessages?: SessionMessageInfo[] sessionMessages?: SessionMessageInfo[]
sessionStatus?: Record<string, SessionStatus> sessionStatus?: Record<string, SessionStatus>
settings?: Record<string, boolean> settings?: Record<string, boolean>
sessions?: Session[] sessions?: Session[]
@@ -103,40 +133,26 @@ export async function setupTimeline(
seedHistory?: boolean seedHistory?: boolean
} = {}, } = {},
) { ) {
eventSequence = 0
durableSequence = -1
const sessions = input.sessions ?? [session()] const sessions = input.sessions ?? [session()]
const messages = const messages =
input.currentMessages ?? input.sessionMessages ??
validateTimelineMessages([ validateTimelineMessages([
...(input.seedHistory ? historyMessages(18) : []), ...(input.seedHistory ? historyMessages(18) : []),
...(input.messages ?? [userMessage(), assistantMessage()]), ...(input.messages ?? [userMessage(), assistantMessage()]),
]) ])
const active = messages.findLast((message) => const active = messages.findLast((message) => message.type === "assistant")
"info" in message ? message.info.role === "assistant" : message.type === "assistant", const initialStatus: SessionStatus =
) active?.type === "assistant" && active.time.completed === undefined ? { type: "busy" } : { type: "idle" }
const initialStatus = decodeStatus( const transport = await installSseTransport(page, { server, retry: input.eventRetry ?? 20 })
active &&
("info" in active
? active.info.role === "assistant" && active.info.time.completed === undefined
: active.type === "assistant" && active.time.completed === undefined)
? { type: "busy" }
: { type: "idle" },
decodeOptions,
)
const server = `http://${process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"}:${process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"}`
const transport = await installSseTransport<EventPayload>(page, {
server,
retry: input.eventRetry ?? 20,
})
await mockOpenCodeServer(page, { await mockOpenCodeServer(page, {
protocol: "v2",
directory, directory,
project: project(), project: project(),
provider: provider(), provider: provider(),
sessions, sessions,
sessionStatus: input.sessionStatus ?? { [sessionID]: initialStatus }, sessionStatus: input.sessionStatus ?? { [sessionID]: initialStatus },
pageMessages: () => ({ pageMessages: () => ({ items: messages }),
items: messages,
}),
}) })
await page.addInitScript((settings) => { await page.addInitScript((settings) => {
localStorage.setItem( localStorage.setItem(
@@ -151,9 +167,6 @@ export async function setupTimeline(
}, },
}), }),
) )
if (settings.newLayoutDesigns === false) {
localStorage.setItem("app-version.v1", JSON.stringify({ version: "1.17.20" }))
}
}, input.settings ?? {}) }, input.settings ?? {})
if (input.locale) { if (input.locale) {
await page.addInitScript((locale) => { await page.addInitScript((locale) => {
@@ -172,7 +185,7 @@ export async function setupTimeline(
mobile: false, mobile: false,
}) })
} }
await page.goto(`/${base64Encode(directory)}/session/${sessionID}`) await page.goto(`/server/${base64Encode(server)}/session/${sessionID}`)
await expectSessionReady(page, { server, sessionID, title }) await expectSessionReady(page, { server, sessionID, title })
await transport.waitForConnection() await transport.waitForConnection()
if (input.cpuRate && input.cpuRate > 1) { if (input.cpuRate && input.cpuRate > 1) {
@@ -182,15 +195,25 @@ export async function setupTimeline(
return { return {
transport, transport,
async send(event: TimelineEvent, delay = 0) { async send(input: TimelineEvent, delay = 0) {
const valid = validateTimelineEvent(event) const events = timelineEvents(input)
await transport.send(valid, { marker: describeEvent(valid) }) if (events.length === 1) await transport.send(events[0]!, { marker: describeEvent(events[0]!) })
if (events.length > 1)
await transport.burst(
events,
events.map((item) => ({ marker: describeEvent(item) })),
)
if (delay) await page.waitForTimeout(delay) if (delay) await page.waitForTimeout(delay)
}, },
async sendAll(sequence: { event: TimelineEvent; delay: number }[]) { async sendAll(sequence: { event: TimelineEvent; delay: number }[]) {
for (const item of sequence) { for (const item of sequence) {
const valid = validateTimelineEvent(item.event) const events = timelineEvents(item.event)
await transport.send(valid, { marker: describeEvent(valid) }) if (events.length === 1) await transport.send(events[0]!, { marker: describeEvent(events[0]!) })
if (events.length > 1)
await transport.burst(
events,
events.map((event) => ({ marker: describeEvent(event) })),
)
await page.waitForTimeout(item.delay) await page.waitForTimeout(item.delay)
} }
}, },
@@ -210,72 +233,63 @@ export async function setupTimeline(
) )
}, },
async waitForPart(partID: string) { async waitForPart(partID: string) {
const part = page.locator(`[data-timeline-part-id="${partID}"]`) const part = page.locator(`[data-timeline-part-id="${renderedPartID(partID)}"]`)
await expect(part).toHaveCount(1) await expect(part).toHaveCount(1)
await expect(part).toBeVisible() await expect(part).toBeVisible()
}, },
} }
} }
function describeEvent(event: EventPayload) { function timelineEvents(input: TimelineEvent) {
if (event.payload.type === "message.part.updated") { return (Array.isArray(input) ? input : [input]).map(validateTimelineEvent)
const part = event.payload.properties.part
return [
event.payload.type,
part.id,
part.type === "tool" ? part.tool : part.type,
part.type === "tool" ? part.state.status : undefined,
]
.filter(Boolean)
.join(":")
}
if (event.payload.type === "session.status") {
const status = event.payload.properties.status
return [event.payload.type, status.type, status.type === "retry" ? status.attempt : undefined]
.filter((value) => value !== undefined)
.join(":")
}
return event.payload.type
} }
export function event<const Type extends TimelinePayload["type"]>( function describeEvent(event: OpenCodeEvent) {
type: Type, if (event.type.startsWith("session.tool.")) {
properties: Extract<TimelinePayload, { type: Type }>["properties"], const data = event.data as { id?: string }
): TimelineEvent return [event.type, data.id].filter(Boolean).join(":")
export function event(type: TimelinePayload["type"], properties: TimelinePayload["properties"]): TimelineEvent { }
return validateTimelineEvent({ return event.type
directory,
payload: { id: `evt_timeline_${String(++eventSequence).padStart(4, "0")}`, type, properties },
})
} }
export function validateTimelineEvent(input: unknown): TimelineEvent { export function event(
return decodeEvent(input, decodeOptions) as TimelineEvent type: "session.status",
data: Extract<OpenCodeEvent, { type: "session.status" }>["data"],
): OpenCodeEvent {
return makeEvent(type, data)
}
export function validateTimelineEvent(input: unknown): OpenCodeEvent {
if (!input || typeof input !== "object") throw new Error("Timeline event must be an object")
if (!("type" in input) || typeof input.type !== "string") throw new Error("Timeline event requires a type")
const definition = EventManifest.ServerDefinitions.find((definition) => definition.type === input.type)
if (!definition) throw new Error(`Unknown timeline event: ${input.type}`)
return Schema.decodeUnknownSync(definition)(input) as OpenCodeEvent
} }
export function validateTimelineMessages(input: readonly TimelineMessage[]): TimelineMessage[] { export function validateTimelineMessages(input: readonly TimelineMessage[]): TimelineMessage[] {
input.forEach((message) => decodeMessage(message, decodeOptions)) const messages = input.map((message): TimelineMessage => {
const messages = [...input] const decoded = Schema.decodeUnknownSync(SessionMessage.Info)(message)
if (decoded.type !== "user" && decoded.type !== "assistant")
throw new Error(`Unsupported timeline message type: ${decoded.type}`)
return message
})
const messageIDs = new Set<string>() const messageIDs = new Set<string>()
const partIDs = new Set<string>() let parentID: string | undefined
const users = new Set(messages.filter((message) => message.info.role === "user").map((message) => message.info.id))
messages.forEach((message) => { messages.forEach((message) => {
if (messageIDs.has(message.info.id)) if (messageIDs.has(message.id)) throw new Error(`Timeline fixture has duplicate message ID: ${message.id}`)
throw new Error(`Timeline fixture has duplicate message ID: ${message.info.id}`) messageIDs.add(message.id)
messageIDs.add(message.info.id) if (message.type === "user") parentID = message.id
if (message.info.role === "assistant" && !users.has(message.info.parentID)) if (message.type === "assistant") {
throw new Error(`Timeline assistant ${message.info.id} must reference a parent user in the fixture`) const expected = typeof message.metadata?.parentID === "string" ? message.metadata.parentID : parentID
message.parts.forEach((part) => { if (!expected || expected !== parentID)
if (part.sessionID !== message.info.sessionID || part.messageID !== message.info.id) throw new Error(`Timeline assistant ${message.id} must reference a parent user in the fixture`)
throw new Error(`Timeline part ${part.id} ownership does not match message ${message.info.id}`) message.content.forEach((part) => {
if (message.info.role === "user" && !["text", "file", "agent", "subtask"].includes(part.type)) if (part.type !== "tool") return
throw new Error(`Timeline user message ${message.info.id} cannot own ${part.type} part ${part.id}`) if (partRefs.has(part.id) && partRefs.get(part.id)?.messageID !== message.id)
if (message.info.role === "assistant" && ["agent", "subtask"].includes(part.type)) throw new Error(`Timeline fixture has duplicate part ID: ${part.id}`)
throw new Error(`Timeline assistant message ${message.info.id} cannot own ${part.type} part ${part.id}`) })
if (partIDs.has(part.id)) throw new Error(`Timeline fixture has duplicate part ID: ${part.id}`) }
partIDs.add(part.id)
})
}) })
return messages return messages
} }
@@ -337,55 +351,137 @@ export function historyMessages(count: number): TimelineMessage[] {
}).flat() }).flat()
} }
export function partUpdated(part: Part | PartSeed<"assistant">) { export function partUpdated(part: PartSeed<"assistant">): readonly OpenCodeEvent[] {
const owned = "messageID" in part ? part : { ...part, sessionID, messageID: assistantID } const messageID = part.messageID ?? assistantID
decodePart(owned, decodeOptions) const started = startedParts.has(part.id)
return event("message.part.updated", { const ref = partRef(part.id, messageID, part.type)
sessionID, if (part.type === "text") {
part: owned, startedParts.add(part.id)
time: 1700000002000, return [
}) ...(started
? []
: [makeEvent("session.text.started", { sessionID, assistantMessageID: messageID, ordinal: ref.ordinal! })]),
makeEvent("session.text.ended", {
sessionID,
assistantMessageID: messageID,
ordinal: ref.ordinal!,
text: part.text,
}),
]
}
if (part.type === "reasoning") {
startedParts.add(part.id)
return [
...(started
? []
: [
makeEvent("session.reasoning.started", {
sessionID,
assistantMessageID: messageID,
ordinal: ref.ordinal!,
state: jsonRecord(part.metadata),
}),
]),
makeEvent("session.reasoning.ended", {
sessionID,
assistantMessageID: messageID,
ordinal: ref.ordinal!,
text: part.text,
state: jsonRecord(part.metadata),
}),
]
}
return toolEvents(part, messageID)
}
export function renderedPartID(partID: string) {
const ref = partRefs.get(partID)
if (!ref || ref.type === "tool") return partID
return `${ref.messageID}:${ref.type}:${ref.ordinal}`
} }
export function partDelta(partID: string, delta: string, messageID = assistantID) { export function partDelta(partID: string, delta: string, messageID = assistantID) {
return event("message.part.delta", { sessionID, messageID, partID, field: "text", delta }) const ref = partRefs.get(partID)
if (!ref || ref.type !== "text" || ref.ordinal === undefined) throw new Error(`Unknown text part: ${partID}`)
return makeEvent("session.text.delta", {
sessionID,
assistantMessageID: messageID,
ordinal: ref.ordinal,
delta,
})
} }
export function messageUpdated(info: Message) { export function messageUpdated(info: SessionMessageAssistant) {
return event("message.updated", { sessionID, info }) if (info.error)
return makeEvent("session.step.failed", {
sessionID,
assistantMessageID: info.id,
error: info.error,
cost: info.cost,
tokens: info.tokens,
})
return makeEvent("session.step.ended", {
sessionID,
assistantMessageID: info.id,
finish: info.finish ?? "stop",
cost: info.cost ?? 0,
tokens: info.tokens ?? tokens,
})
} }
export function status(type: SessionStatus["type"], attempt = 1) { export function status(type: SessionStatus["type"], attempt = 1) {
return event("session.status", { if (type === "busy") return makeEvent("session.execution.started", { sessionID })
if (type === "idle") return makeEvent("session.execution.succeeded", { sessionID })
return makeEvent("session.retry.scheduled", {
sessionID, sessionID,
status: type === "retry" ? { type, attempt, message: "Rate limited", next: 1700000010000 } : { type }, assistantMessageID: assistantID,
attempt,
at: 1700000010000,
error: { type: "provider.error", message: "Rate limited" },
}) })
} }
export function userMessage( export function userMessage(
parts?: PartSeed<"user">[], parts?: PartSeed<"user">[],
input: { id?: string; summary?: UserMessage["summary"]; created?: number } = {}, input: { id?: string; summary?: unknown; created?: number } = {},
): Extract<TimelineMessage, { info: { role: "user" } }> { ): SessionMessageUser {
const id = input.id ?? userID const id = input.id ?? userID
const seeds = parts ?? [userText("Build the timeline stability matrix.", { id: `prt_${id}_text` })] const seeds = parts ?? [userText("Build the timeline stability matrix.", { id: `prt_${id}_text` })]
const message = { return {
info: { id,
id, type: "user",
sessionID, time: { created: input.created ?? 1700000000000 },
role: "user", text: seeds.flatMap((part) => (part.type === "text" ? [part.text] : [])).join("\n"),
time: { created: input.created ?? 1700000000000 }, files: seeds.flatMap((part) => {
summary: input.summary ?? { diffs: [] }, if (part.type !== "file") return []
agent: "build", const mention = part.source?.text
model, ? { text: part.source.text.value, start: part.source.text.start, end: part.source.text.end }
}, : undefined
parts: seeds.map((part) => ({ return [
...part, {
sessionID, data: part.url.match(/^data:[^,]*;base64,(.*)$/)?.[1] ?? "",
messageID: id, mime: part.mime,
})), source: part.url.startsWith("data:")
} satisfies Extract<TimelineMessage, { info: { role: "user" } }> ? ({ type: "inline" } as const)
decodeMessage(message, decodeOptions) : ({ type: "uri", uri: part.source?.path ?? part.url } as const),
return message ...(part.filename ? { name: part.filename } : {}),
...(mention ? { mention } : {}),
},
]
}),
agents: seeds.flatMap((part) => {
if (part.type !== "agent") return []
return [
{
name: part.name,
...(part.source
? { mention: { text: part.source.value, start: part.source.start, end: part.source.end } }
: {}),
},
]
}),
...(input.summary === undefined ? {} : { metadata: { summary: input.summary as JsonValue } }),
}
} }
export function assistantMessage( export function assistantMessage(
@@ -394,49 +490,43 @@ export function assistantMessage(
id?: string id?: string
parentID?: string parentID?: string
completed?: boolean completed?: boolean
error?: AssistantMessage["error"] error?: SessionStructuredError
created?: number created?: number
} = {}, } = {},
): Extract<TimelineMessage, { info: { role: "assistant" } }> { ): SessionMessageAssistant {
if (input.error && (typeof input.error.type !== "string" || typeof input.error.message !== "string"))
throw new Error("Invalid assistant error")
const id = input.id ?? assistantID const id = input.id ?? assistantID
const message = { const created = input.created ?? 1700000001000
info: { const ordinals = { text: 0, reasoning: 0 }
id, const content = parts.map((part) => messageContent(part, id, ordinals))
sessionID, nextOrdinals.set(id, ordinals)
role: "assistant", return {
time: { id,
created: input.created ?? 1700000001000, type: "assistant",
...(input.completed === false ? {} : { completed: (input.created ?? 1700000001000) + 1_000 }), metadata: { parentID: input.parentID ?? userID },
}, time: { created, ...(input.completed === false ? {} : { completed: created + 1_000 }) },
parentID: input.parentID ?? userID, model: { id: model.modelID, providerID: model.providerID, variant: model.variant },
modelID: model.modelID, agent: "build",
providerID: model.providerID, content,
mode: "build", cost: 0.01,
agent: "build", tokens,
path: { cwd: directory, root: directory }, ...(input.completed === false ? {} : { finish: "stop" as const }),
cost: 0.01, ...(input.error ? { error: input.error } : {}),
tokens: { input: 100, output: 200, reasoning: 0, cache: { read: 0, write: 0 } }, }
variant: "max",
...(input.error ? { error: input.error } : {}),
},
parts: parts.map((part) => ({ ...part, sessionID, messageID: id })),
} satisfies Extract<TimelineMessage, { info: { role: "assistant" } }>
decodeMessage(message, decodeOptions)
return message
} }
export function userText( export function userText(text: string, input: Partial<Omit<TextSeed, "type" | "text">> = {}): TextSeed {
text: string,
input: Partial<Omit<Extract<PartSeed<"user">, { type: "text" }>, "type" | "text">> = {},
): Extract<PartSeed<"user">, { type: "text" }> {
return { id: "prt_user_text", type: "text", text, ...input } return { id: "prt_user_text", type: "text", text, ...input }
} }
export function textPart(id: string, text: string): Extract<PartSeed<"assistant">, { type: "text" }> { export function textPart(id: string, text: string): TextSeed {
partRef(id, assistantID, "text")
return { id, type: "text", text } return { id, type: "text", text }
} }
export function reasoningPart(id: string, text: string): Extract<PartSeed<"assistant">, { type: "reasoning" }> { export function reasoningPart(id: string, text: string): ReasoningSeed {
partRef(id, assistantID, "reasoning")
return { id, type: "reasoning", text, time: { start: 1700000001000 } } return { id, type: "reasoning", text, time: { start: 1700000001000 } }
} }
@@ -446,35 +536,35 @@ export function toolPart(
state: "pending", state: "pending",
input: Record<string, unknown>, input: Record<string, unknown>,
options?: ToolOptions<"pending">, options?: ToolOptions<"pending">,
): Omit<ToolPart, "sessionID" | "messageID"> ): ToolSeed
export function toolPart( export function toolPart(
id: string, id: string,
tool: string, tool: string,
state: "running", state: "running",
input: Record<string, unknown>, input: Record<string, unknown>,
options?: ToolOptions<"running">, options?: ToolOptions<"running">,
): Omit<ToolPart, "sessionID" | "messageID"> ): ToolSeed
export function toolPart( export function toolPart(
id: string, id: string,
tool: string, tool: string,
state: "completed", state: "completed",
input: Record<string, unknown>, input: Record<string, unknown>,
options?: ToolOptions<"completed">, options?: ToolOptions<"completed">,
): Omit<ToolPart, "sessionID" | "messageID"> ): ToolSeed
export function toolPart( export function toolPart(
id: string, id: string,
tool: string, tool: string,
state: "error", state: "error",
input: Record<string, unknown>, input: Record<string, unknown>,
options?: ToolOptions<"error">, options?: ToolOptions<"error">,
): Omit<ToolPart, "sessionID" | "messageID"> ): ToolSeed
export function toolPart( export function toolPart(
id: string, id: string,
tool: string, tool: string,
state: ToolStatus, state: ToolStatus,
input: Record<string, unknown>, input: Record<string, unknown>,
options: ToolOptions<ToolStatus> = {}, options: ToolOptions<ToolStatus> = {},
): Omit<ToolPart, "sessionID" | "messageID"> { ): ToolSeed {
const base = { id, type: "tool" as const, callID: id, tool } const base = { id, type: "tool" as const, callID: id, tool }
if (state === "pending") return { ...base, state: { status: state, input, raw: "" } } if (state === "pending") return { ...base, state: { status: state, input, raw: "" } }
if (state === "running") if (state === "running")
@@ -512,12 +602,7 @@ export function toolPart(
} }
} }
export function shell( export function shell(id: string, state: ToolStatus, output = "", command = `echo ${id}`): ToolSeed {
id: string,
state: ToolStatus,
output = "",
command = `echo ${id}`,
): Omit<ToolPart, "sessionID" | "messageID"> {
if (state === "pending") return toolPart(id, "bash", state, { command }) if (state === "pending") return toolPart(id, "bash", state, { command })
if (state === "running") if (state === "running")
return toolPart(id, "bash", state, { command }, { title: command, metadata: { command, output } }) return toolPart(id, "bash", state, { command }, { title: command, metadata: { command, output } })
@@ -526,7 +611,7 @@ export function shell(
return toolPart(id, "bash", state, { command }, { title: command, output, metadata: { command, output } }) return toolPart(id, "bash", state, { command }, { title: command, output, metadata: { command, output } })
} }
export function completedAssistantInfo(info: AssistantMessage): AssistantMessage { export function completedAssistantInfo(info: SessionMessageAssistant): SessionMessageAssistant {
return { ...info, time: { ...info.time, completed: 1700000003000 } } return { ...info, time: { ...info.time, completed: 1700000003000 } }
} }
@@ -554,16 +639,200 @@ export function session(input: Partial<Session> = {}): Session {
} }
} }
function eventSchema< function messageContent(
const Type extends TimelinePayload["type"], part: PartSeed<"assistant">,
const Properties extends Schema.Codec<unknown, unknown>, messageID: string,
>(type: Type, properties: Properties) { ordinals: { text: number; reasoning: number },
return Schema.Struct({ ): SessionMessageAssistant["content"][number] {
directory: Schema.String, if (part.type === "tool") {
project: Schema.optional(Schema.String), partRefs.set(part.id, { messageID, type: part.type })
workspace: Schema.optional(Schema.String), toolStates.set(part.callID, part.state.status)
payload: Schema.Struct({ id: Event.ID, type: Schema.Literal(type), properties }), } else {
}) partRefs.set(part.id, { messageID, type: part.type, ordinal: ordinals[part.type]++ })
startedParts.add(part.id)
}
if (part.type === "text") return { type: "text", text: part.text }
if (part.type === "reasoning")
return {
type: "reasoning",
text: part.text,
state: jsonRecord(part.metadata),
time: part.time
? { created: part.time.start, ...(part.time.end === undefined ? {} : { completed: part.time.end }) }
: undefined,
}
const state = part.state
const time = "time" in state ? state.time : undefined
const completed = state.status === "completed" || state.status === "error" ? state.time.end : undefined
const base = {
type: "tool" as const,
id: part.callID,
name: part.tool,
time: {
created: time?.start ?? 1700000001000,
...(time?.start === undefined ? {} : { ran: time.start }),
...(completed === undefined ? {} : { completed }),
},
...(part.executed === undefined ? {} : { executed: part.executed }),
...(part.providerState ? { providerState: jsonRecord(part.providerState) } : {}),
...(part.providerResultState ? { providerResultState: jsonRecord(part.providerResultState) } : {}),
}
if (state.status === "pending") return { ...base, state: { status: "streaming", input: state.raw } }
if (state.status === "running")
return {
...base,
state: { status: "running", input: jsonRecord(state.input), metadata: jsonRecord(state.metadata) },
}
if (state.status === "error")
return {
...base,
state: {
status: "error",
input: jsonRecord(state.input),
error: { type: "ToolError", message: state.error },
metadata: jsonRecord(state.metadata),
},
}
return {
...base,
state: {
status: "completed",
input: jsonRecord(state.input),
content: [{ type: "text", text: state.output }],
metadata: jsonRecord(state.metadata),
},
}
}
function toolEvents(part: ToolSeed, messageID: string): readonly OpenCodeEvent[] {
const previous = toolStates.get(part.callID)
if (previous === "completed" || previous === "error") return []
const events: OpenCodeEvent[] = []
if (!previous) {
events.push(
makeEvent("session.tool.input.started", {
sessionID,
assistantMessageID: messageID,
id: part.callID,
name: part.tool,
}),
)
}
if (part.state.status === "pending") {
toolStates.set(part.callID, part.state.status)
return events
}
if (!previous || previous === "pending") {
events.push(
makeEvent("session.tool.input.ended", {
sessionID,
assistantMessageID: messageID,
id: part.callID,
text: JSON.stringify(part.state.input),
}),
makeEvent("session.tool.called", {
sessionID,
assistantMessageID: messageID,
id: part.callID,
input: part.state.input,
executed: part.executed ?? true,
state: jsonRecord(part.providerState),
}),
)
}
if (part.state.status === "running") {
if (previous === "running" || Object.keys(part.state.metadata).length)
events.push(
makeEvent("session.tool.progress", {
sessionID,
assistantMessageID: messageID,
id: part.callID,
metadata: jsonRecord(part.state.metadata),
}),
)
toolStates.set(part.callID, part.state.status)
return events
}
if (part.state.status === "error") {
events.push(
makeEvent("session.tool.failed", {
sessionID,
assistantMessageID: messageID,
id: part.callID,
error: { type: "ToolError", message: part.state.error },
metadata: jsonRecord(part.state.metadata),
executed: part.executed ?? true,
resultState: jsonRecord(part.providerResultState),
}),
)
toolStates.set(part.callID, part.state.status)
return events
}
events.push(
makeEvent("session.tool.success", {
sessionID,
assistantMessageID: messageID,
id: part.callID,
content: [{ type: "text", text: part.state.output }],
metadata: jsonRecord(part.state.metadata),
executed: part.executed ?? true,
resultState: jsonRecord(part.providerResultState),
}),
)
toolStates.set(part.callID, part.state.status)
return events
}
function partRef(id: string, messageID: string, type: PartRef["type"]): PartRef {
const current = partRefs.get(id)
if (current) return current
if (type === "tool") {
const ref = { messageID, type } satisfies PartRef
partRefs.set(id, ref)
return ref
}
const next = nextOrdinals.get(messageID) ?? { text: 0, reasoning: 0 }
const ref = { messageID, type, ordinal: next[type]++ } satisfies PartRef
nextOrdinals.set(messageID, next)
partRefs.set(id, ref)
return ref
}
function makeEvent<Type extends OpenCodeEvent["type"]>(
type: Type,
data: Extract<OpenCodeEvent, { type: Type }>["data"],
): OpenCodeEvent {
const id = `evt_timeline_${String(++eventSequence).padStart(4, "0")}`
const base = { id, created: 1700000002000 + eventSequence, type, data, location: { directory } }
const definition = EventManifest.ServerDefinitions.find((definition) => definition.type === type)
if (!definition) throw new Error(`Unknown timeline event: ${type}`)
const input =
definition.durability === "durable"
? {
...base,
durable: { aggregateID: sessionID, seq: ++durableSequence, version: definition.durable.version },
}
: base
return Schema.decodeUnknownSync(definition)(input) as unknown as OpenCodeEvent
}
function jsonRecord(value: Record<string, unknown> | undefined): Record<string, JsonValue> {
if (!value) return {}
return Object.fromEntries(
Object.entries(value).flatMap(([key, item]) => {
const next = jsonValue(item)
return next === undefined ? [] : [[key, next]]
}),
)
}
function jsonValue(value: unknown): JsonValue | undefined {
if (value === null || typeof value === "string" || typeof value === "boolean") return value
if (typeof value === "number") return Number.isFinite(value) ? value : null
if (Array.isArray(value)) return value.map((item) => jsonValue(item) ?? null)
if (!value || typeof value !== "object") return
return jsonRecord(value as Record<string, unknown>)
} }
function provider() { function provider() {
@@ -6,7 +6,16 @@ import {
stopVisualProbe, stopVisualProbe,
visualPlan, visualPlan,
} from "../../utils/visual-stability" } from "../../utils/visual-stability"
import { assistantMessage, setupTimeline, shell, textPart, toolPart, userMessage, waitForVisualSettle } from "./fixture" import {
assistantMessage,
renderedPartID,
setupTimeline,
shell,
textPart,
toolPart,
userMessage,
waitForVisualSettle,
} from "./fixture"
test("expands and collapses a long completed shell without overlap", async ({ page }, testInfo) => { test("expands and collapses a long completed shell without overlap", async ({ page }, testInfo) => {
const shellID = "prt_interaction_01_shell" const shellID = "prt_interaction_01_shell"
@@ -20,11 +29,20 @@ test("expands and collapses a long completed shell without overlap", async ({ pa
cpuRate: 4, cpuRate: 4,
seedHistory: true, seedHistory: true,
}) })
const trigger = page.locator(`[data-timeline-part-id="${shellID}"] [data-slot="collapsible-trigger"]`) const trigger = page.locator(`[data-timeline-part-id="${renderedPartID(shellID)}"] [data-slot="collapsible-trigger"]`)
await waitForVisualSettle(page, [`[data-timeline-part-id="${shellID}"]`, `[data-timeline-part-id="${followingID}"]`]) await waitForVisualSettle(page, [
`[data-timeline-part-id="${renderedPartID(shellID)}"]`,
`[data-timeline-part-id="${renderedPartID(followingID)}"]`,
])
const regions = defineVisualRegions({ const regions = defineVisualRegions({
shell: { selector: `[data-timeline-part-id="${shellID}"]`, closest: '[data-timeline-row="AssistantPart"]' }, shell: {
following: { selector: `[data-timeline-part-id="${followingID}"]`, closest: '[data-timeline-row="AssistantPart"]' }, selector: `[data-timeline-part-id="${renderedPartID(shellID)}"]`,
closest: '[data-timeline-row="AssistantPart"]',
},
following: {
selector: `[data-timeline-part-id="${renderedPartID(followingID)}"]`,
closest: '[data-timeline-row="AssistantPart"]',
},
}) })
const plan = visualPlan(regions, [ const plan = visualPlan(regions, [
{ type: "required", regions: ["shell", "following"] }, { type: "required", regions: ["shell", "following"] },
@@ -76,7 +94,7 @@ test("expands and collapses a completed context group without overlap", async ({
seedHistory: true, seedHistory: true,
}) })
const trigger = page.locator(`${group} [data-slot="collapsible-trigger"]`) const trigger = page.locator(`${group} [data-slot="collapsible-trigger"]`)
await waitForVisualSettle(page, [group, `[data-timeline-part-id="${followingID}"]`]) await waitForVisualSettle(page, [group, `[data-timeline-part-id="${renderedPartID(followingID)}"]`])
for (const [name, expanded] of [ for (const [name, expanded] of [
["context-expand", true], ["context-expand", true],
["context-collapse", false], ["context-collapse", false],
@@ -85,7 +103,7 @@ test("expands and collapses a completed context group without overlap", async ({
const regions = defineVisualRegions({ const regions = defineVisualRegions({
context: { selector: group, closest: '[data-timeline-row="AssistantPart"]' }, context: { selector: group, closest: '[data-timeline-row="AssistantPart"]' },
following: { following: {
selector: `[data-timeline-part-id="${followingID}"]`, selector: `[data-timeline-part-id="${renderedPartID(followingID)}"]`,
closest: '[data-timeline-row="AssistantPart"]', closest: '[data-timeline-row="AssistantPart"]',
}, },
}) })
@@ -144,11 +162,22 @@ test("expands and collapses an edit diff without moving twice", async ({ page },
cpuRate: 4, cpuRate: 4,
seedHistory: true, seedHistory: true,
}) })
const trigger = page.locator(`[data-timeline-part-id="${editID}"] [data-slot="collapsible-trigger"]`).first() const trigger = page
await waitForVisualSettle(page, [`[data-timeline-part-id="${editID}"]`, `[data-timeline-part-id="${followingID}"]`]) .locator(`[data-timeline-part-id="${renderedPartID(editID)}"] [data-slot="collapsible-trigger"]`)
.first()
await waitForVisualSettle(page, [
`[data-timeline-part-id="${renderedPartID(editID)}"]`,
`[data-timeline-part-id="${renderedPartID(followingID)}"]`,
])
const regions = defineVisualRegions({ const regions = defineVisualRegions({
edit: { selector: `[data-timeline-part-id="${editID}"]`, closest: '[data-timeline-row="AssistantPart"]' }, edit: {
following: { selector: `[data-timeline-part-id="${followingID}"]`, closest: '[data-timeline-row="AssistantPart"]' }, selector: `[data-timeline-part-id="${renderedPartID(editID)}"]`,
closest: '[data-timeline-row="AssistantPart"]',
},
following: {
selector: `[data-timeline-part-id="${renderedPartID(followingID)}"]`,
closest: '[data-timeline-row="AssistantPart"]',
},
}) })
await startVisualProbe(page, regions) await startVisualProbe(page, regions)
await trigger.click() await trigger.click()
@@ -173,64 +202,6 @@ test("expands and collapses an edit diff without moving twice", async ({ page },
) )
}) })
test("shows all and expands historical diff summary without overlap", async ({ page }, testInfo) => {
const firstUser = userMessage(undefined, {
summary: {
diffs: Array.from({ length: 12 }, (_, index) => ({
file: `src/diff-${index}.ts`,
status: "modified",
additions: 1,
deletions: 1,
patch: `@@ -1 +1 @@\n-export const value = ${index}\n+export const value = ${index + 1}`,
})),
},
})
const nextUserID = "msg_2000_diff_interaction_user"
await setupTimeline(page, {
messages: [
firstUser,
assistantMessage(),
userMessage(undefined, { id: nextUserID, created: 1700000010000 }),
assistantMessage([], {
id: "msg_2001_diff_interaction_assistant",
parentID: nextUserID,
created: 1700000011000,
}),
],
cpuRate: 4,
})
const scroller = page.locator(".scroll-view__viewport", { has: page.locator("[data-timeline-row]") })
await scroller.evaluate((element) => (element.scrollTop = 0))
const diff = page.locator('[data-timeline-row="DiffSummary"]')
const following = page.locator(`[data-message-id="${nextUserID}"]`).first()
await expect(diff).toBeVisible()
const regions = defineVisualRegions({
diff: { selector: '[data-timeline-row="DiffSummary"]' },
following: { selector: `[data-message-id="${nextUserID}"]` },
})
await startVisualProbe(page, regions)
await page.getByText(/show all/i).click()
await page.waitForTimeout(500)
await diff.locator('[data-slot="session-turn-diff-trigger"]').first().click()
await page.waitForTimeout(900)
const trace = await stopVisualProbe<keyof typeof regions>(page)
await reportVisualStability(
testInfo,
"diff-summary-expand",
trace,
visualPlan(regions, [
{ type: "required", regions: ["diff", "following"] },
{ type: "unique", regions: ["diff", "following"] },
{ type: "stable", regions: ["diff", "following"] },
{ type: "opacity", regions: "all" },
{ type: "continuity", regions: "all" },
{ type: "motion", regions: "all", maxPositionReversals: 1, maxReversals: 2 },
{ type: "label-stability", regions: "all" },
{ type: "flow", regions: ["diff", "following"] },
]),
)
})
function lines(count: number) { function lines(count: number) {
return Array.from({ length: count }, (_, index) => `line ${index + 1}`).join("\n") return Array.from({ length: count }, (_, index) => `line ${index + 1}`).join("\n")
} }
@@ -14,6 +14,7 @@ import {
partDelta, partDelta,
partUpdated, partUpdated,
reasoningPart, reasoningPart,
renderedPartID,
setupTimeline, setupTimeline,
shell, shell,
status, status,
@@ -50,7 +51,7 @@ test.describe("timeline visual lifecycle stability", () => {
prt_shell_long: shellRegion(ids[2]), prt_shell_long: shellRegion(ids[2]),
following: shellRegion(followingID), following: shellRegion(followingID),
}) })
await waitForVisualSettle(page, [`[data-timeline-part-id="${followingID}"]`]) await waitForVisualSettle(page, [`[data-timeline-part-id="${renderedPartID(followingID)}"]`])
await startVisualProbe(page, regions) await startVisualProbe(page, regions)
await timeline.sendAll([ await timeline.sendAll([
{ event: partUpdated(shell(ids[0]!, "completed", "")), delay: 180 }, { event: partUpdated(shell(ids[0]!, "completed", "")), delay: 180 },
@@ -60,7 +61,7 @@ test.describe("timeline visual lifecycle stability", () => {
{ event: partUpdated(shell(ids[1]!, "completed", lines(2))), delay: 260 }, { event: partUpdated(shell(ids[1]!, "completed", lines(2))), delay: 260 },
{ event: partUpdated(shell(ids[2]!, "running", lines(50))), delay: 100 }, { event: partUpdated(shell(ids[2]!, "running", lines(50))), delay: 100 },
{ event: partUpdated(shell(ids[2]!, "completed", lines(50))), delay: 450 }, { event: partUpdated(shell(ids[2]!, "completed", lines(50))), delay: 450 },
{ event: messageUpdated(completedAssistantInfo(assistant.info)), delay: 100 }, { event: messageUpdated(completedAssistantInfo(assistant)), delay: 100 },
{ event: status("idle"), delay: 700 }, { event: status("idle"), delay: 700 },
]) ])
const trace = await stopVisualProbe<keyof typeof regions>(page) const trace = await stopVisualProbe<keyof typeof regions>(page)
@@ -84,9 +85,11 @@ test.describe("timeline visual lifecycle stability", () => {
{ perMarker: true }, { perMarker: true },
), ),
) )
await expect(page.locator(`[data-timeline-part-id="${ids[2]}"] [data-slot="bash-pre"]`)).toContainText("line 50") await expect(
page.locator(`[data-timeline-part-id="${renderedPartID(ids[2])}"] [data-slot="bash-pre"]`),
).toContainText("line 50")
const short = page.locator(`[data-timeline-part-id="${ids[1]}"]`) const short = page.locator(`[data-timeline-part-id="${renderedPartID(ids[1])}"]`)
await short.locator('[data-slot="collapsible-trigger"]').click() await short.locator('[data-slot="collapsible-trigger"]').click()
await expect(short.locator('[data-slot="collapsible-trigger"]')).toHaveAttribute("aria-expanded", "false") await expect(short.locator('[data-slot="collapsible-trigger"]')).toHaveAttribute("aria-expanded", "false")
await timeline.send(partUpdated(textPart("prt_late_sibling", "A later sibling rerender.")), 250) await timeline.send(partUpdated(textPart("prt_late_sibling", "A later sibling rerender.")), 250)
@@ -106,26 +109,31 @@ test.describe("timeline visual lifecycle stability", () => {
}) })
await timeline.send(status("busy"), 120) await timeline.send(status("busy"), 120)
await expect(page.locator('[data-timeline-row="Thinking"]')).toBeVisible() await expect(page.locator('[data-timeline-row="Thinking"]')).toBeVisible()
const initialReasoning = reasoningPart(reasoningID, "")
const initialText = textPart(textID, "Starting")
const regions = defineVisualRegions({ const regions = defineVisualRegions({
thinking: { selector: '[data-timeline-row="Thinking"]' }, thinking: { selector: '[data-timeline-row="Thinking"]' },
reasoning: { reasoning: {
selector: `[data-timeline-part-id="${reasoningID}"]`, selector: `[data-timeline-part-id="${renderedPartID(reasoningID)}"]`,
closest: '[data-timeline-row="AssistantPart"]',
},
text: {
selector: `[data-timeline-part-id="${renderedPartID(textID)}"]`,
closest: '[data-timeline-row="AssistantPart"]', closest: '[data-timeline-row="AssistantPart"]',
}, },
text: { selector: `[data-timeline-part-id="${textID}"]`, closest: '[data-timeline-row="AssistantPart"]' },
}) })
await startVisualProbe(page, regions) await startVisualProbe(page, regions)
await timeline.send(partUpdated(reasoningPart(reasoningID, "")), 100) await timeline.send(partUpdated(initialReasoning), 100)
await expect(page.locator(`[data-timeline-part-id="${reasoningID}"]`)).toHaveCount(0) await expect(page.locator(`[data-timeline-part-id="${renderedPartID(reasoningID)}"]`)).toHaveCount(0)
await timeline.send(partUpdated(reasoningPart(reasoningID, "## Planning\n\nChecking the visible timeline.")), 160) await timeline.send(partUpdated(reasoningPart(reasoningID, "## Planning\n\nChecking the visible timeline.")), 160)
await timeline.waitForPart(reasoningID) await timeline.waitForPart(reasoningID)
await expect(page.locator('[data-timeline-row="Thinking"]')).toHaveCount(0) await expect(page.locator('[data-timeline-row="Thinking"]')).toHaveCount(0)
await timeline.send(partUpdated(textPart(textID, "Starting")), 100) await timeline.send(partUpdated(initialText), 100)
await timeline.send(partDelta(textID, " **stable"), 90) await timeline.send(partDelta(textID, " **stable"), 90)
await timeline.send(partDelta(textID, " output** with `code` and [a link"), 130) await timeline.send(partDelta(textID, " output** with `code` and [a link"), 130)
await timeline.send(partDelta(textID, "](https://example.com)."), 220) await timeline.send(partDelta(textID, "](https://example.com)."), 220)
await timeline.send(messageUpdated(completedAssistantInfo(assistant.info)), 120) await timeline.send(messageUpdated(completedAssistantInfo(assistant)), 120)
await timeline.send(status("idle"), 500) await timeline.send(status("idle"), 500)
const trace = await stopVisualProbe<keyof typeof regions>(page) const trace = await stopVisualProbe<keyof typeof regions>(page)
await reportVisualStability( await reportVisualStability(
@@ -144,7 +152,7 @@ test.describe("timeline visual lifecycle stability", () => {
{ type: "flow", regions: ["reasoning", "text"] }, { type: "flow", regions: ["reasoning", "text"] },
]), ]),
) )
await expect(page.locator(`[data-timeline-part-id="${textID}"]`)).toContainText("stable output") await expect(page.locator(`[data-timeline-part-id="${renderedPartID(textID)}"]`)).toContainText("stable output")
}) })
}) })
@@ -153,5 +161,5 @@ function lines(count: number) {
} }
function shellRegion(id: string) { function shellRegion(id: string) {
return { selector: `[data-timeline-part-id="${id}"]`, closest: '[data-timeline-row="AssistantPart"]' } return { selector: `[data-timeline-part-id="${renderedPartID(id)}"]`, closest: '[data-timeline-row="AssistantPart"]' }
} }
@@ -6,14 +6,14 @@ import {
stopVisualProbe, stopVisualProbe,
visualPlan, visualPlan,
} from "../../utils/visual-stability" } from "../../utils/visual-stability"
import { assistantMessage, setupTimeline, textPart, userMessage } from "./fixture" import { assistantMessage, renderedPartID, setupTimeline, textPart, userMessage } from "./fixture"
test("detects blanking caused by ancestor opacity", async ({ page }) => { test("detects blanking caused by ancestor opacity", async ({ page }) => {
const partID = "prt_oracle_ancestor_opacity" const partID = "prt_oracle_ancestor_opacity"
await setupTimeline(page, { messages: [userMessage(), assistantMessage([textPart(partID, "Visible content")])] }) await setupTimeline(page, { messages: [userMessage(), assistantMessage([textPart(partID, "Visible content")])] })
const row = page.locator(`[data-timeline-part-id="${partID}"]`).first() const row = page.locator(`[data-timeline-part-id="${renderedPartID(partID)}"]`).first()
const regions = defineVisualRegions({ const regions = defineVisualRegions({
content: { selector: `[data-timeline-part-id="${partID}"]` }, content: { selector: `[data-timeline-part-id="${renderedPartID(partID)}"]` },
}) })
await startVisualProbe(page, regions) await startVisualProbe(page, regions)
await row.evaluate((element) => { await row.evaluate((element) => {
@@ -41,13 +41,13 @@ test("detects blanking caused by ancestor opacity", async ({ page }) => {
test("detects root opacity when probing descendant opacity", async ({ page }) => { test("detects root opacity when probing descendant opacity", async ({ page }) => {
const partID = "prt_oracle_descendant_opacity" const partID = "prt_oracle_descendant_opacity"
await setupTimeline(page, { messages: [userMessage(), assistantMessage([textPart(partID, "Visible content")])] }) await setupTimeline(page, { messages: [userMessage(), assistantMessage([textPart(partID, "Visible content")])] })
const row = page.locator(`[data-timeline-part-id="${partID}"]`).first() const row = page.locator(`[data-timeline-part-id="${renderedPartID(partID)}"]`).first()
await row.evaluate((element) => { await row.evaluate((element) => {
element.innerHTML = '<span data-probe-opacity="true">Visible content</span>' element.innerHTML = '<span data-probe-opacity="true">Visible content</span>'
}) })
const regions = defineVisualRegions({ const regions = defineVisualRegions({
content: { content: {
selector: `[data-timeline-part-id="${partID}"]`, selector: `[data-timeline-part-id="${renderedPartID(partID)}"]`,
opacitySelectors: ['[data-probe-opacity="true"]'], opacitySelectors: ['[data-probe-opacity="true"]'],
}, },
}) })
@@ -9,6 +9,7 @@ import {
import { import {
assistantMessage, assistantMessage,
partUpdated, partUpdated,
renderedPartID,
setupTimeline, setupTimeline,
shell, shell,
textPart, textPart,
@@ -34,8 +35,14 @@ test("does not reverse visible rows when the user wheels during shell remeasurem
}) })
const scroller = page.locator(".scroll-view__viewport", { has: page.locator("[data-timeline-row]") }) const scroller = page.locator(".scroll-view__viewport", { has: page.locator("[data-timeline-row]") })
const regions = defineVisualRegions({ const regions = defineVisualRegions({
shell: { selector: `[data-timeline-part-id="${shellID}"]`, closest: '[data-timeline-row="AssistantPart"]' }, shell: {
following: { selector: `[data-timeline-part-id="${followingID}"]`, closest: '[data-timeline-row="AssistantPart"]' }, selector: `[data-timeline-part-id="${renderedPartID(shellID)}"]`,
closest: '[data-timeline-row="AssistantPart"]',
},
following: {
selector: `[data-timeline-part-id="${renderedPartID(followingID)}"]`,
closest: '[data-timeline-row="AssistantPart"]',
},
}) })
await startVisualProbe(page, regions) await startVisualProbe(page, regions)
await timeline.send(partUpdated(shell(shellID, "running", lines(30))), 80) await timeline.send(partUpdated(shell(shellID, "running", lines(30))), 80)
@@ -143,8 +150,8 @@ test("tracks keyboard scrolling from a focused timeline descendant", async ({ pa
reducedMotion: true, reducedMotion: true,
}) })
const scroller = page.locator(".scroll-view__viewport", { has: page.locator("[data-timeline-row]") }) const scroller = page.locator(".scroll-view__viewport", { has: page.locator("[data-timeline-row]") })
const row = page.locator(`[data-timeline-part-id="${shellID}"]`).first() const row = page.locator(`[data-timeline-part-id="${renderedPartID(shellID)}"]`).first()
const trigger = page.locator(`[data-timeline-part-id="${shellID}"] [data-slot="collapsible-trigger"]`) const trigger = page.locator(`[data-timeline-part-id="${renderedPartID(shellID)}"] [data-slot="collapsible-trigger"]`)
await row.evaluate((element) => element.setAttribute("tabindex", "0")) await row.evaluate((element) => element.setAttribute("tabindex", "0"))
await row.focus() await row.focus()
for (let index = 0; index < 3; index++) { for (let index = 0; index < 3; index++) {
@@ -182,7 +189,7 @@ test("does not claim keyboard scrolling owned by a nested scrollable", async ({
seedHistory: true, seedHistory: true,
}) })
const scroller = page.locator(".scroll-view__viewport", { has: page.locator("[data-timeline-row]") }) const scroller = page.locator(".scroll-view__viewport", { has: page.locator("[data-timeline-row]") })
const nested = page.locator(`[data-timeline-part-id="${shellID}"] [data-scrollable]`) const nested = page.locator(`[data-timeline-part-id="${renderedPartID(shellID)}"] [data-scrollable]`)
await nested.evaluate((element) => (element.scrollTop = element.scrollHeight)) await nested.evaluate((element) => (element.scrollTop = element.scrollHeight))
await nested.focus() await nested.focus()
await page.waitForFunction(() => { await page.waitForFunction(() => {
@@ -209,7 +216,7 @@ test("does not claim keyboard scrolling owned by a nested scrollable", async ({
await nested.press("PageUp") await nested.press("PageUp")
await expect.poll(() => scroller.evaluate((element) => element.scrollTop)).toBeLessThan(boundaryBefore) await expect.poll(() => scroller.evaluate((element) => element.scrollTop)).toBeLessThan(boundaryBefore)
const nonOverflowing = page.locator(`[data-timeline-part-id="${shellID}"]`).first() const nonOverflowing = page.locator(`[data-timeline-part-id="${renderedPartID(shellID)}"]`).first()
await nonOverflowing.evaluate((element) => { await nonOverflowing.evaluate((element) => {
element.setAttribute("data-scrollable", "") element.setAttribute("data-scrollable", "")
element.setAttribute("tabindex", "0") element.setAttribute("tabindex", "0")
@@ -238,12 +245,18 @@ test("jump to latest lands on stable final rows after offscreen growth", async (
) )
await timeline.send(partUpdated(shell(shellID, "running", lines(50))), 300) await timeline.send(partUpdated(shell(shellID, "running", lines(50))), 300)
const regions = defineVisualRegions({ const regions = defineVisualRegions({
shell: { selector: `[data-timeline-part-id="${shellID}"]`, closest: '[data-timeline-row="AssistantPart"]' }, shell: {
following: { selector: `[data-timeline-part-id="${followingID}"]`, closest: '[data-timeline-row="AssistantPart"]' }, selector: `[data-timeline-part-id="${renderedPartID(shellID)}"]`,
closest: '[data-timeline-row="AssistantPart"]',
},
following: {
selector: `[data-timeline-part-id="${renderedPartID(followingID)}"]`,
closest: '[data-timeline-row="AssistantPart"]',
},
}) })
await startVisualProbe(page, regions) await startVisualProbe(page, regions)
await page.getByRole("button", { name: /Jump to latest/i }).click() await page.getByRole("button", { name: /Jump to latest/i }).click()
await expect(page.locator(`[data-timeline-part-id="${followingID}"]`)).toBeVisible() await expect(page.locator(`[data-timeline-part-id="${renderedPartID(followingID)}"]`)).toBeVisible()
await page.waitForTimeout(600) await page.waitForTimeout(600)
const trace = await stopVisualProbe<keyof typeof regions>(page) const trace = await stopVisualProbe<keyof typeof regions>(page)
await reportVisualStability( await reportVisualStability(
@@ -277,8 +290,14 @@ test("handles a single row taller than the viewport", async ({ page }, testInfo)
seedHistory: true, seedHistory: true,
}) })
const regions = defineVisualRegions({ const regions = defineVisualRegions({
shell: { selector: `[data-timeline-part-id="${shellID}"]`, closest: '[data-timeline-row="AssistantPart"]' }, shell: {
following: { selector: `[data-timeline-part-id="${followingID}"]`, closest: '[data-timeline-row="AssistantPart"]' }, selector: `[data-timeline-part-id="${renderedPartID(shellID)}"]`,
closest: '[data-timeline-row="AssistantPart"]',
},
following: {
selector: `[data-timeline-part-id="${renderedPartID(followingID)}"]`,
closest: '[data-timeline-row="AssistantPart"]',
},
}) })
await startVisualProbe(page, regions) await startVisualProbe(page, regions)
await timeline.send(partUpdated(shell(shellID, "completed", lines(100))), 700) await timeline.send(partUpdated(shell(shellID, "completed", lines(100))), 700)
@@ -9,6 +9,7 @@ import {
import { import {
assistantMessage, assistantMessage,
partUpdated, partUpdated,
renderedPartID,
setupTimeline, setupTimeline,
shell, shell,
textPart, textPart,
@@ -68,13 +69,16 @@ for (const profile of profiles) {
const scroller = page.locator(".scroll-view__viewport", { has: page.locator("[data-timeline-row]") }) const scroller = page.locator(".scroll-view__viewport", { has: page.locator("[data-timeline-row]") })
await scroller.evaluate((element) => (element.scrollTop = element.scrollHeight)) await scroller.evaluate((element) => (element.scrollTop = element.scrollHeight))
await waitForVisualSettle(page, [ await waitForVisualSettle(page, [
`[data-timeline-part-id="${shellID}"]`, `[data-timeline-part-id="${renderedPartID(shellID)}"]`,
`[data-timeline-part-id="${followingID}"]`, `[data-timeline-part-id="${renderedPartID(followingID)}"]`,
]) ])
const regions = defineVisualRegions({ const regions = defineVisualRegions({
shell: { selector: `[data-timeline-part-id="${shellID}"]`, closest: '[data-timeline-row="AssistantPart"]' }, shell: {
selector: `[data-timeline-part-id="${renderedPartID(shellID)}"]`,
closest: '[data-timeline-row="AssistantPart"]',
},
following: { following: {
selector: `[data-timeline-part-id="${followingID}"]`, selector: `[data-timeline-part-id="${renderedPartID(followingID)}"]`,
closest: '[data-timeline-row="AssistantPart"]', closest: '[data-timeline-row="AssistantPart"]',
}, },
}) })
@@ -120,10 +124,19 @@ test("keeps following row stable when a collapsed shell receives 50 lines", asyn
cpuRate: 4, cpuRate: 4,
seedHistory: true, seedHistory: true,
}) })
await waitForVisualSettle(page, [`[data-timeline-part-id="${shellID}"]`, `[data-timeline-part-id="${followingID}"]`]) await waitForVisualSettle(page, [
`[data-timeline-part-id="${renderedPartID(shellID)}"]`,
`[data-timeline-part-id="${renderedPartID(followingID)}"]`,
])
const regions = defineVisualRegions({ const regions = defineVisualRegions({
shell: { selector: `[data-timeline-part-id="${shellID}"]`, closest: '[data-timeline-row="AssistantPart"]' }, shell: {
following: { selector: `[data-timeline-part-id="${followingID}"]`, closest: '[data-timeline-row="AssistantPart"]' }, selector: `[data-timeline-part-id="${renderedPartID(shellID)}"]`,
closest: '[data-timeline-row="AssistantPart"]',
},
following: {
selector: `[data-timeline-part-id="${renderedPartID(followingID)}"]`,
closest: '[data-timeline-row="AssistantPart"]',
},
}) })
await startVisualProbe(page, regions) await startVisualProbe(page, regions)
await timeline.send(partUpdated(shell(shellID, "running", lines(50))), 240) await timeline.send(partUpdated(shell(shellID, "running", lines(50))), 240)
@@ -164,10 +177,19 @@ test("keeps rows stable when a running shell becomes an error", async ({ page },
cpuRate: 4, cpuRate: 4,
seedHistory: true, seedHistory: true,
}) })
await waitForVisualSettle(page, [`[data-timeline-part-id="${shellID}"]`, `[data-timeline-part-id="${followingID}"]`]) await waitForVisualSettle(page, [
`[data-timeline-part-id="${renderedPartID(shellID)}"]`,
`[data-timeline-part-id="${renderedPartID(followingID)}"]`,
])
const regions = defineVisualRegions({ const regions = defineVisualRegions({
shell: { selector: `[data-timeline-part-id="${shellID}"]`, closest: '[data-timeline-row="AssistantPart"]' }, shell: {
following: { selector: `[data-timeline-part-id="${followingID}"]`, closest: '[data-timeline-row="AssistantPart"]' }, selector: `[data-timeline-part-id="${renderedPartID(shellID)}"]`,
closest: '[data-timeline-row="AssistantPart"]',
},
following: {
selector: `[data-timeline-part-id="${renderedPartID(followingID)}"]`,
closest: '[data-timeline-row="AssistantPart"]',
},
}) })
await startVisualProbe(page, regions) await startVisualProbe(page, regions)
await timeline.send( await timeline.send(
@@ -215,16 +237,20 @@ test("keeps rows stable when later text arrives before shell output", async ({ p
cpuRate: 4, cpuRate: 4,
seedHistory: true, seedHistory: true,
}) })
await waitForVisualSettle(page, [`[data-timeline-part-id="${shellID}"]`]) const following = textPart(followingID, "Later assistant content arrived before shell output.")
await waitForVisualSettle(page, [`[data-timeline-part-id="${renderedPartID(shellID)}"]`])
const regions = defineVisualRegions({ const regions = defineVisualRegions({
shell: { selector: `[data-timeline-part-id="${shellID}"]`, closest: '[data-timeline-row="AssistantPart"]' }, shell: {
selector: `[data-timeline-part-id="${renderedPartID(shellID)}"]`,
closest: '[data-timeline-row="AssistantPart"]',
},
following: { following: {
selector: `[data-timeline-part-id="${followingID}"]`, selector: `[data-timeline-part-id="${renderedPartID(followingID)}"]`,
closest: '[data-timeline-row="AssistantPart"]', closest: '[data-timeline-row="AssistantPart"]',
}, },
}) })
await startVisualProbe(page, regions) await startVisualProbe(page, regions)
await timeline.send(partUpdated(textPart(followingID, "Later assistant content arrived before shell output.")), 240) await timeline.send(partUpdated(following), 240)
await timeline.send(partUpdated(shell(shellID, "running", lines(20))), 300) await timeline.send(partUpdated(shell(shellID, "running", lines(20))), 300)
await timeline.send(partUpdated(shell(shellID, "completed", lines(20))), 600) await timeline.send(partUpdated(shell(shellID, "completed", lines(20))), 600)
const trace = await stopVisualProbe<keyof typeof regions>(page) const trace = await stopVisualProbe<keyof typeof regions>(page)
@@ -11,8 +11,8 @@ import {
partUpdated, partUpdated,
session, session,
sessionID, sessionID,
renderedPartID,
setupTimeline, setupTimeline,
textPart,
toolPart, toolPart,
userMessage, userMessage,
} from "./fixture" } from "./fixture"
@@ -27,7 +27,7 @@ test("adds a task child-session link without replacing the task row", async ({ p
cpuRate: 4, cpuRate: 4,
}) })
const regions = defineVisualRegions({ const regions = defineVisualRegions({
task: { selector: `[data-timeline-part-id="${taskID}"] [data-slot="collapsible-trigger"]` }, task: { selector: `[data-timeline-part-id="${renderedPartID(taskID)}"] [data-slot="collapsible-trigger"]` },
}) })
await startVisualProbe(page, regions) await startVisualProbe(page, regions)
await timeline.send( await timeline.send(
@@ -53,54 +53,3 @@ test("adds a task child-session link without replacing the task row", async ({ p
page.locator(`a[href$="/session/${childID}"]`, { has: page.locator('[data-component="task-tool-card"]') }), page.locator(`a[href$="/session/${childID}"]`, { has: page.locator('[data-component="task-tool-card"]') }),
).toBeVisible() ).toBeVisible()
}) })
test("changes generic tool arguments without replacing the row", async ({ page }, testInfo) => {
const toolID = "prt_generic_mutation"
const followingID = "prt_generic_mutation_following"
const timeline = await setupTimeline(page, {
messages: [
userMessage(),
assistantMessage(
[
toolPart(toolID, "mcp_probe", "running", { target: "one", count: 1 }),
textPart(followingID, "Following generic tool"),
],
{ completed: false },
),
],
cpuRate: 4,
})
const regions = defineVisualRegions({
tool: { selector: `[data-timeline-part-id="${toolID}"]`, closest: '[data-timeline-row="AssistantPart"]' },
following: { selector: `[data-timeline-part-id="${followingID}"]`, closest: '[data-timeline-row="AssistantPart"]' },
})
await startVisualProbe(page, regions)
await timeline.send(
partUpdated(toolPart(toolID, "mcp_probe", "running", { target: "two", count: 2, mode: "deep" })),
200,
)
await timeline.send(
partUpdated(toolPart(toolID, "mcp_probe", "completed", { target: "two", count: 2, mode: "deep" })),
400,
)
const trace = await stopVisualProbe<keyof typeof regions>(page)
await reportVisualStability(
testInfo,
"generic-mutation",
trace,
visualPlan(
regions,
[
{ type: "required", regions: ["tool", "following"] },
{ type: "unique", regions: ["tool", "following"] },
{ type: "stable", regions: ["tool", "following"] },
{ type: "opacity", regions: "all" },
{ type: "continuity", regions: "all" },
{ type: "motion", regions: "all", maxPositionReversals: 0 },
{ type: "label-stability", regions: "all" },
{ type: "flow", regions: ["tool", "following"] },
],
{ perMarker: true },
),
)
})
@@ -12,6 +12,7 @@ import {
partUpdated, partUpdated,
session, session,
sessionID, sessionID,
renderedPartID,
setupTimeline, setupTimeline,
status, status,
textPart, textPart,
@@ -48,8 +49,8 @@ test.describe("timeline tool state stability", () => {
}) })
await timeline.send(status("busy"), 120) await timeline.send(status("busy"), 120)
for (const id of ids) await timeline.waitForPart(`prt_state_${id}`) for (const id of ids) await timeline.waitForPart(`prt_state_${id}`)
await expect(page.locator(`[data-timeline-part-id="${questionID}"]`)).toHaveCount(0) await expect(page.locator(`[data-timeline-part-id="${renderedPartID(questionID)}"]`)).toHaveCount(0)
await expect(page.locator(`[data-timeline-part-id="${todoID}"]`)).toHaveCount(0) await expect(page.locator(`[data-timeline-part-id="${renderedPartID(todoID)}"]`)).toHaveCount(0)
const regionIDs = [ const regionIDs = [
"prt_state_webfetch", "prt_state_webfetch",
@@ -104,8 +105,10 @@ test.describe("timeline tool state stability", () => {
{ type: "label-stability", regions: "all" }, { type: "label-stability", regions: "all" },
]), ]),
) )
await expect(page.locator(`[data-timeline-part-id="${questionID}"]`)).toContainText("Keep it stable") await expect(page.locator(`[data-timeline-part-id="${renderedPartID(questionID)}"]`)).toContainText(
await expect(page.locator(`[data-timeline-part-id="${todoID}"]`)).toHaveCount(0) "Keep it stable",
)
await expect(page.locator(`[data-timeline-part-id="${renderedPartID(todoID)}"]`)).toHaveCount(0)
await expect( await expect(
page.locator(`a[href$="/session/${childID}"]`, { has: page.locator('[data-component="task-tool-card"]') }), page.locator(`a[href$="/session/${childID}"]`, { has: page.locator('[data-component="task-tool-card"]') }),
).toBeVisible() ).toBeVisible()
@@ -145,7 +148,7 @@ test.describe("timeline tool state stability", () => {
}, },
context: { selector: groupSelector, closest: '[data-timeline-row="AssistantPart"]' }, context: { selector: groupSelector, closest: '[data-timeline-row="AssistantPart"]' },
following: { following: {
selector: '[data-timeline-part-id="prt_ctx_following"]', selector: `[data-timeline-part-id="${renderedPartID("prt_ctx_following")}"]`,
closest: '[data-timeline-row="AssistantPart"]', closest: '[data-timeline-row="AssistantPart"]',
}, },
}) })
@@ -194,5 +197,5 @@ function questionInput() {
} }
function toolRegion(id: string) { function toolRegion(id: string) {
return { selector: `[data-timeline-part-id="${id}"]`, closest: '[data-timeline-row="AssistantPart"]' } return { selector: `[data-timeline-part-id="${renderedPartID(id)}"]`, closest: '[data-timeline-row="AssistantPart"]' }
} }
@@ -7,13 +7,13 @@ import {
visualPlan, visualPlan,
} from "../../utils/visual-stability" } from "../../utils/visual-stability"
import { import {
assistantID,
assistantMessage, assistantMessage,
completedAssistantInfo, completedAssistantInfo,
event, event,
messageUpdated, messageUpdated,
partDelta, partDelta,
partUpdated, partUpdated,
renderedPartID,
setupTimeline, setupTimeline,
shell, shell,
status, status,
@@ -22,35 +22,6 @@ import {
userMessage, userMessage,
} from "./fixture" } from "./fixture"
test("keeps unchanged siblings stable while a middle part is inserted and removed", async ({ page }, testInfo) => {
const firstID = "prt_mutation_01_first"
const middleID = "prt_mutation_02_middle"
const lastID = "prt_mutation_03_last"
const timeline = await setupTimeline(page, {
messages: [
userMessage(),
assistantMessage([textPart(firstID, "First stable row"), textPart(lastID, "Last stable row")], {
completed: false,
}),
],
cpuRate: 4,
})
const regions = defineVisualRegions({
first: { selector: `[data-timeline-part-id="${firstID}"]`, closest: '[data-timeline-row="AssistantPart"]' },
last: { selector: `[data-timeline-part-id="${lastID}"]`, closest: '[data-timeline-row="AssistantPart"]' },
})
await startVisualProbe(page, regions)
await timeline.send(partUpdated(textPart(middleID, "Inserted middle row. ".repeat(12))), 350)
await expect(page.locator(`[data-timeline-part-id="${middleID}"]`)).toBeVisible()
await timeline.send(
event("message.part.removed", { sessionID: "ses_timeline_stability", messageID: assistantID, partID: middleID }),
500,
)
await expect(page.locator(`[data-timeline-part-id="${middleID}"]`)).toHaveCount(0)
const trace = await stopVisualProbe<keyof typeof regions>(page)
await reportVisualStability(testInfo, "middle-insert-remove", trace, stablePairPlan(regions, 1))
})
test("streams text through growth, canonical replacement, and completion", async ({ page }, testInfo) => { test("streams text through growth, canonical replacement, and completion", async ({ page }, testInfo) => {
const textID = "prt_text_reconcile" const textID = "prt_text_reconcile"
const followingID = "prt_text_reconcile_following" const followingID = "prt_text_reconcile_following"
@@ -59,14 +30,20 @@ test("streams text through growth, canonical replacement, and completion", async
}) })
const timeline = await setupTimeline(page, { messages: [userMessage(), assistant], cpuRate: 4 }) const timeline = await setupTimeline(page, { messages: [userMessage(), assistant], cpuRate: 4 })
const regions = defineVisualRegions({ const regions = defineVisualRegions({
text: { selector: `[data-timeline-part-id="${textID}"]`, closest: '[data-timeline-row="AssistantPart"]' }, text: {
following: { selector: `[data-timeline-part-id="${followingID}"]`, closest: '[data-timeline-row="AssistantPart"]' }, selector: `[data-timeline-part-id="${renderedPartID(textID)}"]`,
closest: '[data-timeline-row="AssistantPart"]',
},
following: {
selector: `[data-timeline-part-id="${renderedPartID(followingID)}"]`,
closest: '[data-timeline-row="AssistantPart"]',
},
}) })
await startVisualProbe(page, regions) await startVisualProbe(page, regions)
await timeline.send(partDelta(textID, " streamed content"), 100) await timeline.send(partDelta(textID, " streamed content"), 100)
await timeline.send(partDelta(textID, "\n\n- item one\n- item two\n- item three"), 180) await timeline.send(partDelta(textID, "\n\n- item one\n- item two\n- item three"), 180)
await timeline.send(partUpdated(textPart(textID, "Canonical replacement with a shorter final paragraph.")), 200) await timeline.send(partUpdated(textPart(textID, "Canonical replacement with a shorter final paragraph.")), 200)
await timeline.send(messageUpdated(completedAssistantInfo(assistant.info)), 500) await timeline.send(messageUpdated(completedAssistantInfo(assistant)), 500)
const trace = await stopVisualProbe<keyof typeof regions>(page) const trace = await stopVisualProbe<keyof typeof regions>(page)
await reportVisualStability( await reportVisualStability(
testInfo, testInfo,
@@ -105,17 +82,23 @@ test("inserts a completed question between stable rows", async ({ page }, testIn
], ],
cpuRate: 4, cpuRate: 4,
}) })
await expect(page.locator(`[data-timeline-part-id="${questionID}"]`)).toHaveCount(0) await expect(page.locator(`[data-timeline-part-id="${renderedPartID(questionID)}"]`)).toHaveCount(0)
const regions = defineVisualRegions({ const regions = defineVisualRegions({
first: { selector: `[data-timeline-part-id="${firstID}"]`, closest: '[data-timeline-row="AssistantPart"]' }, first: {
last: { selector: `[data-timeline-part-id="${lastID}"]`, closest: '[data-timeline-row="AssistantPart"]' }, selector: `[data-timeline-part-id="${renderedPartID(firstID)}"]`,
closest: '[data-timeline-row="AssistantPart"]',
},
last: {
selector: `[data-timeline-part-id="${renderedPartID(lastID)}"]`,
closest: '[data-timeline-row="AssistantPart"]',
},
}) })
await startVisualProbe(page, regions) await startVisualProbe(page, regions)
await timeline.send( await timeline.send(
partUpdated(toolPart(questionID, "question", "completed", input, { metadata: { answers: [["Yes"]] } })), partUpdated(toolPart(questionID, "question", "completed", input, { metadata: { answers: [["Yes"]] } })),
600, 600,
) )
await expect(page.locator(`[data-timeline-part-id="${questionID}"]`)).toBeVisible() await expect(page.locator(`[data-timeline-part-id="${renderedPartID(questionID)}"]`)).toBeVisible()
const trace = await stopVisualProbe<keyof typeof regions>(page) const trace = await stopVisualProbe<keyof typeof regions>(page)
await reportVisualStability(testInfo, "question-insert", trace, stablePairPlan(regions, 0)) await reportVisualStability(testInfo, "question-insert", trace, stablePairPlan(regions, 0))
}) })
@@ -132,8 +115,8 @@ test("replaces thinking with an assistant error without a blank turn", async ({
await startVisualProbe(page, regions) await startVisualProbe(page, regions)
await timeline.send( await timeline.send(
messageUpdated({ messageUpdated({
...assistant.info, ...assistant,
error: { name: "APIError", data: { message: "Provider failed visibly", isRetryable: false } }, error: { type: "APIError", message: "Provider failed visibly" },
}), }),
500, 500,
) )
@@ -203,59 +186,6 @@ test("updates retry attempts and long provider messages without remounting the r
) )
}) })
test("reducer-hardening: removes a historical turn one message at a time without moving a visible lower anchor twice", async ({
page,
}, testInfo) => {
const removeUserID = "msg_0500_remove_user"
const removeAssistantID = "msg_0501_remove_assistant"
const anchorUserID = "msg_2000_anchor_user"
const timeline = await setupTimeline(page, {
messages: [
userMessage(undefined, { id: removeUserID, created: 1690000000000 }),
assistantMessage([textPart("prt_remove_text", "Removed historical content. ".repeat(15))], {
id: removeAssistantID,
parentID: removeUserID,
created: 1690000001000,
}),
userMessage(undefined, { id: anchorUserID, created: 1700000000000 }),
assistantMessage([textPart("prt_anchor_text", "Visible anchor response")], {
id: "msg_2001_anchor_assistant",
parentID: anchorUserID,
created: 1700000001000,
}),
],
cpuRate: 4,
})
const regions = defineVisualRegions({
anchor: { selector: `[data-timeline-row="UserMessage"][data-message-id="${anchorUserID}"]` },
})
await startVisualProbe(page, regions)
await timeline.send(
event("message.removed", { sessionID: "ses_timeline_stability", messageID: removeAssistantID }),
200,
)
await timeline.send(event("message.removed", { sessionID: "ses_timeline_stability", messageID: removeUserID }), 500)
const trace = await stopVisualProbe<keyof typeof regions>(page)
await reportVisualStability(
testInfo,
"historical-turn-remove",
trace,
visualPlan(
regions,
[
{ type: "required", regions: ["anchor"] },
{ type: "unique", regions: ["anchor"] },
{ type: "stable", regions: ["anchor"] },
{ type: "opacity", regions: "all" },
{ type: "continuity", regions: "all" },
{ type: "motion", regions: "all", maxPositionReversals: 0 },
{ type: "label-stability", regions: "all" },
],
{ perMarker: true },
),
)
})
function stablePairPlan( function stablePairPlan(
regions: Record<"first" | "last", { selector: string; closest?: string }>, regions: Record<"first" | "last", { selector: string; closest?: string }>,
maxPositionReversals: number, maxPositionReversals: number,
@@ -48,7 +48,6 @@ benchmark.describe("performance: review pane scaling", () => {
await setupTimelineBenchmark(page, { await setupTimelineBenchmark(page, {
historyTurns: 0, historyTurns: 0,
eventBatch: 1, eventBatch: 1,
newLayoutDesigns: true,
}) })
await page.route("**/vcs/diff**", (route) => await page.route("**/vcs/diff**", (route) =>
route.fulfill({ route.fulfill({
@@ -1,3 +1,4 @@
import type { SessionMessageAssistant, SessionMessageInfo, SessionMessageUser } from "@opencode-ai/client/promise"
import type { Page } from "@playwright/test" import type { Page } from "@playwright/test"
import { expectSessionTitle } from "../../utils/waits" import { expectSessionTitle } from "../../utils/waits"
import { mockOpenCodeServer } from "../../utils/mock-server" import { mockOpenCodeServer } from "../../utils/mock-server"
@@ -11,42 +12,35 @@ type ParentHydrationBenchmarkMode = "natural" | "candidate"
const mode = process.env.SESSION_PARENT_HYDRATION_BENCHMARK_MODE ?? "natural" const mode = process.env.SESSION_PARENT_HYDRATION_BENCHMARK_MODE ?? "natural"
if (mode !== "natural" && mode !== "candidate") throw new Error(`Unknown parent hydration benchmark mode: ${mode}`) if (mode !== "natural" && mode !== "candidate") throw new Error(`Unknown parent hydration benchmark mode: ${mode}`)
const userID = "msg_parent_hydration_user" const userID = "msg_parent_hydration_user"
const userSeed = fixture.messages[fixture.targetID][0] as SessionMessageUser
const user = { const user = {
...fixture.messages[fixture.targetID][0]!, ...userSeed,
info: { ...fixture.messages[fixture.targetID][0]!.info, id: userID, time: { created: 1700001000000 } }, id: userID,
parts: fixture.messages[fixture.targetID][0]!.parts.map((part, index) => ({ time: { created: 1700001000000 },
...part, } satisfies SessionMessageInfo
id: `prt_parent_hydration_user_${index}`, const assistantSeed = fixture.messages[fixture.targetID][3] as SessionMessageAssistant
messageID: userID,
})),
}
const assistantSeed = fixture.messages[fixture.targetID][3]!
const assistants = Array.from({ length: 14 }, (_, index) => { const assistants = Array.from({ length: 14 }, (_, index) => {
const messageID = `msg_parent_hydration_${String(index).padStart(2, "0")}` const messageID = `msg_parent_hydration_${String(index).padStart(2, "0")}`
return { return {
...assistantSeed, ...assistantSeed,
info: { id: messageID,
...assistantSeed.info, time: { created: 1700001001000 + index * 1_000, completed: 1700001001500 + index * 1_000 },
id: messageID, content: assistantSeed.content.map((part, partIndex) =>
parentID: userID, part.type === "tool"
time: { created: 1700001001000 + index * 1_000, completed: 1700001001500 + index * 1_000 }, ? { ...part, id: `call_parent_hydration_${String(index).padStart(2, "0")}_${partIndex}` }
}, : part,
parts: assistantSeed.parts.map((part, partIndex) => ({ ),
...part, } satisfies SessionMessageInfo
id: `prt_parent_hydration_${String(index).padStart(2, "0")}_${partIndex}`,
messageID,
})),
}
}) })
const messages = [user, ...assistants] const messages = [user, ...assistants]
const target = fixture.sessions.find((session) => session.id === fixture.targetID)! const target = fixture.sessions.find((session) => session.id === fixture.targetID)!
const lastID = userID const lastID = userID
const lastAssistant = assistants.at(-1)! const lastAssistant = assistants.at(-1)!
const lastPart = lastAssistant.parts.at(-1)! const lastPart = lastAssistant.content.at(-1)!
const lastPartID = const lastPartID =
lastPart.type === "tool" lastPart.type === "tool"
? lastPart.id ? lastPart.id
: `${lastAssistant.info.id}:${lastPart.type}:${lastAssistant.parts.filter((part) => part.type === lastPart.type).length - 1}` : `${lastAssistant.id}:${lastPart.type}:${lastAssistant.content.filter((part) => part.type === lastPart.type).length - 1}`
benchmark("hydrates an orphaned latest turn after a cold session click", async ({ browser, report }, testInfo) => { benchmark("hydrates an orphaned latest turn after a cold session click", async ({ browser, report }, testInfo) => {
benchmark.setTimeout(180_000) benchmark.setTimeout(180_000)
@@ -107,30 +101,25 @@ async function trial(page: Page, mode: ParentHydrationBenchmarkMode) {
}, },
pageMessages: (sessionID, limit, before) => { pageMessages: (sessionID, limit, before) => {
const items = sessionID === fixture.targetID ? messages : fixture.messages[fixture.sourceID] const items = sessionID === fixture.targetID ? messages : fixture.messages[fixture.sourceID]
const end = before ? items.findIndex((message) => message.info.id === before) : items.length const end = before ? items.findIndex((message) => message.id === before) : items.length
const start = Math.max(0, end - limit) const start = Math.max(0, end - limit)
return { items: items.slice(start, end), cursor: start > 0 ? items[start]!.info.id : undefined } return { items: items.slice(start, end), cursor: start > 0 ? items[start]!.id : undefined }
}, },
}) })
await page.route(`**/session/${fixture.targetID}`, (route) => { await page.route(`**/api/session/${fixture.targetID}`, (route) =>
const current = new URL(route.request().url()).pathname.startsWith("/api/") route.fulfill({
return route.fulfill({
status: 200, status: 200,
contentType: "application/json", contentType: "application/json",
body: JSON.stringify( body: JSON.stringify({
current data: {
? { ...target,
data: { cost: 0,
...target, tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
cost: 0, location: { directory: target.directory },
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, },
location: { directory: target.directory }, }),
}, }),
} )
: target,
),
})
})
await installStressSessionTabs(page, { sessionIDs: [fixture.sourceID] }) await installStressSessionTabs(page, { sessionIDs: [fixture.sourceID] })
await page.goto(stressSessionHref(fixture.sourceID)) await page.goto(stressSessionHref(fixture.sourceID))
await expectSessionTitle(page, fixture.expected.sourceTitle) await expectSessionTitle(page, fixture.expected.sourceTitle)
@@ -148,8 +137,8 @@ async function trial(page: Page, mode: ParentHydrationBenchmarkMode) {
{ href, title: target.title }, { href, title: target.title },
) )
const metrics = await measureSessionSwitch(page, { const metrics = await measureSessionSwitch(page, {
destinationIDs: messages.map((message) => message.info.id), destinationIDs: messages.map((message) => message.id),
sourceIDs: fixture.messages[fixture.sourceID].map((message) => message.info.id), sourceIDs: fixture.messages[fixture.sourceID].map((message) => message.id),
lastID, lastID,
requiredPartID: lastPartID, requiredPartID: lastPartID,
requireBottomAnchor: false, requireBottomAnchor: false,
@@ -32,8 +32,8 @@ benchmark("samples cached session repaint after the click", async ({ page, repor
await installCachedRepaintProbe(page, { await installCachedRepaintProbe(page, {
targetHref: stressSessionHref(fixture.targetID), targetHref: stressSessionHref(fixture.targetID),
destination: fixture.messages[fixture.targetID].map((message) => message.info.id), destination: fixture.messages[fixture.targetID].map((message) => message.id),
source: fixture.messages[fixture.sourceID].map((message) => message.info.id), source: fixture.messages[fixture.sourceID].map((message) => message.id),
last: fixture.expected.targetMessageIDs.at(-1)!, last: fixture.expected.targetMessageIDs.at(-1)!,
windowMs: 1_000, windowMs: 1_000,
}) })
@@ -13,21 +13,8 @@ import { measureSessionSwitch, waitForStableTimeline } from "./session-tab-switc
type Result = Awaited<ReturnType<typeof measureSessionSwitch>> type Result = Awaited<ReturnType<typeof measureSessionSwitch>>
benchmark("benchmarks cold and hot session tab switching", async ({ browser, report }, testInfo) => {
benchmark.setTimeout(180_000)
const results = { cold: [] as Result[], hot: [] as Result[] }
for (const mode of ["cold", "hot"] as const) {
for (let run = 0; run < 5; run++) {
results[mode].push(
await withBenchmarkPage(browser, `session-tab-switch-${mode}-${run}`, (page) => trial(page, mode), testInfo),
)
}
}
report({ results, summary: summarize(results) })
})
benchmark( benchmark(
"benchmarks v2 session tab switching with and without the review pane", "benchmarks session tab switching with and without the review pane",
async ({ browser, report }, testInfo) => { async ({ browser, report }, testInfo) => {
benchmark.setTimeout(360_000) benchmark.setTimeout(360_000)
const runs = Number(process.env.SESSION_TAB_SWITCH_RUNS ?? 5) const runs = Number(process.env.SESSION_TAB_SWITCH_RUNS ?? 5)
@@ -41,8 +28,8 @@ benchmark(
results[reviewPane][mode].push( results[reviewPane][mode].push(
await withBenchmarkPage( await withBenchmarkPage(
browser, browser,
`session-tab-switch-v2-${reviewPane}-${mode}-${run}`, `session-tab-switch-${reviewPane}-${mode}-${run}`,
(page) => trial(page, mode, { newLayoutDesigns: true, reviewPane }), (page) => trial(page, mode, reviewPane),
testInfo, testInfo,
), ),
) )
@@ -53,14 +40,10 @@ benchmark(
}, },
) )
async function trial( async function trial(page: Page, mode: "cold" | "hot", reviewPane: "closed" | "open") {
page: Page, const reviewDiffs = createReviewDiffs()
mode: "cold" | "hot",
options?: { newLayoutDesigns?: boolean; reviewPane?: "closed" | "open" },
) {
const reviewDiffs = options?.newLayoutDesigns ? createReviewDiffs() : undefined
await mockStressTimeline(page, { vcsDiff: reviewDiffs }) await mockStressTimeline(page, { vcsDiff: reviewDiffs })
if (options?.newLayoutDesigns) await installTimelineSettings(page) await installTimelineSettings(page)
await installStressSessionTabs(page) await installStressSessionTabs(page)
if (mode === "hot") { if (mode === "hot") {
await page.goto(stressSessionHref(fixture.targetID)) await page.goto(stressSessionHref(fixture.targetID))
@@ -72,13 +55,13 @@ async function trial(
await expectSessionTitle(page, fixture.expected.sourceTitle) await expectSessionTitle(page, fixture.expected.sourceTitle)
} }
await waitForStableTimeline(page, fixture.expected.sourceMessageIDs.at(-1)!) await waitForStableTimeline(page, fixture.expected.sourceMessageIDs.at(-1)!)
if (options?.reviewPane === "open") { if (reviewPane === "open") {
await openReviewPane(page) await openReviewPane(page)
await waitForStableTimeline(page, fixture.expected.sourceMessageIDs.at(-1)!) await waitForStableTimeline(page, fixture.expected.sourceMessageIDs.at(-1)!)
} }
const destinationIDs = fixture.messages[fixture.targetID].map((message) => message.info.id) const destinationIDs = fixture.messages[fixture.targetID].map((message) => message.id)
const sourceIDs = fixture.messages[fixture.sourceID].map((message) => message.info.id) const sourceIDs = fixture.messages[fixture.sourceID].map((message) => message.id)
const lastID = fixture.expected.targetMessageIDs.at(-1)! const lastID = fixture.expected.targetMessageIDs.at(-1)!
const href = stressSessionHref(fixture.targetID) const href = stressSessionHref(fixture.targetID)
const result = await measureSessionSwitch(page, { const result = await measureSessionSwitch(page, {
@@ -134,8 +117,6 @@ async function openReviewPane(page: Page) {
await page.getByRole("button", { name: "Toggle review" }).click() await page.getByRole("button", { name: "Toggle review" }).click()
const panel = page.locator("#review-panel") const panel = page.locator("#review-panel")
await expect(panel).toBeVisible() await expect(panel).toBeVisible()
// Text-based readiness works across review implementations; the legacy list mounts
// diff viewers lazily while V2 mounts the active preview eagerly.
await page.waitForFunction(() => { await page.waitForFunction(() => {
const panel = document.querySelector<HTMLElement>("#review-panel") const panel = document.querySelector<HTMLElement>("#review-panel")
const text = panel?.textContent ?? "" const text = panel?.textContent ?? ""
@@ -1,4 +1,5 @@
import { base64Encode } from "@opencode-ai/core/util/encode" import { base64Encode } from "@opencode-ai/core/util/encode"
import type { JsonValue, OpenCodeEvent, SessionMessageAssistant, SessionMessageInfo } from "@opencode-ai/client/promise"
import type { Page } from "@playwright/test" import type { Page } from "@playwright/test"
import { mockOpenCodeServer } from "../../utils/mock-server" import { mockOpenCodeServer } from "../../utils/mock-server"
import { expectAppVisible, expectSessionTitle } from "../../utils/waits" import { expectAppVisible, expectSessionTitle } from "../../utils/waits"
@@ -10,37 +11,20 @@ const sessionID = "ses_timeline_state_regression"
const userMessageID = "msg_user_regression" const userMessageID = "msg_user_regression"
const assistantMessageID = "msg_assistant_regression" const assistantMessageID = "msg_assistant_regression"
const editPartID = "prt_0001_edit" const editPartID = "prt_0001_edit"
export const textPartID = "prt_9999_text" export const textPartID = `${assistantMessageID}:text:0`
const title = "Timeline collapse state regression" const title = "Timeline collapse state regression"
const model = { providerID: "opencode", modelID: "claude-opus-4-6", variant: "max" } const model = { providerID: "opencode", modelID: "claude-opus-4-6", variant: "max" }
type EventPayload = { type EventPayload = OpenCodeEvent
directory: string
payload: Record<string, unknown>
}
const userMessage = { const userMessage = {
info: { id: userMessageID,
id: userMessageID, type: "user",
sessionID, time: { created: 1700000000000 },
role: "user", text: "Please edit the file.",
time: { created: 1700000000000 }, } satisfies SessionMessageInfo
summary: { diffs: [] },
agent: "build",
model,
},
parts: [
{
id: "prt_user_text",
sessionID,
messageID: userMessageID,
type: "text",
text: "Please edit the file.",
},
],
}
const editPart = { const editPart: ToolSeed = {
id: editPartID, id: editPartID,
sessionID, sessionID,
messageID: assistantMessageID, messageID: assistantMessageID,
@@ -66,39 +50,22 @@ const editPart = {
}, },
} }
const streamedTextPart = {
id: textPartID,
sessionID,
messageID: assistantMessageID,
type: "text",
text: "Streaming added a later assistant text part.",
}
const assistantMessage = { const assistantMessage = {
info: { id: assistantMessageID,
id: assistantMessageID, type: "assistant",
sessionID, time: { created: 1700000001000 },
role: "assistant", model: { id: model.modelID, providerID: model.providerID, variant: model.variant },
time: { created: 1700000001000 }, agent: "build",
parentID: userMessageID, cost: 0.01,
modelID: model.modelID, tokens: { input: 100, output: 200, reasoning: 0, cache: { read: 0, write: 0 } },
providerID: model.providerID, content: [toolContent(editPart)],
mode: "build", } satisfies SessionMessageInfo
agent: "build",
path: { cwd: directory, root: directory },
cost: 0.01,
tokens: { input: 100, output: 200, reasoning: 0, cache: { read: 0, write: 0 } },
variant: "max",
},
parts: [editPart],
}
export async function setupTimelineBenchmark( export async function setupTimelineBenchmark(
page: Page, page: Page,
options: { options: {
historyTurns: number historyTurns: number
eventBatch: number eventBatch: number
newLayoutDesigns?: boolean
vcsDiff?: unknown[] vcsDiff?: unknown[]
turnDiffs?: unknown[] turnDiffs?: unknown[]
}, },
@@ -106,7 +73,7 @@ export async function setupTimelineBenchmark(
const events: EventPayload[] = [] const events: EventPayload[] = []
let eventBatch = options.eventBatch let eventBatch = options.eventBatch
const currentUserMessage = options.turnDiffs const currentUserMessage = options.turnDiffs
? { ...userMessage, info: { ...userMessage.info, summary: { diffs: options.turnDiffs } } } ? { ...userMessage, metadata: { diffs: options.turnDiffs as JsonValue } }
: userMessage : userMessage
await mockOpenCodeServer(page, { await mockOpenCodeServer(page, {
directory, directory,
@@ -124,26 +91,23 @@ export async function setupTimelineBenchmark(
events: () => events.splice(0, eventBatch), events: () => events.splice(0, eventBatch),
eventRetry: 16, eventRetry: 16,
}) })
await page.addInitScript( await page.addInitScript(() => {
(input) => { localStorage.setItem(
localStorage.setItem( "settings.v3",
"settings.v3", JSON.stringify({
JSON.stringify({ general: {
general: { editToolPartsExpanded: true,
newLayoutDesigns: input.newLayoutDesigns, shellToolPartsExpanded: true,
editToolPartsExpanded: true, showReasoningSummaries: true,
shellToolPartsExpanded: true, },
showReasoningSummaries: true, }),
}, )
}), })
)
},
{ newLayoutDesigns: options.newLayoutDesigns ?? false },
)
await page.setViewportSize({ width: 1366, height: 768 }) await page.setViewportSize({ width: 1366, height: 768 })
const scroller = page.locator(".scroll-view__viewport", { has: page.locator("[data-timeline-row]") }) const scroller = page.locator(".scroll-view__viewport", { has: page.locator("[data-timeline-row]") })
const text = page.locator(`[data-timeline-part-id="${textPartID}"]`).first() const text = page.locator(`[data-timeline-part-id="${textPartID}"]`).first()
await page.goto(`/${base64Encode(directory)}/session/${sessionID}`) const server = `http://${process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"}:${process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"}`
await page.goto(`/server/${base64Encode(server)}/session/${sessionID}`)
await expectSessionTitle(page, title) await expectSessionTitle(page, title)
await expectAppVisible(scroller) await expectAppVisible(scroller)
return { return {
@@ -187,34 +151,27 @@ export async function setupTimelineBenchmark(
} }
} }
export function buildInitialStreamEvent(deltaCount: number): EventPayload { export function buildInitialStreamEvent(deltaCount: number): EventPayload[] {
return { return [
directory, timelineEvent("session.text.started", { sessionID, assistantMessageID, ordinal: 0 }, true),
payload: { timelineEvent("session.text.delta", {
type: "message.part.updated", sessionID,
properties: { assistantMessageID,
part: { ordinal: 0,
...streamedTextPart, delta: `Streaming${streamChunk(0, deltaCount + 1)}\n\n\`\`\`ts\nconst initial = true\n\`\`\``,
text: `Streaming${streamChunk(0, deltaCount + 1)}\n\n\`\`\`ts\nconst initial = true\n\`\`\``, }),
}, ]
},
},
}
} }
export function buildStreamDeltaEvents(deltaCount: number): EventPayload[] { export function buildStreamDeltaEvents(deltaCount: number): EventPayload[] {
return Array.from({ length: deltaCount }, (_, index) => ({ return Array.from({ length: deltaCount }, (_, index) =>
directory, timelineEvent("session.text.delta", {
payload: { sessionID,
type: "message.part.delta", assistantMessageID,
properties: { ordinal: 0,
messageID: assistantMessageID, delta: streamChunk(index + 1, deltaCount + 1),
partID: textPartID, }),
field: "text", )
delta: streamChunk(index + 1, deltaCount + 1),
},
},
}))
} }
function performanceTurn(index: number) { function performanceTurn(index: number) {
@@ -320,48 +277,92 @@ function performanceTurn(index: number) {
}, },
] ]
: []), : []),
] ] as unknown as ContentSeed[]
return [ return [
{ {
info: { id: userID,
id: userID, type: "user",
sessionID, time: { created: 1690000000000 + index * 2_000 },
role: "user", text: `Historical prompt ${index}`,
time: { created: 1690000000000 + index * 2_000 },
summary: { diffs: [] },
agent: "build",
model,
},
parts: [
{
id: `prt_0000_${suffix}_user`,
sessionID,
messageID: userID,
type: "text",
text: `Historical prompt ${index}`,
},
],
}, },
{ {
info: { id: assistantID,
id: assistantID, type: "assistant",
sessionID, time: { created: 1690000001000 + index * 2_000, completed: 1690000001500 + index * 2_000 },
role: "assistant", model: { id: model.modelID, providerID: model.providerID, variant: model.variant },
time: { created: 1690000001000 + index * 2_000, completed: 1690000001500 + index * 2_000 }, agent: "build",
parentID: userID, cost: 0.01,
modelID: model.modelID, tokens: { input: 100, output: 200, reasoning: 0, cache: { read: 0, write: 0 } },
providerID: model.providerID, finish: "stop",
mode: "build", content: parts.map((part) => {
agent: "build", if (part.type === "text") return { type: "text" as const, text: part.text }
path: { cwd: directory, root: directory }, if (part.type === "reasoning")
cost: 0.01, return {
tokens: { input: 100, output: 200, reasoning: 0, cache: { read: 0, write: 0 } }, type: "reasoning" as const,
variant: "max", text: part.text,
finish: "stop", time: { created: part.time.start, completed: part.time.end },
}, }
parts, return toolContent(part)
}),
}, },
] ] satisfies SessionMessageInfo[]
}
type ToolSeed = {
id?: string
sessionID?: string
messageID?: string
type: "tool"
callID: string
tool: string
state: {
status: string
input: Record<string, unknown>
output: string
title?: string
metadata: Record<string, unknown>
time: { start: number; end: number }
}
}
type ContentSeedBase = { id?: string; sessionID?: string; messageID?: string }
type ContentSeed =
| (ContentSeedBase & { type: "text"; text: string })
| (ContentSeedBase & { type: "reasoning"; text: string; time: { start: number; end: number } })
| ToolSeed
function toolContent(part: ToolSeed): SessionMessageAssistant["content"][number] {
return {
type: "tool",
id: part.callID,
name: part.tool,
time: { created: part.state.time.start, ran: part.state.time.start, completed: part.state.time.end },
state: {
status: "completed",
input: part.state.input as Record<string, JsonValue>,
content: [{ type: "text", text: part.state.output }],
metadata: part.state.metadata as Record<string, JsonValue>,
},
}
}
let eventSequence = 0
function timelineEvent<Type extends "session.text.started" | "session.text.delta">(
type: Type,
data: Extract<OpenCodeEvent, { type: Type }>["data"],
durable = false,
): Extract<OpenCodeEvent, { type: Type }> {
eventSequence++
return {
id: `evt_timeline_benchmark_${eventSequence}`,
created: 1700000002000 + eventSequence,
type,
data,
location: { directory },
...(durable ? { durable: { aggregateID: sessionID, seq: eventSequence, version: 1 } } : {}),
} as unknown as Extract<OpenCodeEvent, { type: Type }>
} }
function historicalMarkdown(index: number) { function historicalMarkdown(index: number) {
@@ -15,7 +15,6 @@ import {
} from "./session-timeline-stream-probe" } from "./session-timeline-stream-probe"
type TimelineStreamOptions = { type TimelineStreamOptions = {
newLayoutDesigns?: boolean
reviewDiffs?: boolean reviewDiffs?: boolean
reviewPane?: boolean reviewPane?: boolean
} }
@@ -45,34 +44,27 @@ benchmark.describe("performance: session timeline streaming", () => {
report(result.metrics, result.context) report(result.metrics, result.context)
}) })
benchmark("streams assistant text in v2 with review pane closed", async ({ page, report }) => { benchmark("streams assistant text with review diffs and pane closed", async ({ page, report }) => {
benchmark.setTimeout(Number(process.env.TIMELINE_COMPLETION_TIMEOUT_MS ?? 420_000) + 60_000) benchmark.setTimeout(Number(process.env.TIMELINE_COMPLETION_TIMEOUT_MS ?? 420_000) + 60_000)
const result = await runTimelineStreamBenchmark(page, { newLayoutDesigns: true }) const result = await runTimelineStreamBenchmark(page, { reviewDiffs: true })
report(result.metrics, result.context) report(result.metrics, result.context)
}) })
benchmark("streams assistant text in v2 with review diffs and pane closed", async ({ page, report }) => { benchmark("streams assistant text with review pane open", async ({ page, report }) => {
benchmark.setTimeout(Number(process.env.TIMELINE_COMPLETION_TIMEOUT_MS ?? 420_000) + 60_000) benchmark.setTimeout(Number(process.env.TIMELINE_COMPLETION_TIMEOUT_MS ?? 420_000) + 60_000)
const result = await runTimelineStreamBenchmark(page, { newLayoutDesigns: true, reviewDiffs: true }) const result = await runTimelineStreamBenchmark(page, { reviewPane: true })
report(result.metrics, result.context)
})
benchmark("streams assistant text in v2 with review pane open", async ({ page, report }) => {
benchmark.setTimeout(Number(process.env.TIMELINE_COMPLETION_TIMEOUT_MS ?? 420_000) + 60_000)
const result = await runTimelineStreamBenchmark(page, { newLayoutDesigns: true, reviewPane: true })
report(result.metrics, result.context) report(result.metrics, result.context)
}) })
}) })
benchmark.describe("performance: review pane", () => { benchmark.describe("performance: review pane", () => {
benchmark("loads v2 review diffs and switches active files", async ({ page, report }) => { benchmark("loads review diffs and switches active files", async ({ page, report }) => {
benchmark.setTimeout(240_000) benchmark.setTimeout(240_000)
const historyTurns = Number(process.env.REVIEW_PANE_HISTORY_TURNS ?? 72) const historyTurns = Number(process.env.REVIEW_PANE_HISTORY_TURNS ?? 72)
const diffs = createReviewDiffs() const diffs = createReviewDiffs()
const fixture = await setupTimelineBenchmark(page, { const fixture = await setupTimelineBenchmark(page, {
historyTurns, historyTurns,
eventBatch: 1, eventBatch: 1,
newLayoutDesigns: true,
vcsDiff: diffs, vcsDiff: diffs,
}) })
@@ -112,7 +104,6 @@ async function runTimelineStreamBenchmark(page: Page, options: TimelineStreamOpt
const fixture = await setupTimelineBenchmark(page, { const fixture = await setupTimelineBenchmark(page, {
historyTurns, historyTurns,
eventBatch, eventBatch,
newLayoutDesigns: options.newLayoutDesigns,
// Turn diffs exercise timeline data cost; the pane-open scenario serves the same // Turn diffs exercise timeline data cost; the pane-open scenario serves the same
// diffs through the default git mode so it works across review implementations. // diffs through the default git mode so it works across review implementations.
turnDiffs: options.reviewDiffs ? diffs : undefined, turnDiffs: options.reviewDiffs ? diffs : undefined,
@@ -173,7 +164,6 @@ async function runTimelineStreamBenchmark(page: Page, options: TimelineStreamOpt
queuedDeltas: deltas.length, queuedDeltas: deltas.length,
historyTurns, historyTurns,
eventBatch, eventBatch,
newLayoutDesigns: options.newLayoutDesigns === true,
reviewPane: options.reviewPane === true ? "open" : "closed", reviewPane: options.reviewPane === true ? "open" : "closed",
reviewDiffs: diffs?.length ?? 0, reviewDiffs: diffs?.length ?? 0,
}, },
@@ -125,17 +125,20 @@ export async function installTimelineStreamProbe(
const scrollTo = Element.prototype.scrollTo const scrollTo = Element.prototype.scrollTo
const scrollTop = Object.getOwnPropertyDescriptor(Element.prototype, "scrollTop")! const scrollTop = Object.getOwnPropertyDescriptor(Element.prototype, "scrollTop")!
if (profileVisual) { if (profileVisual) {
Element.prototype.scrollTo = function (...args) { function measuredScrollTo(this: Element, options?: ScrollToOptions): void
function measuredScrollTo(this: Element, x: number, y: number): void
function measuredScrollTo(this: Element, first?: number | ScrollToOptions, second?: number) {
state.scroll.calls += 1 state.scroll.calls += 1
const top = typeof args[0] === "object" ? args[0]?.top : args[1] const top = typeof first === "object" ? first?.top : second
if (typeof top === "number") { if (typeof top === "number") {
const target = Math.min(top, this.scrollHeight - this.clientHeight) const target = Math.min(top, this.scrollHeight - this.clientHeight)
if (Math.abs(this.scrollTop - target) < 1) state.scroll.callNoops += 1 if (Math.abs(this.scrollTop - target) < 1) state.scroll.callNoops += 1
} }
if (state.scroll.lastCallFrame === state.scroll.frame) state.scroll.sameFrameCalls += 1 if (state.scroll.lastCallFrame === state.scroll.frame) state.scroll.sameFrameCalls += 1
state.scroll.lastCallFrame = state.scroll.frame state.scroll.lastCallFrame = state.scroll.frame
return scrollTo.apply(this, args) Reflect.apply(scrollTo, this, typeof first === "number" ? [first, second] : [first])
} }
Element.prototype.scrollTo = measuredScrollTo
Object.defineProperty(Element.prototype, "scrollTop", { Object.defineProperty(Element.prototype, "scrollTop", {
configurable: true, configurable: true,
get: scrollTop.get, get: scrollTop.get,
@@ -28,9 +28,22 @@ const directory = "C:/OpenCode/SmokeProject"
const projectID = "proj_smoke_timeline" const projectID = "proj_smoke_timeline"
const model = { providerID: "opencode", modelID: "claude-opus-4-6", variant: "max" } const model = { providerID: "opencode", modelID: "claude-opus-4-6", variant: "max" }
type MessageInfo = Record<string, unknown> & { id: string; role: "user" | "assistant" } type MessagePart = {
type MessagePart = Record<string, unknown> & { id: string; type: string; text?: string; tool?: string } id: string
type Message = { info: MessageInfo; parts: MessagePart[] } type: "text" | "reasoning" | "tool"
text?: string
time?: { start: number; end?: number }
callID?: string
tool?: string
state?: {
status: "completed"
input: Record<string, unknown>
output: string
title: unknown
metadata: Record<string, unknown>
time: { start: number; end: number }
}
}
function lorem(seed: number, length: number) { function lorem(seed: number, length: number) {
let out = "" let out = ""
@@ -48,54 +61,59 @@ function id(prefix: string, value: number) {
return `${prefix}_smoke_${String(value).padStart(4, "0")}` return `${prefix}_smoke_${String(value).padStart(4, "0")}`
} }
function userMessage(sessionID: string, index: number, textLength: number, diffs: unknown[] = []): Message { function userMessage(_sessionID: string, index: number, textLength: number, diffs: unknown[] = []): SessionMessageInfo {
const messageID = id("msg_user", index) const messageID = id("msg_user", index)
return { return {
info: { id: messageID,
id: messageID, type: "user",
sessionID, time: { created: 1700000000000 + index * 10_000 },
role: "user", text: lorem(index, textLength),
time: { created: 1700000000000 + index * 10_000 }, metadata: diffs.length ? { diffs: diffs as JsonValue } : undefined,
summary: { diffs },
agent: "build",
model,
},
parts: [
{
id: id("prt_user_text", index),
sessionID,
messageID,
type: "text",
text: lorem(index, textLength),
},
],
} }
} }
function assistantMessage(sessionID: string, index: number, parentID: string, parts: MessagePart[]): Message { function assistantMessage(
_sessionID: string,
index: number,
_parentID: string,
parts: MessagePart[],
): SessionMessageInfo {
const messageID = id("msg_assistant", index) const messageID = id("msg_assistant", index)
return { return {
info: { id: messageID,
id: messageID, type: "assistant",
sessionID, time: { created: 1700000000000 + index * 10_000 + 1_000, completed: 1700000000000 + index * 10_000 + 8_000 },
role: "assistant", model: { id: model.modelID, providerID: model.providerID, variant: model.variant },
time: { created: 1700000000000 + index * 10_000 + 1_000, completed: 1700000000000 + index * 10_000 + 8_000 }, agent: "build",
parentID, cost: 0.01,
modelID: model.modelID, tokens: { input: 100, output: 200, reasoning: 0, cache: { read: 0, write: 0 } },
providerID: model.providerID, finish: "stop",
mode: "build", content: parts.map(messageContent),
agent: "build", }
path: { cwd: directory, root: directory }, }
cost: 0.01,
tokens: { input: 100, output: 200, reasoning: 0, cache: { read: 0, write: 0 } }, function messageContent(part: MessagePart): SessionMessageAssistant["content"][number] {
variant: "max", if (part.type === "text") return { type: "text", text: part.text ?? "" }
finish: "stop", if (part.type === "reasoning")
return {
type: "reasoning",
text: part.text ?? "",
time: part.time
? { created: part.time.start, ...(part.time.end === undefined ? {} : { completed: part.time.end }) }
: undefined,
}
const state = part.state!
return {
type: "tool",
id: part.callID ?? part.id,
name: part.tool!,
time: { created: state.time.start, ran: state.time.start, completed: state.time.end },
state: {
status: "completed",
input: state.input as Record<string, JsonValue>,
content: [{ type: "text", text: state.output }],
metadata: state.metadata as Record<string, JsonValue>,
}, },
parts: parts.map((part) => ({
...part,
sessionID,
messageID,
})),
} }
} }
@@ -200,7 +218,7 @@ function code(seed: number, lines: number, width = 32) {
).join("\n") ).join("\n")
} }
function turn(index: number): Message[] { function turn(index: number): SessionMessageInfo[] {
const diff = index % 9 === 0 ? [fileDiff(`src/generated/summary-${index}.ts`, index)] : [] const diff = index % 9 === 0 ? [fileDiff(`src/generated/summary-${index}.ts`, index)] : []
const user = userMessage(targetID, index, 100 + (index % 4) * 80, diff) const user = userMessage(targetID, index, 100 + (index % 4) * 80, diff)
const parts = [ const parts = [
@@ -241,7 +259,7 @@ function turn(index: number): Message[] {
? [toolPart(index, 12, "task", { description: "Inspect generated fixture", subagent_type: "explore" }, 160)] ? [toolPart(index, 12, "task", { description: "Inspect generated fixture", subagent_type: "explore" }, 160)]
: []), : []),
] ]
return [user, assistantMessage(targetID, index, user.info.id, parts)] return [user, assistantMessage(targetID, index, user.id, parts)]
} }
const targetMessages = Array.from({ length: 72 }, (_, index) => turn(index)).flat() const targetMessages = Array.from({ length: 72 }, (_, index) => turn(index)).flat()
@@ -267,16 +285,10 @@ const childMessages = Array.from({ length: 4 }, (_, index) => [
userMessage(childID, index + 2000, 120), userMessage(childID, index + 2000, 120),
assistantMessage(childID, index + 2000, id("msg_user", index + 2000), [textPart(index + 2000, 0, 240)]), assistantMessage(childID, index + 2000, id("msg_user", index + 2000), [textPart(index + 2000, 0, 240)]),
]).flat() ]).flat()
const messages: Record<string, SessionMessageInfo[]> = {
function renderable(part: MessagePart) { [sourceID]: sourceMessages,
if (part.type === "tool" && part.tool === "todowrite") return false [targetID]: targetMessages,
if (part.type === "text") return !!part.text.trim() [childID]: childMessages,
if (part.type === "reasoning") return !!part.text.trim()
return part.type !== "step-start" && part.type !== "step-finish" && part.type !== "patch"
}
function orderedParts(message: Message) {
return message.parts.slice().sort((a, b) => a.id.localeCompare(b.id))
} }
export const fixture = { export const fixture = {
@@ -333,37 +345,39 @@ export const fixture = {
sourceID, sourceID,
targetID, targetID,
childID, childID,
messages: { [sourceID]: sourceMessages, [targetID]: targetMessages, [childID]: childMessages }, messages,
expected: { expected: {
sourceTitle: "Uncommitted changes inquiry", sourceTitle: "Uncommitted changes inquiry",
targetTitle: "Example Game: sample jump movement & sample physics analysis", targetTitle: "Example Game: sample jump movement & sample physics analysis",
childTitle: "Inspect child navigation", childTitle: "Inspect child navigation",
sourceMessageIDs: sourceMessages sourceMessageIDs: sourceMessages.filter((message) => message.type === "user").map((message) => message.id),
.filter((message) => message.info.role === "user") targetMessageIDs: targetMessages.filter((message) => message.type === "user").map((message) => message.id),
.map((message) => message.info.id), childMessageIDs: childMessages.filter((message) => message.type === "user").map((message) => message.id),
targetMessageIDs: targetMessages targetPartIDs: targetMessages.flatMap((message) => {
.filter((message) => message.info.role === "user") if (message.type !== "assistant") return []
.map((message) => message.info.id), const ordinals = { text: 0, reasoning: 0 }
childMessageIDs: childMessages.filter((message) => message.info.role === "user").map((message) => message.info.id), return message.content.flatMap((part) => {
targetPartIDs: targetMessages.flatMap((message) => if (part.type === "text") return part.text.trim() ? [`${message.id}:text:${ordinals.text++}`] : []
orderedParts(message) if (part.type === "reasoning")
.filter(renderable) return part.text.trim() ? [`${message.id}:reasoning:${ordinals.reasoning++}`] : []
.map((part) => part.id), return [part.id]
), })
}),
}, },
} }
export function pageMessages(sessionID: string, limit: number, before?: string) { export function pageMessages(sessionID: string, limit: number, before?: string) {
const messages = fixture.messages[sessionID as keyof typeof fixture.messages] ?? [] const messages = fixture.messages[sessionID] ?? []
const end = before const end = before
? Math.max( ? Math.max(
0, 0,
messages.findIndex((message) => message.info.id === before), messages.findIndex((message) => message.id === before),
) )
: messages.length : messages.length
const start = Math.max(0, end - limit) const start = Math.max(0, end - limit)
return { return {
items: messages.slice(start, end), items: messages.slice(start, end),
cursor: start > 0 ? messages[start]!.info.id : undefined, cursor: start > 0 ? messages[start].id : undefined,
} }
} }
import type { JsonValue, SessionMessageAssistant, SessionMessageInfo } from "@opencode-ai/client/promise"
@@ -9,7 +9,6 @@ export async function installTimelineSettings(page: Page) {
"settings.v3", "settings.v3",
JSON.stringify({ JSON.stringify({
general: { general: {
newLayoutDesigns: true,
editToolPartsExpanded: true, editToolPartsExpanded: true,
shellToolPartsExpanded: true, shellToolPartsExpanded: true,
showReasoningSummaries: true, showReasoningSummaries: true,
@@ -1,121 +1,6 @@
import { expect, test } from "bun:test" import { expect, test } from "bun:test"
import type { SessionMessageInfo } from "@opencode-ai/client/promise"
import type { Page, Route } from "@playwright/test" import type { Page, Route } from "@playwright/test"
import { currentMessage, mockOpenCodeServer } from "../../utils/mock-server" import { mockOpenCodeServer } from "../../utils/mock-server"
test("preserves current messages", () => {
const message = {
id: "msg_current",
type: "user",
time: { created: 1 },
text: "current",
files: [{ data: "e30=", mime: "application/json", source: { type: "inline" } }],
} satisfies SessionMessageInfo
expect(currentMessage(message)).toBe(message)
})
test("converts rich legacy messages to current message types", () => {
expect(
currentMessage({
info: { id: "msg_user", role: "user", time: { created: 1 } },
parts: [
{ type: "text", text: "Use @src/a.ts with @explore" },
{
type: "file",
mime: "application/json",
filename: "data.json",
url: "data:application/json;base64,e30=",
},
{
type: "file",
mime: "text/plain",
filename: "a.ts",
url: "src/a.ts",
source: { type: "file", text: { value: "@src/a.ts", start: 4, end: 13 } },
},
{ type: "agent", name: "explore", source: { value: "@explore", start: 19, end: 27 } },
],
}),
).toEqual({
id: "msg_user",
type: "user",
time: { created: 1 },
text: "Use @src/a.ts with @explore",
files: [
{ data: "e30=", mime: "application/json", name: "data.json", source: { type: "inline" } },
{
data: "",
mime: "text/plain",
name: "a.ts",
source: { type: "uri", uri: "src/a.ts" },
mention: { text: "@src/a.ts", start: 4, end: 13 },
},
],
agents: [{ name: "explore", mention: { text: "@explore", start: 19, end: 27 } }],
})
expect(
currentMessage({
info: {
id: "msg_assistant",
role: "assistant",
time: { created: 2, completed: 5 },
agent: "explore",
modelID: "model",
providerID: "provider",
variant: "high",
cost: 0.5,
tokens: { input: 1, output: 2, reasoning: 3, cache: { read: 4, write: 5 } },
finish: "tool-calls",
error: { name: "MessageAbortedError", data: { message: "Stopped" } },
},
parts: [
{ type: "text", text: "Answer" },
{ type: "reasoning", text: "Thinking", time: { start: 2, end: 3 } },
{
id: "prt_tool",
callID: "call_tool",
type: "tool",
tool: "read",
state: {
status: "completed",
input: { filePath: "src/a.ts" },
output: "contents",
metadata: { title: "a.ts" },
time: { start: 3, end: 4 },
},
},
],
}),
).toEqual({
id: "msg_assistant",
type: "assistant",
time: { created: 2, completed: 5 },
agent: "explore",
model: { id: "model", providerID: "provider", variant: "high" },
cost: 0.5,
tokens: { input: 1, output: 2, reasoning: 3, cache: { read: 4, write: 5 } },
finish: "tool-calls",
error: { type: "MessageAbortedError", message: "Stopped" },
content: [
{ type: "text", text: "Answer" },
{ type: "reasoning", text: "Thinking", time: { created: 2, completed: 3 } },
{
type: "tool",
id: "call_tool",
name: "read",
time: { created: 3, ran: 3, completed: 4 },
state: {
status: "completed",
input: { filePath: "src/a.ts" },
content: [{ type: "text", text: "contents" }],
metadata: { title: "a.ts" },
},
},
],
})
})
test("applies message latency after a list response gate is released", async () => { test("applies message latency after a list response gate is released", async () => {
const events: string[] = [] const events: string[] = []
@@ -5,7 +5,7 @@ import {
type VisualStabilityTrace, type VisualStabilityTrace,
} from "../../utils/visual-stability" } from "../../utils/visual-stability"
import { analyzeVisualObservations } from "../../utils/visual-stability/analyzer" import { analyzeVisualObservations } from "../../utils/visual-stability/analyzer"
import { legacyVisualPlan, visualPlan, type VisualInvariant } from "../../utils/visual-stability/invariant" import { visualPlan, type VisualInvariant } from "../../utils/visual-stability/invariant"
import { defineVisualRegions, mapVisualRegions } from "../../utils/visual-stability/regions" import { defineVisualRegions, mapVisualRegions } from "../../utils/visual-stability/regions"
function trace(samples: VisualStabilityTrace["samples"]): VisualStabilityTrace { function trace(samples: VisualStabilityTrace["samples"]): VisualStabilityTrace {
@@ -346,19 +346,6 @@ test("evaluates the typed invariant algebra over explicit observations", () => {
]) ])
}) })
test("legacy plan adapter preserves analyzer messages and order", () => {
const input = trace([
frame(0, region({ label: "Exploring", opacity: 1, bottom: 40 }), region({ top: 40, bottom: 60 })),
frame(16, region({ label: "Explored", opacity: 0.2, bottom: 50 }), region({ top: 49, bottom: 69 })),
frame(32, region({ label: "Exploring", opacity: 1, bottom: 50 }), region({ top: 50, bottom: 70 })),
])
const options = { flow: ["changing", "following"], stable: ["changing"] }
expect(analyzeVisualObservations(input.samples, legacyVisualPlan(options))).toEqual(
analyzeVisualStability(input, options),
)
})
function frame( function frame(
at: number, at: number,
changing: VisualStabilityTrace["samples"][number]["regions"][string], changing: VisualStabilityTrace["samples"][number]["regions"][string],
@@ -13,7 +13,6 @@ test("closing the active server's last tab opens the remaining server tab", asyn
await mockServers(page, requests) await mockServers(page, requests)
await page.addInitScript( await page.addInitScript(
({ serverB, sessionA, sessionB }) => { ({ serverB, sessionA, sessionB }) => {
localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } }))
localStorage.setItem("opencode.global.dat:server", JSON.stringify({ list: [serverB] })) localStorage.setItem("opencode.global.dat:server", JSON.stringify({ list: [serverB] }))
localStorage.setItem( localStorage.setItem(
"opencode.window.browser.dat:tabs", "opencode.window.browser.dat:tabs",
@@ -39,31 +38,6 @@ test("closing the active server's last tab opens the remaining server tab", asyn
await expect(page.getByText(sessionB.title).first()).toBeVisible() await expect(page.getByText(sessionB.title).first()).toBeVisible()
const sessionBRequests = requests.filter((url) => url.includes(`/session/${sessionB.id}`)) const sessionBRequests = requests.filter((url) => url.includes(`/session/${sessionB.id}`))
expect(sessionBRequests.every((url) => url.startsWith(serverB))).toBe(true) expect(sessionBRequests.every((url) => url.startsWith(serverB))).toBe(true)
expect(
requests.some((request) => {
const url = new URL(request)
return url.origin === serverB && url.searchParams.get("directory") === sessionB.directory
}),
).toBe(true)
})
test("legacy session routes preserve an existing tab's server", async ({ page }) => {
await mockServers(page, [])
await page.addInitScript(
({ serverB, sessionB }) => {
localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } }))
localStorage.setItem("opencode.global.dat:server", JSON.stringify({ list: [serverB] }))
localStorage.setItem(
"opencode.window.browser.dat:tabs",
JSON.stringify([{ type: "session", server: serverB, sessionId: sessionB }]),
)
},
{ serverB, sessionB: sessionB.id },
)
const hrefB = `/server/${base64Encode(serverB)}/session/${sessionB.id}`
await page.goto(`/${base64Encode(sessionB.directory)}/session/${sessionB.id}`)
await expect(page).toHaveURL(new RegExp(`${hrefB.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}$`))
}) })
function session(id: string, directory: string, title: string) { function session(id: string, directory: string, title: string) {
@@ -93,40 +67,26 @@ async function mockServers(page: Page, requests: string[]) {
if (url.pathname === "/api/session/active") return json(route, { data: {} }) if (url.pathname === "/api/session/active") return json(route, { data: {} })
if (url.pathname === `/api/session/${current.id}`) return json(route, { data: currentSession(current) }) if (url.pathname === `/api/session/${current.id}`) return json(route, { data: currentSession(current) })
if (url.pathname === `/api/session/${current.id}/message`) return json(route, { data: [], cursor: {} }) if (url.pathname === `/api/session/${current.id}/message`) return json(route, { data: [], cursor: {} })
if (/^\/session\/[^/]+$/.test(url.pathname)) return json(route, { name: "NotFoundError" }, 404) if (["/api/agent", "/api/provider", "/api/model", "/api/command", "/api/reference"].includes(url.pathname))
if (/^\/session\/[^/]+\/(children|todo|diff)$/.test(url.pathname)) return json(route, []) return json(route, { location: { directory: current.directory }, data: [] })
if (["/skill", "/command", "/lsp", "/formatter", "/permission", "/question", "/vcs/diff"].includes(url.pathname)) if (url.pathname === "/api/model/default")
return json(route, []) return json(route, { location: { directory: current.directory }, data: null })
if (url.pathname === "/provider") if (url.pathname === "/api/permission/request" || url.pathname === "/api/question/request")
return json(route, { all: [], connected: [], default: { providerID: "", modelID: "" } }) return json(route, { location: { directory: current.directory }, data: [] })
if (url.pathname === "/agent") return json(route, [{ name: "build", mode: "primary" }]) if (url.pathname === "/api/mcp") return json(route, { location: { directory: current.directory }, data: [] })
if (url.pathname === "/project" || url.pathname === "/project/current") { if (url.pathname === "/api/mcp/resource")
return json(route, { location: { directory: current.directory }, data: { resources: [], templates: [] } })
if (url.pathname === "/api/project" || url.pathname === "/api/project/current") {
const project = { const project = {
id: current.projectID, id: current.projectID,
worktree: current.directory, canonical: current.directory,
vcs: "git", vcs: "git",
time: { created: 1, updated: 1 }, time: { created: 1, updated: 1 },
sandboxes: [], sandboxes: [],
} }
return json(route, url.pathname === "/project" ? [project] : project) return json(route, url.pathname === "/api/project" ? [project] : { id: project.id, directory: current.directory })
} }
if (url.pathname === "/path") if (url.pathname === "/api/location") return json(route, { directory: current.directory })
return json(route, {
state: current.directory,
config: current.directory,
worktree: current.directory,
directory: current.directory,
home: current.directory,
})
if (url.pathname === "/api/path")
return json(route, {
state: current.directory,
config: current.directory,
worktree: current.directory,
directory: current.directory,
home: current.directory,
})
if (url.pathname === "/vcs") return json(route, { branch: "main", default_branch: "main" })
if (url.pathname === "/api/vcs") if (url.pathname === "/api/vcs")
return json(route, { return json(route, {
location: { directory: current.directory }, location: { directory: current.directory },
@@ -124,7 +124,6 @@ async function setup(page: Page) {
await page.addInitScript( await page.addInitScript(
({ directory, server, sessionID }) => { ({ directory, server, sessionID }) => {
localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } }))
localStorage.setItem( localStorage.setItem(
"opencode.global.dat:server", "opencode.global.dat:server",
JSON.stringify({ JSON.stringify({
@@ -28,7 +28,6 @@ test("matches the rounded panel corners to the dark new-session background", asy
}) })
await page.addInitScript( await page.addInitScript(
({ directory, draftID, server }) => { ({ directory, draftID, server }) => {
localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } }))
localStorage.setItem("opencode-theme-id", "oc-2") localStorage.setItem("opencode-theme-id", "oc-2")
localStorage.setItem("opencode-color-scheme", "dark") localStorage.setItem("opencode-color-scheme", "dark")
localStorage.setItem( localStorage.setItem(
@@ -81,10 +81,6 @@ test("expands a folder whose path has a trailing Windows separator", async ({ pa
await page.addInitScript( await page.addInitScript(
({ directory, server, sessionID }) => { ({ directory, server, sessionID }) => {
localStorage.setItem(
"settings.v3",
JSON.stringify({ general: { newLayoutDesigns: true, shouldDisplayTabsToast: false } }),
)
localStorage.setItem( localStorage.setItem(
"opencode.global.dat:server", "opencode.global.dat:server",
JSON.stringify({ JSON.stringify({
@@ -1,60 +0,0 @@
import { expect, test } from "@playwright/test"
import type { Page } from "@playwright/test"
import { fixture, pageMessages } from "../smoke/session-timeline.fixture"
import { mockOpenCodeServer } from "../utils/mock-server"
import { expectAppVisible } from "../utils/waits"
const NAMES = ["alpha-service", "bravo-web", "charlie-api", "delta-tools", "echo-infra", "foxtrot-docs"]
const worktrees = NAMES.map((name) => `/opencode-demo/${name}`)
// The sixth project sits outside the five-item recent cap, so it is only reachable if the
// dialog hands every recent project to the list filter instead of a pre-truncated slice.
const OUTSIDE_CAP = "foxtrot-docs"
// Dialog rows carry data-directory-path; the sidebar project list does not, so this
// scopes assertions to the picker instead of matching the sidebar entry of the same name.
const rows = (page: Page) => page.locator("[data-directory-path]")
const row = (page: Page, name: string) => page.locator(`[data-directory-path*="${name}"]`)
async function openProjectDialog(page: Page) {
await mockOpenCodeServer(page, {
sessions: fixture.sessions,
provider: fixture.provider,
directory: fixture.directory,
project: fixture.project,
pageMessages,
fileList: () => [],
findFiles: () => [],
})
await page.addInitScript((dirs) => {
localStorage.setItem(
"opencode.global.dat:server",
JSON.stringify({
projects: { local: dirs.map((worktree: string) => ({ worktree, expanded: false })) },
lastProject: {},
}),
)
}, worktrees)
await page.goto("/")
const add = page.getByRole("button", { name: "Add project" }).first()
await expectAppVisible(add)
await add.click()
await expect(rows(page)).toHaveCount(5)
return page.getByRole("textbox").last()
}
test("searches every recent project, not just the five most recent", async ({ page }) => {
const search = await openProjectDialog(page)
await expect(row(page, OUTSIDE_CAP)).toHaveCount(0)
await search.fill("foxtrot")
await expect(row(page, OUTSIDE_CAP)).toHaveCount(1)
})
test("still caps the idle recent list at five projects", async ({ page }) => {
await openProjectDialog(page)
await expect(row(page, NAMES[4])).toHaveCount(1)
await expect(row(page, OUTSIDE_CAP)).toHaveCount(0)
})
@@ -6,6 +6,7 @@ import { expectAppVisible } from "../utils/waits"
const directory = "C:/OpenCode/PromptInputV2Editing" const directory = "C:/OpenCode/PromptInputV2Editing"
const projectID = "proj_prompt_input_v2_editing" const projectID = "proj_prompt_input_v2_editing"
const sessionID = "ses_prompt_input_v2_editing" const sessionID = "ses_prompt_input_v2_editing"
const server = `http://${process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"}:${process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"}`
test("preserves the draft when a populated command menu triggers a built-in", async ({ page }) => { test("preserves the draft when a populated command menu triggers a built-in", async ({ page }) => {
await mockOpenCodeServer(page, { await mockOpenCodeServer(page, {
@@ -32,11 +33,7 @@ test("preserves the draft when a populated command menu triggers a built-in", as
], ],
pageMessages: () => ({ items: [] }), pageMessages: () => ({ items: [] }),
}) })
await page.addInitScript(() => { await page.goto(`/server/${base64Encode(server)}/session/${sessionID}`)
localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } }))
})
await page.goto(`/${base64Encode(directory)}/session/${sessionID}`)
const composer = page.locator('[data-component="prompt-input-v2"]') const composer = page.locator('[data-component="prompt-input-v2"]')
const input = composer.locator('[data-component="prompt-input"]') const input = composer.locator('[data-component="prompt-input"]')
await expectAppVisible(composer) await expectAppVisible(composer)
@@ -6,6 +6,7 @@ import { expectAppVisible } from "../utils/waits"
const directory = "C:/OpenCode/PromptThinkingLevelRegression" const directory = "C:/OpenCode/PromptThinkingLevelRegression"
const projectID = "proj_prompt_thinking_level_regression" const projectID = "proj_prompt_thinking_level_regression"
const sessionID = "ses_prompt_thinking_level_regression" const sessionID = "ses_prompt_thinking_level_regression"
const server = `http://${process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"}:${process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"}`
test("shows the V2 thinking level control while relevant", async ({ page }) => { test("shows the V2 thinking level control while relevant", async ({ page }) => {
await mockOpenCodeServer(page, { await mockOpenCodeServer(page, {
@@ -49,11 +50,7 @@ test("shows the V2 thinking level control while relevant", async ({ page }) => {
], ],
pageMessages: () => ({ items: [] }), pageMessages: () => ({ items: [] }),
}) })
await page.addInitScript(() => { await page.goto(`/server/${base64Encode(server)}/session/${sessionID}`)
localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } }))
})
await page.goto(`/${base64Encode(directory)}/session/${sessionID}`)
const composer = page.locator('[data-component="prompt-input-v2"]') const composer = page.locator('[data-component="prompt-input-v2"]')
const input = composer.locator('[data-component="prompt-input"]') const input = composer.locator('[data-component="prompt-input"]')
const control = composer.getByRole("button", { name: "Choose model variant" }) const control = composer.getByRole("button", { name: "Choose model variant" })
@@ -49,7 +49,7 @@ test("auto-accept responds for an unfocused server session", async ({ page }) =>
const permissionRequests: string[] = [] const permissionRequests: string[] = []
const permissionResponses: PermissionResponse[] = [] const permissionResponses: PermissionResponse[] = []
await installSseTransport(page, { server: serverB }) await installSseTransport(page, { server: serverB })
const transport = await installSseTransport<{ directory: string; payload: Record<string, unknown> }>(page, { const transport = await installSseTransport(page, {
server: serverA, server: serverA,
retry: 20, retry: 20,
}) })
@@ -82,18 +82,17 @@ test("auto-accept responds for an unfocused server session", async ({ page }) =>
await transport.waitForConnection() await transport.waitForConnection()
await transport.send({ await transport.send({
directory: directoryA, id: "evt_permission_background_a",
payload: { created: 1700000001000,
id: "event-permission-background-a", type: "permission.asked",
type: "permission.asked", location: { directory: directoryA },
properties: { data: {
id: "permission-background-a", id: "permission-background-a",
sessionID: sessionA.id, sessionID: sessionA.id,
permission: "bash", action: "bash",
patterns: ["git status"], resources: ["git status"],
metadata: {}, metadata: {},
always: [], save: [],
},
}, },
}) })
@@ -110,18 +109,17 @@ test("auto-accept responds for an unfocused server session", async ({ page }) =>
]) ])
await transport.send({ await transport.send({
directory: directoryA, id: "evt_permission_background_a_child",
payload: { created: 1700000002000,
id: "event-permission-background-a-child", type: "permission.asked",
type: "permission.asked", location: { directory: directoryA },
properties: { data: {
id: "permission-background-a-child", id: "permission-background-a-child",
sessionID: childSessionA.id, sessionID: childSessionA.id,
permission: "bash", action: "bash",
patterns: ["git diff"], resources: ["git diff"],
metadata: {}, metadata: {},
always: [], save: [],
},
}, },
}) })
@@ -156,7 +154,6 @@ type PermissionResponse = {
async function configureServers(page: Page, tabs: { type: "session"; server: string; sessionId: string }[] = []) { async function configureServers(page: Page, tabs: { type: "session"; server: string; sessionId: string }[] = []) {
await page.addInitScript( await page.addInitScript(
({ serverB, tabs }) => { ({ serverB, tabs }) => {
localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } }))
localStorage.setItem("opencode.global.dat:server", JSON.stringify({ list: [serverB] })) localStorage.setItem("opencode.global.dat:server", JSON.stringify({ list: [serverB] }))
localStorage.setItem("opencode.window.browser.dat:tabs", JSON.stringify(tabs)) localStorage.setItem("opencode.window.browser.dat:tabs", JSON.stringify(tabs))
}, },
@@ -220,44 +217,14 @@ async function mockServers(page: Page, permissionRequests: string[], permissionR
} }
if (url.pathname === "/api/project/current") if (url.pathname === "/api/project/current")
return json(route, { id: remote ? sessionB.projectID : "project-server-a", directory }) return json(route, { id: remote ? sessionB.projectID : "project-server-a", directory })
if (url.pathname === "/api/session") return json(route, { data: sessions.map(currentSession), cursor: {} }) if (url.pathname === "/api/session")
return json(route, { data: sessions.map((session) => currentSession(session)), cursor: {} })
if (url.pathname === "/api/session/active") return json(route, { data: {} }) if (url.pathname === "/api/session/active") return json(route, { data: {} })
const currentSessionInfo = sessions.find((session) => url.pathname === `/api/session/${session.id}`) const currentSessionInfo = sessions.find((session) => url.pathname === `/api/session/${session.id}`)
if (currentSessionInfo) return json(route, { data: currentSession(currentSessionInfo) }) if (currentSessionInfo) return json(route, { data: currentSession(currentSessionInfo) })
if (sessions.some((session) => url.pathname === `/api/session/${session.id}/message`)) if (sessions.some((session) => url.pathname === `/api/session/${session.id}/message`))
return json(route, { data: [], cursor: {} }) return json(route, { data: [], cursor: {} })
if (/^\/session\/[^/]+$/.test(url.pathname)) return json(route, { name: "NotFoundError" }, 404) if (url.pathname === "/api/location") return json(route, { directory })
if (/^\/session\/[^/]+\/message$/.test(url.pathname)) return json(route, [])
if (/^\/session\/[^/]+\/(children|todo|diff)$/.test(url.pathname)) return json(route, [])
if (url.pathname === "/permission") {
permissionRequests.push(url.toString())
return json(route, [])
}
if (["/skill", "/command", "/lsp", "/formatter", "/question", "/vcs/diff", "/pty/shells"].includes(url.pathname))
return json(route, [])
if (url.pathname === "/provider") return json(route, provider(remote ? "server-b" : "server-a"))
if (url.pathname === "/agent") return json(route, [{ name: "build", mode: "primary" }])
if (url.pathname === "/project" || url.pathname === "/project/current") {
const project = {
id: remote ? sessionB.projectID : "project-server-a",
worktree: directory,
vcs: "git",
time: { created: 1, updated: 1 },
sandboxes: [],
}
return json(route, url.pathname === "/project" ? [project] : project)
}
if (url.pathname === "/path")
return json(route, {
state: directory,
config: directory,
worktree: directory,
directory,
home: directory,
})
if (url.pathname === "/api/path")
return json(route, { state: directory, config: directory, worktree: directory, directory, home: directory })
if (url.pathname === "/vcs") return json(route, { branch: "main", default_branch: "main" })
if (url.pathname === "/api/vcs") if (url.pathname === "/api/vcs")
return json(route, { location: { directory }, data: { branch: "main", defaultBranch: "main" } }) return json(route, { location: { directory }, data: { branch: "main", defaultBranch: "main" } })
if (url.pathname === "/api/pty/shells") return json(route, { location: { directory }, data: [] }) if (url.pathname === "/api/pty/shells") return json(route, { location: { directory }, data: [] })
@@ -11,7 +11,6 @@ test("tab busy indicator reflects the tab server's own session status", async ({
await mockServers(page) await mockServers(page)
await page.addInitScript( await page.addInitScript(
({ serverA, serverB, sessionA, sessionB }) => { ({ serverA, serverB, sessionA, sessionB }) => {
localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } }))
localStorage.setItem("opencode.global.dat:server", JSON.stringify({ list: [serverB] })) localStorage.setItem("opencode.global.dat:server", JSON.stringify({ list: [serverB] }))
localStorage.setItem( localStorage.setItem(
"opencode.window.browser.dat:tabs", "opencode.window.browser.dat:tabs",
@@ -58,47 +57,33 @@ async function mockServers(page: Page) {
const current = url.origin === serverA ? sessionA : sessionB const current = url.origin === serverA ? sessionA : sessionB
const directory = url.searchParams.get("directory") const directory = url.searchParams.get("directory")
if (directory && directory !== current.directory) return json(route, { name: "InvalidDirectory" }, 500) if (directory && directory !== current.directory) return json(route, { name: "InvalidDirectory" }, 500)
if (url.pathname === "/api/event") return sse(route, url.pathname === "/api/event") if (url.pathname === "/api/event") return sse(route)
if (url.pathname === "/api/health") return json(route, { pid: 1 }) if (url.pathname === "/api/health") return json(route, { pid: 1 })
if (url.pathname === "/api/session/active") if (url.pathname === "/api/session/active")
return json(route, { data: url.origin === serverB ? { [sessionB.id]: { type: "running" } } : {} }) return json(route, { data: url.origin === serverB ? { [sessionB.id]: { type: "running" } } : {} })
if (url.pathname === "/api/session") return json(route, { data: [currentSession(current)], cursor: {} }) if (url.pathname === "/api/session") return json(route, { data: [currentSession(current)], cursor: {} })
if (url.pathname === `/api/session/${current.id}`) return json(route, { data: currentSession(current) }) if (url.pathname === `/api/session/${current.id}`) return json(route, { data: currentSession(current) })
if (url.pathname === `/api/session/${current.id}/message`) return json(route, { data: [], cursor: {} }) if (url.pathname === `/api/session/${current.id}/message`) return json(route, { data: [], cursor: {} })
if (/^\/session\/[^/]+$/.test(url.pathname)) return json(route, { name: "NotFoundError" }, 404) if (["/api/agent", "/api/provider", "/api/model", "/api/command", "/api/reference"].includes(url.pathname))
if (/^\/session\/[^/]+\/(children|todo|diff)$/.test(url.pathname)) return json(route, []) return json(route, { location: { directory: current.directory }, data: [] })
if (["/skill", "/command", "/lsp", "/formatter", "/permission", "/question", "/vcs/diff"].includes(url.pathname)) if (url.pathname === "/api/model/default")
return json(route, []) return json(route, { location: { directory: current.directory }, data: null })
if (url.pathname === "/provider") if (url.pathname === "/api/permission/request" || url.pathname === "/api/question/request")
return json(route, { all: [], connected: [], default: { providerID: "", modelID: "" } }) return json(route, { location: { directory: current.directory }, data: [] })
if (url.pathname === "/agent") return json(route, [{ name: "build", mode: "primary" }]) if (url.pathname === "/api/mcp") return json(route, { location: { directory: current.directory }, data: [] })
if (url.pathname === "/project" || url.pathname === "/project/current") { if (url.pathname === "/api/mcp/resource")
return json(route, { location: { directory: current.directory }, data: { resources: [], templates: [] } })
if (url.pathname === "/api/project" || url.pathname === "/api/project/current") {
const project = { const project = {
id: current.projectID, id: current.projectID,
worktree: current.directory, canonical: current.directory,
vcs: "git", vcs: "git",
time: { created: 1, updated: 1 }, time: { created: 1, updated: 1 },
sandboxes: [], sandboxes: [],
} }
return json(route, url.pathname === "/project" ? [project] : project) return json(route, url.pathname === "/api/project" ? [project] : { id: project.id, directory: current.directory })
} }
if (url.pathname === "/path") if (url.pathname === "/api/location") return json(route, { directory: current.directory })
return json(route, {
state: current.directory,
config: current.directory,
worktree: current.directory,
directory: current.directory,
home: current.directory,
})
if (url.pathname === "/api/path")
return json(route, {
state: current.directory,
config: current.directory,
worktree: current.directory,
directory: current.directory,
home: current.directory,
})
if (url.pathname === "/vcs") return json(route, { branch: "main", default_branch: "main" })
if (url.pathname === "/api/vcs") if (url.pathname === "/api/vcs")
return json(route, { return json(route, {
location: { directory: current.directory }, location: { directory: current.directory },
@@ -117,10 +102,10 @@ function json(route: Route, body: unknown, status = 200) {
}) })
} }
function sse(route: Route, current: boolean) { function sse(route: Route) {
return route.fulfill({ return route.fulfill({
status: 200, status: 200,
contentType: "text/event-stream", contentType: "text/event-stream",
body: current ? 'data: {"id":"evt_connected","type":"server.connected","data":{}}\n\n' : ": ok\n\n", body: 'data: {"id":"evt_connected","type":"server.connected","data":{}}\n\n',
}) })
} }
@@ -1,39 +0,0 @@
import { expect, test } from "@playwright/test"
import { mockOpenCodeServer } from "../utils/mock-server"
const draftID = "draft_removed_layout_preference"
const directory = "C:/OpenCode/RemovedLayoutPreference"
const server = `http://${process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"}:${process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"}`
test("ignores persisted old layout preferences when opening drafts", async ({ page }) => {
await mockOpenCodeServer(page, {
directory,
project: {
id: "proj_removed_layout_preference",
worktree: directory,
vcs: "git",
name: "removed-layout-preference",
time: { created: 1700000000000, updated: 1700000000000 },
sandboxes: [],
},
provider: { all: [], connected: [], default: {} },
sessions: [],
pageMessages: () => ({ items: [] }),
})
await page.addInitScript(
({ directory, draftID, server }) => {
localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: false } }))
localStorage.setItem(
"opencode.window.browser.dat:tabs",
JSON.stringify([{ type: "draft", draftID, server, directory }]),
)
},
{ directory, draftID, server },
)
await page.goto(`/new-session?draftId=${draftID}`)
await expect(page).toHaveURL(`/new-session?draftId=${draftID}`)
await expect(page.locator("body")).toHaveAttribute("data-new-layout", "")
await expect(page.getByRole("textbox", { name: "Prompt" })).toBeVisible()
})
@@ -7,6 +7,7 @@ const directory = "C:/OpenCode/ReviewImageFlashRegression"
const sessionID = "ses_review_image_flash_regression" const sessionID = "ses_review_image_flash_regression"
const title = "Review image flash regression" const title = "Review image flash regression"
const imageFile = "assets/preview.png" const imageFile = "assets/preview.png"
const server = `http://${process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"}:${process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"}`
test("clicking an image file in the v2 review pane does not blank the panel", async ({ page }) => { test("clicking an image file in the v2 review pane does not blank the panel", async ({ page }) => {
await openReview(page) await openReview(page)
@@ -26,9 +27,6 @@ test("clicking an image file in the v2 review pane does not blank the panel", as
async function openReview(page: Page) { async function openReview(page: Page) {
await page.setViewportSize({ width: 960, height: 900 }) await page.setViewportSize({ width: 960, height: 900 })
await page.addInitScript(() => {
localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } }))
})
await mockOpenCodeServer(page, { await mockOpenCodeServer(page, {
directory, directory,
project: { project: {
@@ -112,30 +110,16 @@ async function openReview(page: Page) {
pageMessages: () => ({ pageMessages: () => ({
items: [ items: [
{ {
info: { id: "msg_review_image_flash_regression",
id: "msg_review_image_flash_regression", type: "user",
sessionID, time: { created: 1700000000000 },
role: "user", text: "Review this change.",
time: { created: 1700000000000 },
summary: { diffs: [] },
agent: "build",
model: { providerID: "opencode", modelID: "test" },
},
parts: [
{
id: "prt_review_image_flash_regression",
sessionID,
messageID: "msg_review_image_flash_regression",
type: "text",
text: "Review this change.",
},
],
}, },
], ],
}), }),
}) })
await page.goto(`/${base64Encode(directory)}/session/${sessionID}`) await page.goto(`/server/${base64Encode(server)}/session/${sessionID}`)
await expectSessionTitle(page, title) await expectSessionTitle(page, title)
await page.getByRole("button", { name: "Toggle review" }).click() await page.getByRole("button", { name: "Toggle review" }).click()
await expectAppVisible(page.locator('#review-panel [data-component="session-review-v2"]')) await expectAppVisible(page.locator('#review-panel [data-component="session-review-v2"]'))
@@ -6,6 +6,7 @@ import { expectAppVisible, expectSessionTitle } from "../utils/waits"
const directory = "C:/OpenCode/ReviewLineCommentRegression" const directory = "C:/OpenCode/ReviewLineCommentRegression"
const sessionID = "ses_review_line_comment_regression" const sessionID = "ses_review_line_comment_regression"
const title = "Review line comment regression" const title = "Review line comment regression"
const server = `http://${process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"}:${process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"}`
test.beforeEach(async ({ page }) => { test.beforeEach(async ({ page }) => {
await openReview(page) await openReview(page)
@@ -88,7 +89,6 @@ test("stages a submitted line comment in the prompt context", async ({ page }) =
async function openReview(page: Page) { async function openReview(page: Page) {
await page.setViewportSize({ width: 700, height: 900 }) await page.setViewportSize({ width: 700, height: 900 })
await mockOpenCodeServer(page, { await mockOpenCodeServer(page, {
protocol: "v2",
directory, directory,
project: { project: {
id: "proj_review_line_comment_regression", id: "proj_review_line_comment_regression",
@@ -123,30 +123,16 @@ async function openReview(page: Page) {
pageMessages: () => ({ pageMessages: () => ({
items: [ items: [
{ {
info: { id: "msg_review_line_comment_regression",
id: "msg_review_line_comment_regression", type: "user",
sessionID, time: { created: 1700000000000 },
role: "user", text: "Review this change.",
time: { created: 1700000000000 },
summary: { diffs: [] },
agent: "build",
model: { providerID: "opencode", modelID: "test" },
},
parts: [
{
id: "prt_review_line_comment_regression",
sessionID,
messageID: "msg_review_line_comment_regression",
type: "text",
text: "Review this change.",
},
],
}, },
], ],
}), }),
}) })
await page.goto(`/${base64Encode(directory)}/session/${sessionID}`) await page.goto(`/server/${base64Encode(server)}/session/${sessionID}`)
await expectSessionTitle(page, title) await expectSessionTitle(page, title)
const changes = page.getByRole("tab", { name: "Changes" }) const changes = page.getByRole("tab", { name: "Changes" })
const diffResponse = page.waitForResponse( const diffResponse = page.waitForResponse(
@@ -14,7 +14,6 @@ test.use({ viewport: { width: 1440, height: 900 } })
test("opens and searches project files inline", async ({ page }) => { test("opens and searches project files inline", async ({ page }) => {
const searches: { query: string; dirs?: string; limit?: number }[] = [] const searches: { query: string; dirs?: string; limit?: number }[] = []
await mockOpenCodeServer(page, { await mockOpenCodeServer(page, {
protocol: "v2",
directory, directory,
project: { project: {
id: projectID, id: projectID,
@@ -63,7 +62,6 @@ test("opens and searches project files inline", async ({ page }) => {
}) })
await page.addInitScript( await page.addInitScript(
({ directory, server, sessionID }) => { ({ directory, server, sessionID }) => {
localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } }))
localStorage.setItem( localStorage.setItem(
"opencode.global.dat:server", "opencode.global.dat:server",
JSON.stringify({ JSON.stringify({
@@ -54,7 +54,6 @@ async function switchSession(page: Page, title: string) {
async function setup(page: Page) { async function setup(page: Page) {
await mockOpenCodeServer(page, { await mockOpenCodeServer(page, {
protocol: "v2",
directory, directory,
project: { project: {
id: projectID, id: projectID,
@@ -103,7 +102,6 @@ async function setup(page: Page) {
) )
await page.addInitScript( await page.addInitScript(
({ directory, server, sessions }) => { ({ directory, server, sessions }) => {
localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } }))
localStorage.setItem( localStorage.setItem(
"opencode.global.dat:server", "opencode.global.dat:server",
JSON.stringify({ JSON.stringify({
@@ -103,7 +103,6 @@ async function setup(page: Page) {
await page.addInitScript( await page.addInitScript(
({ directory, server, sessions }) => { ({ directory, server, sessions }) => {
localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } }))
localStorage.setItem( localStorage.setItem(
"opencode.global.dat:server", "opencode.global.dat:server",
JSON.stringify({ JSON.stringify({
@@ -6,6 +6,7 @@ const directory = "C:/OpenCode/ReviewTerminalStacked"
const projectID = "proj_review_terminal_stacked" const projectID = "proj_review_terminal_stacked"
const sessionID = "ses_review_terminal_stacked" const sessionID = "ses_review_terminal_stacked"
const title = "Review terminal stacked" const title = "Review terminal stacked"
const server = `http://${process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"}:${process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"}`
const branchDiffs = [ const branchDiffs = [
fileDiff(".github/actions/setup-bun/action.yml", 7), fileDiff(".github/actions/setup-bun/action.yml", 7),
...Array.from({ length: 2_739 }, (_, index) => ...Array.from({ length: 2_739 }, (_, index) =>
@@ -21,7 +22,6 @@ test("keeps the review tree and terminal sized when both panels are open", async
test.setTimeout(120_000) test.setTimeout(120_000)
await page.setViewportSize({ width: 1400, height: 900 }) await page.setViewportSize({ width: 1400, height: 900 })
await mockOpenCodeServer(page, { await mockOpenCodeServer(page, {
protocol: "v2",
directory, directory,
project: { project: {
id: projectID, id: projectID,
@@ -124,15 +124,13 @@ test("keeps the review tree and terminal sized when both panels are open", async
) )
await page.routeWebSocket("**/api/pty/pty_review_terminal/connect", () => undefined) await page.routeWebSocket("**/api/pty/pty_review_terminal/connect", () => undefined)
await page.addInitScript(() => { await page.addInitScript(() => {
localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } }))
localStorage.setItem( localStorage.setItem(
"opencode.global.dat:layout", "opencode.global.dat:layout",
JSON.stringify({ review: { diffStyle: "split", panelOpened: true } }), JSON.stringify({ review: { diffStyle: "split", panelOpened: true } }),
) )
}) })
await page.goto(`/${base64Encode(directory)}/session/${sessionID}`) await page.goto(`/server/${base64Encode(server)}/session/${sessionID}`)
const server = `http://${process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"}:${process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"}`
await expectSessionReady(page, { server, sessionID, title }) await expectSessionReady(page, { server, sessionID, title })
await expect(page.locator("#review-panel")).toBeVisible() await expect(page.locator("#review-panel")).toBeVisible()
await expectTree(page, 2_773, "action.yml") await expectTree(page, 2_773, "action.yml")
@@ -16,8 +16,7 @@ test("shows loaded sessions before the directory path request resolves", async (
const pathBlocked = new Promise<void>((resolve) => { const pathBlocked = new Promise<void>((resolve) => {
releasePath = resolve releasePath = resolve
}) })
await page.route("**/api/path?*", async (route) => { await page.route("**/api/location*", async (route) => {
if (!new URL(route.request().url()).searchParams.has("location[directory]")) return route.fallback()
await pathBlocked await pathBlocked
return route.fallback() return route.fallback()
}) })
@@ -8,6 +8,7 @@ const directory = "C:/OpenCode/RequestDocks"
const projectID = "proj_request_docks" const projectID = "proj_request_docks"
const sessionID = "ses_request_docks" const sessionID = "ses_request_docks"
const title = "Request dock regression" const title = "Request dock regression"
const server = `http://${process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"}:${process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"}`
test("shows a pending question dock", async ({ page }) => { test("shows a pending question dock", async ({ page }) => {
await mockServer(page, { await mockServer(page, {
@@ -34,7 +35,7 @@ test("shows a pending question dock", async ({ page }) => {
], ],
}) })
await page.goto(`/${base64Encode(directory)}/session/${sessionID}`) await page.goto(`/server/${base64Encode(server)}/session/${sessionID}`)
await expectSessionTitle(page, title) await expectSessionTitle(page, title)
const question = page.locator('[data-component="dock-prompt"][data-kind="question"]') const question = page.locator('[data-component="dock-prompt"][data-kind="question"]')
@@ -92,7 +93,7 @@ test("shows a pending permission dock", async ({ page }) => {
], ],
}) })
await page.goto(`/${base64Encode(directory)}/session/${sessionID}`) await page.goto(`/server/${base64Encode(server)}/session/${sessionID}`)
await expectSessionTitle(page, title) await expectSessionTitle(page, title)
const permission = page.locator('[data-component="dock-prompt"][data-kind="permission"]') const permission = page.locator('[data-component="dock-prompt"][data-kind="permission"]')
@@ -111,11 +112,11 @@ test("shows a pending permission dock", async ({ page }) => {
test("restores the draft caret before typing after a request dock closes", async ({ page }) => { test("restores the draft caret before typing after a request dock closes", async ({ page }) => {
const transport = await installSseTransport(page, { const transport = await installSseTransport(page, {
server: `http://${process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"}:${process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"}`, server,
retry: 20, retry: 20,
}) })
await mockServer(page, { forms: [] }) await mockServer(page, { forms: [] })
await page.goto(`/${base64Encode(directory)}/session/${sessionID}`) await page.goto(`/server/${base64Encode(server)}/session/${sessionID}`)
await transport.waitForConnection() await transport.waitForConnection()
await expectSessionTitle(page, title) await expectSessionTitle(page, title)
@@ -138,26 +139,26 @@ test("restores the draft caret before typing after a request dock closes", async
) )
.toBe(cursor) .toBe(cursor)
await transport.send({ await transport.send({
directory, id: "evt_form_created",
payload: { created: 1700000001000,
type: "form.created", type: "form.created",
properties: { location: { directory },
form: { data: {
id: "frm_question_caret", form: {
sessionID, id: "frm_question_caret",
title: "Questions", sessionID,
metadata: { kind: "question", tool: { messageID: "message-caret", id: "call-caret" } }, title: "Questions",
fields: [ metadata: { kind: "question", tool: { messageID: "message-caret", id: "call-caret" } },
{ fields: [
key: "q0", {
type: "string", key: "q0",
title: "Continue", type: "string",
description: "Continue?", title: "Continue",
options: [{ value: "yes", label: "Yes", description: "Continue the session" }], description: "Continue?",
custom: true, options: [{ value: "yes", label: "Yes", description: "Continue the session" }],
}, custom: true,
], },
}, ],
}, },
}, },
}) })
@@ -166,11 +167,11 @@ test("restores the draft caret before typing after a request dock closes", async
await expect(editor).toHaveCount(0) await expect(editor).toHaveCount(0)
await transport.send({ await transport.send({
directory, id: "evt_form_cancelled",
payload: { created: 1700000002000,
type: "form.cancelled", type: "form.cancelled",
properties: { sessionID, id: "frm_question_caret" }, location: { directory },
}, data: { sessionID, id: "frm_question_caret" },
}) })
await expect(question).toHaveCount(0) await expect(question).toHaveCount(0)
await expect(editor).toBeVisible() await expect(editor).toBeVisible()
@@ -183,13 +184,11 @@ async function mockServer(
page: Page, page: Page,
requests: { requests: {
permissions?: unknown[] | (() => unknown[]) permissions?: unknown[] | (() => unknown[])
questions?: unknown[] | (() => unknown[])
forms?: unknown[] | (() => unknown[]) forms?: unknown[] | (() => unknown[])
sessionStatus?: Record<string, unknown> sessionStatus?: Record<string, unknown>
}, },
) { ) {
await mockOpenCodeServer(page, { await mockOpenCodeServer(page, {
protocol: "v2",
directory, directory,
project: { project: {
id: projectID, id: projectID,
@@ -229,11 +228,7 @@ async function mockServer(
], ],
pageMessages: () => ({ items: [] }), pageMessages: () => ({ items: [] }),
permissions: requests.permissions, permissions: requests.permissions,
questions: requests.questions,
forms: requests.forms, forms: requests.forms,
sessionStatus: requests.sessionStatus, sessionStatus: requests.sessionStatus,
}) })
await page.addInitScript(() => {
localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } }))
})
} }
@@ -1,4 +1,5 @@
import { expect, test, type Locator, type Page } from "@playwright/test" import { expect, test, type Locator, type Page } from "@playwright/test"
import type { JsonValue, OpenCodeEvent, SessionMessageAssistant, SessionMessageInfo } from "@opencode-ai/client/promise"
import { mockOpenCodeServer } from "../utils/mock-server" import { mockOpenCodeServer } from "../utils/mock-server"
import { expectAppVisible, expectSessionTitle } from "../utils/waits" import { expectAppVisible, expectSessionTitle } from "../utils/waits"
@@ -8,14 +9,11 @@ const sessionID = "ses_timeline_state_regression"
const userMessageID = "msg_user_regression" const userMessageID = "msg_user_regression"
const assistantMessageID = "msg_assistant_regression" const assistantMessageID = "msg_assistant_regression"
const editPartID = "prt_0001_edit" const editPartID = "prt_0001_edit"
const textPartID = "prt_9999_text" const textPartID = `${assistantMessageID}:text:0`
const title = "Timeline collapse state regression" const title = "Timeline collapse state regression"
const model = { providerID: "opencode", modelID: "claude-opus-4-6", variant: "max" } const model = { providerID: "opencode", modelID: "claude-opus-4-6", variant: "max" }
type EventPayload = { type EventPayload = OpenCodeEvent
directory: string
payload: Record<string, unknown>
}
declare global { declare global {
interface Window { interface Window {
@@ -27,25 +25,11 @@ declare global {
} }
const userMessage = { const userMessage = {
info: { id: userMessageID,
id: userMessageID, type: "user",
sessionID, time: { created: 1700000000000 },
role: "user", text: "Please edit the file.",
time: { created: 1700000000000 }, } satisfies SessionMessageInfo
summary: { diffs: [] },
agent: "build",
model,
},
parts: [
{
id: "prt_user_text",
sessionID,
messageID: userMessageID,
type: "text",
text: "Please edit the file.",
},
],
}
const editPart = { const editPart = {
id: editPartID, id: editPartID,
@@ -74,31 +58,19 @@ const editPart = {
} }
const streamedTextPart = { const streamedTextPart = {
id: textPartID,
sessionID,
messageID: assistantMessageID,
type: "text",
text: "Streaming added a later assistant text part.", text: "Streaming added a later assistant text part.",
} }
const assistantMessage = { const assistantMessage = {
info: { id: assistantMessageID,
id: assistantMessageID, type: "assistant",
sessionID, time: { created: 1700000001000 },
role: "assistant", model: { id: model.modelID, providerID: model.providerID, variant: model.variant },
time: { created: 1700000001000 }, agent: "build",
parentID: userMessageID, cost: 0.01,
modelID: model.modelID, tokens: { input: 100, output: 200, reasoning: 0, cache: { read: 0, write: 0 } },
providerID: model.providerID, content: [toolContent(editPart)],
mode: "build", } satisfies SessionMessageInfo
agent: "build",
path: { cwd: directory, root: directory },
cost: 0.01,
tokens: { input: 100, output: 200, reasoning: 0, cache: { read: 0, write: 0 } },
variant: "max",
},
parts: [editPart],
}
test.describe("regression: session timeline local row state", () => { test.describe("regression: session timeline local row state", () => {
test("keeps a manually collapsed tool collapsed when later assistant content streams", async ({ page }) => { test("keeps a manually collapsed tool collapsed when later assistant content streams", async ({ page }) => {
@@ -106,7 +78,7 @@ test.describe("regression: session timeline local row state", () => {
await mockServer(page, events) await mockServer(page, events)
await configurePage(page) await configurePage(page)
await page.goto(`/${base64Encode(directory)}/session/${sessionID}`) await page.goto(sessionHref())
await expectSessionTitle(page, title) await expectSessionTitle(page, title)
const wrapper = page.locator(`[data-timeline-part-id="${editPartID}"]`).first() const wrapper = page.locator(`[data-timeline-part-id="${editPartID}"]`).first()
@@ -119,15 +91,11 @@ test.describe("regression: session timeline local row state", () => {
await wrapper.locator('[data-slot="collapsible-trigger"]').first().click() await wrapper.locator('[data-slot="collapsible-trigger"]').first().click()
await expectExpanded(wrapper, false) await expectExpanded(wrapper, false)
events.push({ events.push(...textEvents())
directory,
payload: {
type: "message.part.updated",
properties: { part: streamedTextPart },
},
})
await expect(page.locator(`[data-timeline-part-id="${textPartID}"]`).first()).toBeVisible({ timeout: 10_000 }) await expect(page.locator(`[data-timeline-part-id="${assistantMessageID}:text:0"]`).first()).toBeVisible({
timeout: 10_000,
})
expect(await readToolState(page)).toEqual({ expect(await readToolState(page)).toEqual({
expanded: false, expanded: false,
@@ -136,13 +104,13 @@ test.describe("regression: session timeline local row state", () => {
}) })
}) })
test("does not remount an edit diff when sibling parts or diff counts update", async ({ page }) => { test("does not remount an edit diff when a sibling part arrives", async ({ page }) => {
const events: EventPayload[] = [] const events: EventPayload[] = []
await installDiffProbe(page) await installDiffProbe(page)
await mockServer(page, events) await mockServer(page, events)
await configurePage(page) await configurePage(page)
await page.goto(`/${base64Encode(directory)}/session/${sessionID}`) await page.goto(sessionHref())
await expectSessionTitle(page, title) await expectSessionTitle(page, title)
const wrapper = page.locator(`[data-timeline-part-id="${editPartID}"]`).first() const wrapper = page.locator(`[data-timeline-part-id="${editPartID}"]`).first()
@@ -151,15 +119,11 @@ test.describe("regression: session timeline local row state", () => {
await expectAppVisible(file) await expectAppVisible(file)
await markDiffProbe(page) await markDiffProbe(page)
events.push({ events.push(...textEvents())
directory,
payload: {
type: "message.part.updated",
properties: { part: streamedTextPart },
},
})
await expect(page.locator(`[data-timeline-part-id="${textPartID}"]`).first()).toBeVisible({ timeout: 10_000 }) await expect(page.locator(`[data-timeline-part-id="${assistantMessageID}:text:0"]`).first()).toBeVisible({
timeout: 10_000,
})
const siblingProbe = await readDiffProbe(page) const siblingProbe = await readDiffProbe(page)
expect(siblingProbe).toEqual({ expect(siblingProbe).toEqual({
fileMarker: "before", fileMarker: "before",
@@ -169,27 +133,6 @@ test.describe("regression: session timeline local row state", () => {
shadowRoots: 0, shadowRoots: 0,
toolMarker: "before", toolMarker: "before",
}) })
await markDiffProbe(page)
events.push({
directory,
payload: {
type: "message.part.updated",
properties: { part: editPartWithAdditions(2) },
},
})
await expect(wrapper.locator('[data-slot="diff-changes-additions"]').filter({ hasText: "+2" }).first()).toBeVisible(
{ timeout: 10_000 },
)
expect(await readDiffProbe(page)).toEqual({
fileMarker: "before",
frameMarker: "before",
rowKey: `assistant-part:${userMessageID}:part:${assistantMessageID}:${editPartID}`,
rowMarker: "before",
shadowRoots: 0,
toolMarker: "before",
})
}) })
test("keeps a sticky edit header aligned with a multi-hunk diff", async ({ page }) => { test("keeps a sticky edit header aligned with a multi-hunk diff", async ({ page }) => {
@@ -216,10 +159,10 @@ test.describe("regression: session timeline local row state", () => {
}, },
}, },
} }
await mockServer(page, events, [userMessage, { ...assistantMessage, parts: [part] }]) await mockServer(page, events, [userMessage, { ...assistantMessage, content: [toolContent(part)] }])
await configurePage(page) await configurePage(page)
await page.goto(`/${base64Encode(directory)}/session/${sessionID}`) await page.goto(sessionHref())
await expectSessionTitle(page, title) await expectSessionTitle(page, title)
const wrapper = page.locator(`[data-timeline-part-id="${editPartID}"]`).first() const wrapper = page.locator(`[data-timeline-part-id="${editPartID}"]`).first()
@@ -293,7 +236,7 @@ async function readToolState(page: Page) {
row: element.closest("[data-timeline-row]")?.getAttribute("data-timeline-row"), row: element.closest("[data-timeline-row]")?.getAttribute("data-timeline-row"),
streamedTextVisible: !!document.querySelector(`[data-timeline-part-id="${textPartID}"]`), streamedTextVisible: !!document.querySelector(`[data-timeline-part-id="${textPartID}"]`),
}), }),
textPartID, `${assistantMessageID}:text:0`,
) )
} }
@@ -355,22 +298,103 @@ async function readDiffProbe(page: Page) {
}) })
} }
function editPartWithAdditions(additions: number) { function toolContent(part: typeof editPart): SessionMessageAssistant["content"][number] {
return { return {
...editPart, type: "tool",
id: part.callID,
name: part.tool,
time: { created: part.state.time.start, ran: part.state.time.start, completed: part.state.time.end },
state: { state: {
...editPart.state, status: "completed",
metadata: { input: part.state.input,
...editPart.state.metadata, content: [{ type: "text", text: part.state.output }],
filediff: { metadata: part.state.metadata as Record<string, JsonValue>,
...editPart.state.metadata.filediff,
additions,
},
},
}, },
} }
} }
let eventSequence = -1
function textEvents(): OpenCodeEvent[] {
return [
eventValue("session.text.started", { sessionID, assistantMessageID, ordinal: 0 }, 1),
eventValue(
"session.text.ended",
{
sessionID,
assistantMessageID,
ordinal: 0,
text: streamedTextPart.text,
},
1,
),
]
}
function toolEvents(part: typeof editPart): OpenCodeEvent[] {
return [
eventValue(
"session.tool.input.started",
{
sessionID,
assistantMessageID,
id: part.callID,
name: part.tool,
},
1,
),
eventValue(
"session.tool.input.ended",
{
sessionID,
assistantMessageID,
id: part.callID,
text: JSON.stringify(part.state.input),
},
1,
),
eventValue(
"session.tool.called",
{
sessionID,
assistantMessageID,
id: part.callID,
input: part.state.input,
executed: true,
},
1,
),
eventValue(
"session.tool.success",
{
sessionID,
assistantMessageID,
id: part.callID,
content: [{ type: "text", text: part.state.output }],
metadata: part.state.metadata as Record<string, JsonValue>,
executed: true,
},
2,
),
]
}
function eventValue<Type extends OpenCodeEvent["type"]>(
type: Type,
data: Extract<OpenCodeEvent, { type: Type }>["data"],
version: 1 | 2,
): Extract<OpenCodeEvent, { type: Type }> {
eventSequence++
return {
id: `evt_collapse_${eventSequence}`,
created: 1700000002000 + eventSequence,
type,
data,
location: { directory },
durable: { aggregateID: sessionID, seq: eventSequence, version },
} as unknown as Extract<OpenCodeEvent, { type: Type }>
}
function readExpanded(element: Element) { function readExpanded(element: Element) {
const trigger = element.querySelector('[data-slot="collapsible-trigger"]') const trigger = element.querySelector('[data-slot="collapsible-trigger"]')
const aria = trigger?.getAttribute("aria-expanded") const aria = trigger?.getAttribute("aria-expanded")
@@ -385,7 +409,12 @@ function readExpanded(element: Element) {
return !!content && content.getBoundingClientRect().height > 0 return !!content && content.getBoundingClientRect().height > 0
} }
async function mockServer(page: Page, events: EventPayload[], messages = [userMessage, assistantMessage]) { async function mockServer(
page: Page,
events: EventPayload[],
messages: SessionMessageInfo[] = [userMessage, assistantMessage],
) {
eventSequence = -1
await mockOpenCodeServer(page, { await mockOpenCodeServer(page, {
directory, directory,
project: project(), project: project(),
@@ -437,3 +466,8 @@ function provider() {
function base64Encode(value: string) { function base64Encode(value: string) {
return Buffer.from(value, "utf8").toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "") return Buffer.from(value, "utf8").toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "")
} }
function sessionHref() {
const server = `http://${process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"}:${process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"}`
return `/server/${base64Encode(server)}/session/${sessionID}`
}
@@ -1,4 +1,5 @@
import { expect, test, type Page } from "@playwright/test" import { expect, test, type Page } from "@playwright/test"
import type { JsonValue, OpenCodeEvent, SessionMessageAssistant, SessionMessageInfo } from "@opencode-ai/client/promise"
import { mockOpenCodeServer } from "../utils/mock-server" import { mockOpenCodeServer } from "../utils/mock-server"
import { expectAppVisible, expectSessionTitle } from "../utils/waits" import { expectAppVisible, expectSessionTitle } from "../utils/waits"
import { import {
@@ -17,11 +18,6 @@ const model = { providerID: "opencode", modelID: "claude-opus-4-6", variant: "ma
const contextIDs = ["ctx_0100_read", "ctx_0101_glob", "ctx_0102_grep", "ctx_0103_list"] const contextIDs = ["ctx_0100_read", "ctx_0101_glob", "ctx_0102_grep", "ctx_0103_list"]
const followingTextID = `${id("msg_assistant", 10)}:text:0` const followingTextID = `${id("msg_assistant", 10)}:text:0`
type Message = {
info: Record<string, unknown> & { id: string; role: "user" | "assistant" }
parts: Record<string, unknown>[]
}
const messages = [...Array.from({ length: 8 }, (_, index) => turn(index, false)).flat(), ...turn(10, true)] const messages = [...Array.from({ length: 8 }, (_, index) => turn(index, false)).flat(), ...turn(10, true)]
test.describe("regression: session timeline context group resize", () => { test.describe("regression: session timeline context group resize", () => {
@@ -30,7 +26,7 @@ test.describe("regression: session timeline context group resize", () => {
await mockServer(page) await mockServer(page)
await configurePage(page) await configurePage(page)
await page.goto(`/${base64Encode(directory)}/session/${sessionID}`) await page.goto(sessionHref())
await expectSessionTitle(page, title) await expectSessionTitle(page, title)
await expectAppVisible(page.locator(`[data-timeline-part-ids="${contextIDs.join(",")}"]`).first()) await expectAppVisible(page.locator(`[data-timeline-part-ids="${contextIDs.join(",")}"]`).first())
await expectAppVisible(page.locator(`[data-timeline-part-id="${followingTextID}"]`).first()) await expectAppVisible(page.locator(`[data-timeline-part-id="${followingTextID}"]`).first())
@@ -45,7 +41,7 @@ test.describe("regression: session timeline context group resize", () => {
}) })
test("paints a stable exploring to explored transition", async ({ page }) => { test("paints a stable exploring to explored transition", async ({ page }) => {
const events: { directory: string; payload: Record<string, unknown> }[] = [] const events: OpenCodeEvent[] = []
await page.setViewportSize({ width: 1400, height: 900 }) await page.setViewportSize({ width: 1400, height: 900 })
await mockServer(page, events, [ await mockServer(page, events, [
...Array.from({ length: 8 }, (_, index) => turn(index, false)).flat(), ...Array.from({ length: 8 }, (_, index) => turn(index, false)).flat(),
@@ -53,7 +49,7 @@ test.describe("regression: session timeline context group resize", () => {
]) ])
await configurePage(page) await configurePage(page)
await page.goto(`/${base64Encode(directory)}/session/${sessionID}`) await page.goto(sessionHref())
await expectSessionTitle(page, title) await expectSessionTitle(page, title)
const devtools = await page.context().newCDPSession(page) const devtools = await page.context().newCDPSession(page)
await devtools.send("Emulation.setCPUThrottlingRate", { rate: 4 }) await devtools.send("Emulation.setCPUThrottlingRate", { rate: 4 })
@@ -75,25 +71,21 @@ test.describe("regression: session timeline context group resize", () => {
}) })
await startVisualProbe(page, regions) await startVisualProbe(page, regions)
for (const [index, delay] of [120, 350, 80, 500].entries()) { for (const [index, delay] of [120, 350, 80, 500].entries()) {
events.push({ events.push(
directory, ...toolEvents(
payload: { contextTool(
type: "message.part.updated", contextIDs[index]!,
properties: { id("msg_assistant", 10),
part: contextTool( ["read", "glob", "grep", "list"][index]!,
contextIDs[index]!, [
id("msg_assistant", 10), { filePath: "src/recent-a.ts" },
["read", "glob", "grep", "list"][index]!, { path: directory, pattern: "**/*.ts" },
[ { path: directory, pattern: "Explored" },
{ filePath: "src/recent-a.ts" }, { path: "src" },
{ path: directory, pattern: "**/*.ts" }, ][index]!,
{ path: directory, pattern: "Explored" }, ),
{ path: "src" }, ),
][index]!, )
),
},
},
})
await page.waitForTimeout(delay) await page.waitForTimeout(delay)
} }
@@ -211,74 +203,51 @@ async function sampleExpansion(page: Page) {
) )
} }
function turn(index: number, target: boolean, status: "running" | "completed" = "completed"): Message[] { function turn(index: number, target: boolean, status: "running" | "completed" = "completed"): SessionMessageInfo[] {
const userID = id("msg_user", index) const userID = id("msg_user", index)
const assistantID = id("msg_assistant", index) const assistantID = id("msg_assistant", index)
const content: SessionMessageAssistant["content"] = target
? [
toolContent(
contextTool(
contextIDs[0]!,
assistantID,
"read",
{ filePath: "src/recent-a.ts", offset: 0, limit: 120 },
status,
),
),
toolContent(contextTool(contextIDs[1]!, assistantID, "glob", { path: directory, pattern: "**/*.ts" }, status)),
toolContent(
contextTool(
contextIDs[2]!,
assistantID,
"grep",
{ path: directory, pattern: "Explored", include: "*.ts" },
status,
),
),
toolContent(contextTool(contextIDs[3]!, assistantID, "list", { path: "src" }, status)),
{ type: "text", text: "This assistant text is immediately after the explored context group." },
]
: [{ type: "text", text: `Assistant filler ${index}. ${"filler ".repeat(60)}` }]
return [ return [
{ {
info: { id: userID,
id: userID, type: "user",
sessionID, time: { created: 1700000000000 + index * 10_000 },
role: "user", text: `User message ${index}`,
time: { created: 1700000000000 + index * 10_000 },
summary: { diffs: [] },
agent: "build",
model,
},
parts: [{ id: id("prt_user", index), sessionID, messageID: userID, type: "text", text: `User message ${index}` }],
}, },
{ {
info: { id: assistantID,
id: assistantID, type: "assistant",
sessionID, time: { created: 1700000000000 + index * 10_000 + 1_000, completed: 1700000000000 + index * 10_000 + 2_000 },
role: "assistant", model: { id: model.modelID, providerID: model.providerID, variant: model.variant },
time: { created: 1700000000000 + index * 10_000 + 1_000, completed: 1700000000000 + index * 10_000 + 2_000 }, agent: "build",
parentID: userID, cost: 0.01,
modelID: model.modelID, tokens: { input: 100, output: 200, reasoning: 0, cache: { read: 0, write: 0 } },
providerID: model.providerID, finish: "stop",
mode: "build", content,
agent: "build",
path: { cwd: directory, root: directory },
cost: 0.01,
tokens: { input: 100, output: 200, reasoning: 0, cache: { read: 0, write: 0 } },
variant: "max",
finish: "stop",
},
parts: target
? [
contextTool(
contextIDs[0]!,
assistantID,
"read",
{ filePath: "src/recent-a.ts", offset: 0, limit: 120 },
status,
),
contextTool(contextIDs[1]!, assistantID, "glob", { path: directory, pattern: "**/*.ts" }, status),
contextTool(
contextIDs[2]!,
assistantID,
"grep",
{ path: directory, pattern: "Explored", include: "*.ts" },
status,
),
contextTool(contextIDs[3]!, assistantID, "list", { path: "src" }, status),
{
id: followingTextID,
sessionID,
messageID: assistantID,
type: "text",
text: "This assistant text is immediately after the explored context group.",
},
]
: [
{
id: id("prt_text", index),
sessionID,
messageID: assistantID,
type: "text",
text: `Assistant filler ${index}. ${"filler ".repeat(60)}`,
},
],
}, },
] ]
} }
@@ -308,11 +277,76 @@ function contextTool(
} }
} }
async function mockServer( type ContextTool = ReturnType<typeof contextTool>
page: Page,
events: { directory: string; payload: Record<string, unknown> }[] = [], function toolContent(part: ContextTool): SessionMessageAssistant["content"][number] {
fixtureMessages = messages, const base = {
) { type: "tool" as const,
id: part.callID,
name: part.tool,
time: {
created: part.state.time.start,
ran: part.state.time.start,
...(part.state.status === "completed" ? { completed: part.state.time.end } : {}),
},
}
if (part.state.status === "running")
return {
...base,
state: {
status: "running",
input: part.state.input as Record<string, JsonValue>,
metadata: part.state.metadata as Record<string, JsonValue>,
},
}
return {
...base,
state: {
status: "completed",
input: part.state.input as Record<string, JsonValue>,
content: [{ type: "text", text: part.state.output }],
metadata: part.state.metadata as Record<string, JsonValue>,
},
}
}
let eventSequence = -1
function toolEvents(part: ContextTool): OpenCodeEvent[] {
return [
eventValue(
"session.tool.success",
{
sessionID,
assistantMessageID: part.messageID,
id: part.callID,
content: [{ type: "text", text: part.state.output }],
metadata: part.state.metadata,
executed: true,
},
2,
),
]
}
function eventValue<Type extends OpenCodeEvent["type"]>(
type: Type,
data: Extract<OpenCodeEvent, { type: Type }>["data"],
version: 1 | 2,
): Extract<OpenCodeEvent, { type: Type }> {
eventSequence++
return {
id: `evt_context_resize_${eventSequence}`,
created: 1700000002000 + eventSequence,
type,
data,
location: { directory },
durable: { aggregateID: sessionID, seq: eventSequence, version },
} as unknown as Extract<OpenCodeEvent, { type: Type }>
}
async function mockServer(page: Page, events: OpenCodeEvent[] = [], fixtureMessages = messages) {
eventSequence = -1
await mockOpenCodeServer(page, { await mockOpenCodeServer(page, {
directory, directory,
project: project(), project: project(),
@@ -372,3 +406,8 @@ function provider() {
function base64Encode(value: string) { function base64Encode(value: string) {
return Buffer.from(value, "utf8").toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "") return Buffer.from(value, "utf8").toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "")
} }
function sessionHref() {
const server = `http://${process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"}:${process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"}`
return `/server/${base64Encode(server)}/session/${sessionID}`
}
@@ -1,30 +1,5 @@
import { expect, test } from "@playwright/test" import { expect, test } from "@playwright/test"
import { import { assistantMessage, setupTimeline, toolPart, userMessage } from "../performance/timeline-stability/fixture"
assistantMessage,
partUpdated,
setupTimeline,
toolPart,
userMessage,
} from "../performance/timeline-stability/fixture"
test("updates edit diagnostics without resetting manual collapse state", async ({ page }) => {
const editID = "prt_diagnostics_edit"
const base = editPart(editID, [])
const timeline = await setupTimeline(page, {
messages: [userMessage(), assistantMessage([base])],
settings: { editToolPartsExpanded: true },
})
const trigger = page.locator(`[data-timeline-part-id="${editID}"] [data-slot="collapsible-trigger"]`).first()
await trigger.click()
await expect(trigger).toHaveAttribute("aria-expanded", "false")
await timeline.send(
partUpdated(editPart(editID, [diagnostic("First failure", 2), diagnostic("Second failure", 4)])),
300,
)
await expect(trigger).toHaveAttribute("aria-expanded", "false")
await timeline.send(partUpdated(editPart(editID, [])), 300)
await expect(trigger).toHaveAttribute("aria-expanded", "false")
})
test("preserves nested patch file state through outer collapse and reopen", async ({ page }) => { test("preserves nested patch file state through outer collapse and reopen", async ({ page }) => {
const patchID = "prt_nested_patch" const patchID = "prt_nested_patch"
@@ -68,25 +43,6 @@ function patchFile(filePath: string, type: "add" | "update" | "delete") {
} }
} }
function editPart(id: string, diagnostics: Record<string, unknown>[]) {
return toolPart(
id,
"edit",
"completed",
{ filePath: "src/edit.ts" },
{
metadata: {
filediff: { file: "src/edit.ts", additions: 1, deletions: 1, before: source(false), after: source(true) },
diagnostics,
},
},
)
}
function diagnostic(message: string, line: number) {
return { message, severity: 1, range: { start: { line, character: 0 }, end: { line, character: 2 } } }
}
function source(changed: boolean) { function source(changed: boolean) {
return Array.from({ length: 12 }, (_, index) => `export const value${index} = ${changed ? index + 1 : index}\n`).join( return Array.from({ length: 12 }, (_, index) => `export const value${index} = ${changed ? index + 1 : index}\n`).join(
"", "",
@@ -1,4 +1,5 @@
import { base64Encode } from "@opencode-ai/core/util/encode" import { base64Encode } from "@opencode-ai/core/util/encode"
import type { SessionMessageAssistant } from "@opencode-ai/client/promise"
import { expect, test, type Page } from "@playwright/test" import { expect, test, type Page } from "@playwright/test"
import { import {
assistantMessage, assistantMessage,
@@ -16,9 +17,9 @@ import { mockOpenCodeServer } from "../utils/mock-server"
import { installSseTransport } from "../utils/sse-transport" import { installSseTransport } from "../utils/sse-transport"
import { expectSessionTitle } from "../utils/waits" import { expectSessionTitle } from "../utils/waits"
const initialPageSize = 20 const messagePageSize = 200
const historyPageSize = 200 const server = `http://${process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"}:${process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"}`
const messages = Array.from({ length: initialPageSize + 1 }, (_, index) => { const messages = Array.from({ length: messagePageSize / 2 + 1 }, (_, index) => {
const id = `msg_${String(index + 1001).padStart(4, "0")}_history_root_user` const id = `msg_${String(index + 1001).padStart(4, "0")}_history_root_user`
return [ return [
userMessage(undefined, { id, created: 1700000000000 + index * 2_000 }), userMessage(undefined, { id, created: 1700000000000 + index * 2_000 }),
@@ -26,23 +27,23 @@ const messages = Array.from({ length: initialPageSize + 1 }, (_, index) => {
id: `msg_${String(index + 1001).padStart(4, "0")}_history_root_assistant`, id: `msg_${String(index + 1001).padStart(4, "0")}_history_root_assistant`,
parentID: id, parentID: id,
created: 1700000001000 + index * 2_000, created: 1700000001000 + index * 2_000,
completed: index < initialPageSize, completed: index < messagePageSize / 2,
}), }),
] ]
}).flat() }).flat()
const assistants = messages.filter((message) => message.info.role === "assistant") const assistants = messages.filter((message): message is SessionMessageAssistant => message.type === "assistant")
const lastAssistant = assistants.at(-1)! const lastAssistant = assistants.at(-1)!
const lastPartID = `${assistants.at(-1)!.info.id}:text:0` const lastPartID = `${assistants.at(-1)!.id}:text:0`
const userPartID = `${messages.at(-2)!.info.id}:text:0` const userPartID = `${messages.at(-2)!.id}:text:0`
const completed = { const completed = {
...lastAssistant.info, ...lastAssistant,
time: { ...lastAssistant.info.time, completed: lastAssistant.info.time.created + 15_000 }, time: { ...lastAssistant.time, completed: lastAssistant.time.created + 15_000 },
} }
const scenarios = [ const scenarios = [
{ name: "completion", info: completed, idleFirst: false, interrupted: false }, { name: "completion", info: completed, idleFirst: false, interrupted: false },
{ {
name: "interruption", name: "interruption",
info: { ...completed, error: { name: "MessageAbortedError", data: { message: "Stopped" } } }, info: { ...completed, error: { type: "MessageAbortedError", message: "Stopped" } },
idleFirst: true, idleFirst: true,
interrupted: true, interrupted: true,
}, },
@@ -57,12 +58,11 @@ for (const scenario of scenarios) {
const roots: { sessionID: string; messageID: string }[] = [] const roots: { sessionID: string; messageID: string }[] = []
const sequence: string[] = [] const sequence: string[] = []
const history = Promise.withResolvers<void>() const history = Promise.withResolvers<void>()
const transport = await installSseTransport<{ directory: string; payload: Record<string, unknown> }>(page, { const transport = await installSseTransport(page, {
server: `http://${process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"}:${process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"}`, server,
retry: 20, retry: 20,
}) })
await mockOpenCodeServer(page, { await mockOpenCodeServer(page, {
protocol: "v2",
directory, directory,
project: project(), project: project(),
provider: { provider: {
@@ -95,15 +95,15 @@ for (const scenario of scenarios) {
}, },
message: (requestedSessionID, messageID) => { message: (requestedSessionID, messageID) => {
if (requestedSessionID !== sessionID) return if (requestedSessionID !== sessionID) return
return messages.find((item) => item.info.id === messageID) return messages.find((item) => item.id === messageID)
}, },
pageMessages: (_, limit, before) => { pageMessages: (_, limit, before) => {
pages.push({ before, limit }) pages.push({ before, limit })
const end = before ? messages.findIndex((message) => message.info.id === before) : messages.length const end = before ? messages.findIndex((message) => message.id === before) : messages.length
const start = Math.max(0, end - limit) const start = Math.max(0, end - limit)
return { return {
items: messages.slice(start, end), items: messages.slice(start, end),
cursor: start > 0 ? messages[start]!.info.id : undefined, cursor: start > 0 ? messages[start]!.id : undefined,
} }
}, },
}) })
@@ -153,28 +153,25 @@ for (const scenario of scenarios) {
requestAnimationFrame(() => setTimeout(sample, 0)) requestAnimationFrame(() => setTimeout(sample, 0))
}) })
await page.goto(`/${base64Encode(directory)}/session/${sessionID}`) await page.goto(`/server/${base64Encode(server)}/session/${sessionID}`)
await transport.waitForConnection() await transport.waitForConnection()
await expectSessionTitle(page, title) await expectSessionTitle(page, title)
await expect(page.locator(`[data-timeline-part-id="${lastPartID}"]`)).toBeVisible() await expect(page.locator(`[data-timeline-part-id="${lastPartID}"]`)).toBeVisible()
await expect(page.locator(`[data-timeline-part-id="${userPartID}"]`)).toBeVisible() await expect(page.locator(`[data-timeline-part-id="${userPartID}"]`)).toBeVisible()
const viewport = page.locator(".scroll-view__viewport", { has: page.locator("[data-timeline-row]") }) const viewport = page.locator(".scroll-view__viewport", { has: page.locator("[data-timeline-row]") })
await viewport.hover() await viewport.hover()
const deadline = Date.now() + 10_000 const deadline = Date.now() + 30_000
while (requests.filter((request) => request.phase === "start").length < 2) { while (requests.filter((request) => request.phase === "start").length < 2) {
if (Date.now() >= deadline) throw new Error("Timed out scrolling to the history boundary") if (Date.now() >= deadline) throw new Error("Timed out scrolling to the history boundary")
await page.mouse.wheel(0, -240) await page.mouse.wheel(0, -1_200)
await page.waitForTimeout(20) await page.waitForTimeout(20)
} }
expect(requests.filter((request) => request.phase === "end")).toHaveLength(1) expect(requests.filter((request) => request.phase === "end")).toHaveLength(1)
expect(sequence.slice(0, 3)).toEqual([ expect(sequence.slice(0, 3)).toEqual([
"messages:start:latest", "messages:start:latest",
"messages:end:latest", "messages:end:latest",
`messages:start:${messages.at(-initialPageSize)!.info.id}`, `messages:start:${messages.at(-messagePageSize)!.id}`,
]) ])
await expect(page.locator('[data-timeline-part-id*="_history_root_assistant:text:0"]')).toHaveCount(
initialPageSize / 2,
)
await page.evaluate(() => { await page.evaluate(() => {
;( ;(
window as Window & { window as Window & {
@@ -186,15 +183,12 @@ for (const scenario of scenarios) {
expect(await visibleContentHidden(page)).toBe(false) expect(await visibleContentHidden(page)).toBe(false)
const beforeHistory = await probeSamples(page) const beforeHistory = await probeSamples(page)
history.resolve() history.resolve()
await expect
.poll(() => page.locator('[data-timeline-part-id*="_history_root_assistant:text:0"]').count())
.toBeGreaterThan(initialPageSize / 2)
await expect.poll(() => requests.filter((request) => request.phase === "end").length).toBe(2) await expect.poll(() => requests.filter((request) => request.phase === "end").length).toBe(2)
await expect(page.getByRole("button", { name: "Stop" })).toBeVisible() await expect(page.getByRole("button", { name: "Stop" })).toBeVisible()
await waitForProbeSamples(page, beforeHistory) await waitForProbeSamples(page, beforeHistory)
expect(pages).toEqual([ expect(pages).toEqual([
{ before: undefined, limit: initialPageSize }, { before: undefined, limit: messagePageSize },
{ before: messages.at(-initialPageSize)!.info.id, limit: historyPageSize }, { before: messages.at(-messagePageSize)!.id, limit: messagePageSize },
]) ])
expect(roots).toEqual([]) expect(roots).toEqual([])
@@ -5,6 +5,7 @@ import {
messageUpdated, messageUpdated,
partUpdated, partUpdated,
reasoningPart, reasoningPart,
renderedPartID,
setupTimeline, setupTimeline,
shell, shell,
status, status,
@@ -65,7 +66,7 @@ test("transitions thinking and hidden reasoning through busy to idle", async ({
await timeline.send(partUpdated(shell("prt_reasoning_shell", "running")), 160) await timeline.send(partUpdated(shell("prt_reasoning_shell", "running")), 160)
await expect(page.locator('[data-timeline-row="Thinking"]')).toBeVisible() await expect(page.locator('[data-timeline-row="Thinking"]')).toBeVisible()
await timeline.send(partUpdated(shell("prt_reasoning_shell", "completed", "done")), 180) await timeline.send(partUpdated(shell("prt_reasoning_shell", "completed", "done")), 180)
await timeline.send(messageUpdated(completedAssistantInfo(assistant.info)), 100) await timeline.send(messageUpdated(completedAssistantInfo(assistant)), 100)
await timeline.send(status("idle"), 300) await timeline.send(status("idle"), 300)
await expect(page.locator('[data-timeline-row="Thinking"]')).toHaveCount(0) await expect(page.locator('[data-timeline-row="Thinking"]')).toHaveCount(0)
await expect(page.locator(`[data-timeline-part-id="${reasoningID}"]`)).toHaveCount(0) await expect(page.locator(`[data-timeline-part-id="${reasoningID}"]`)).toHaveCount(0)
@@ -82,6 +83,7 @@ test("moves busy through retry and recovery to final idle content", async ({ pag
file: "src/retry.ts", file: "src/retry.ts",
additions: 1, additions: 1,
deletions: 1, deletions: 1,
status: "modified",
patch: "@@ -1 +1 @@\n-export const retry = false\n+export const retry = true", patch: "@@ -1 +1 @@\n-export const retry = false\n+export const retry = true",
}, },
], ],
@@ -94,16 +96,16 @@ test("moves busy through retry and recovery to final idle content", async ({ pag
await expect(page.locator('[data-timeline-row="Thinking"]')).toBeVisible() await expect(page.locator('[data-timeline-row="Thinking"]')).toBeVisible()
await expect(page.locator('[data-timeline-row="DiffSummary"]')).toHaveCount(0) await expect(page.locator('[data-timeline-row="DiffSummary"]')).toHaveCount(0)
await timeline.send(status("retry"), 180) await timeline.send(status("retry"), 180)
await expect(page.locator('[data-timeline-row="Retry"]')).toBeVisible() await expect(page.locator('[data-timeline-row="Thinking"]')).toBeVisible()
await expect(page.locator('[data-timeline-row="Thinking"]')).toHaveCount(0)
await timeline.send(status("busy", 2), 180) await timeline.send(status("busy", 2), 180)
await expect(page.locator('[data-timeline-row="Thinking"]')).toBeVisible() await expect(page.locator('[data-timeline-row="Thinking"]')).toBeVisible()
await timeline.send(partUpdated(textPart("prt_recovered", "Recovered response")), 140) await timeline.send(partUpdated(textPart("prt_recovered", "Recovered response")), 140)
await timeline.send(messageUpdated(completedAssistantInfo(assistant.info)), 100) await timeline.send(messageUpdated(completedAssistantInfo(assistant)), 100)
await timeline.send(status("idle"), 350) await timeline.send(status("idle"), 350)
await expect(page.locator('[data-timeline-row="Retry"]')).toHaveCount(0)
await expect(page.locator('[data-timeline-row="Thinking"]')).toHaveCount(0) await expect(page.locator('[data-timeline-row="Thinking"]')).toHaveCount(0)
await expect(page.locator('[data-timeline-part-id="prt_recovered"]')).toContainText("Recovered response") await expect(page.locator(`[data-timeline-part-id="${renderedPartID("prt_recovered")}"]`)).toContainText(
"Recovered response",
)
}) })
function lines(count: number) { function lines(count: number) {
@@ -1,28 +1,36 @@
import { expect, test } from "@playwright/test" import { expect, test } from "@playwright/test"
import type { SessionMessageInfo } from "@opencode-ai/client/promise" import type { SessionMessageAssistant, SessionMessageInfo } from "@opencode-ai/client/promise"
import { session, sessionID, setupTimeline } from "../performance/timeline-stability/fixture" import { event, session, sessionID, setupTimeline } from "../performance/timeline-stability/fixture"
const user = { id: "msg_user", type: "user", text: "Run it", time: { created: 1 } } satisfies SessionMessageInfo const user = { id: "msg_user", type: "user", text: "Run it", time: { created: 1 } } satisfies SessionMessageInfo
const assistant = (completed: boolean, tool = false, childID?: string) => const assistant = (
({ completed: boolean,
id: "msg_assistant", tool = false,
type: "assistant", childID?: string,
agent: "build", background = false,
model: { id: "model", providerID: "provider" }, ): SessionMessageAssistant => ({
content: tool id: "msg_assistant",
? [ type: "assistant",
{ agent: "build",
type: "tool", model: { id: "model", providerID: "provider" },
id: "call_subagent", content: tool
name: "subagent", ? [
state: { status: "running", input: {}, metadata: childID ? { sessionID: childID } : {} }, {
time: { created: 2 }, type: "tool",
id: "call_subagent",
name: "subagent",
state: {
status: "running",
input: { description: "Inspect code", ...(background ? { background: true } : {}) },
metadata: { status: "running", ...(childID ? { sessionID: childID } : {}) },
}, },
] time: { created: 2 },
: [{ type: "text", text: "Working" }], },
time: { created: 2, ...(completed ? { completed: 3 } : {}) }, ]
}) satisfies SessionMessageInfo : [{ type: "text", text: "Working" }],
time: { created: 2, ...(completed ? { completed: 3 } : {}) },
})
test("renders current protocol notices in CLI order", async ({ page }) => { test("renders current protocol notices in CLI order", async ({ page }) => {
const ownerWarnings: string[] = [] const ownerWarnings: string[] = []
@@ -31,7 +39,7 @@ test("renders current protocol notices in CLI order", async ({ page }) => {
ownerWarnings.push(message.text()) ownerWarnings.push(message.text())
}) })
await setupTimeline(page, { await setupTimeline(page, {
currentMessages: [ sessionMessages: [
user, user,
{ id: "msg_agent", type: "agent-switched", agent: "explore", time: { created: 2 } }, { id: "msg_agent", type: "agent-switched", agent: "explore", time: { created: 2 } },
assistant(true), assistant(true),
@@ -60,12 +68,17 @@ test("renders current protocol notices in CLI order", async ({ page }) => {
await expect(notices.nth(1)).toContainText("explore finished · Search code") await expect(notices.nth(1)).toContainText("explore finished · Search code")
await expect(notices.nth(2)).toContainText("Continuing after restart") await expect(notices.nth(2)).toContainText("Continuing after restart")
await expect(notices.nth(3)).toContainText("Skill · Review") await expect(notices.nth(3)).toContainText("Skill · Review")
await expect(notices).toHaveClass([/text-text-weak/, /text-text-weak/, /text-text-weak/, /text-text-weak/])
await expect(notices.locator(".text-text-strong")).toHaveCount(0)
expect(ownerWarnings).toEqual([]) expect(ownerWarnings).toEqual([])
}) })
test("moves blocking work to the background with Ctrl+B", async ({ page }) => { test("moves blocking work to the background with Ctrl+B", async ({ page }) => {
await setupTimeline(page, { currentMessages: [user, assistant(false, true)] }) await setupTimeline(page, { sessionMessages: [user, assistant(false, true)] })
await expect(page.locator('[data-component="task-tool-card"]')).toBeVisible() const card = page.locator('[data-component="task-tool-card"]')
await expect(card).toBeVisible()
await expect(card).toContainText("Inspect code")
await expect(card).not.toContainText("(background)")
await expect(page.getByText("Called `subagent`", { exact: false })).toHaveCount(0) await expect(page.getByText("Called `subagent`", { exact: false })).toHaveCount(0)
await expect(page.locator('[data-component="background-tool-control"]')).toHaveCount(0) await expect(page.locator('[data-component="background-tool-control"]')).toHaveCount(0)
await expect(page.locator('[data-action="session-background-toggle"]')).toContainText("Move 1 subagent to background") await expect(page.locator('[data-action="session-background-toggle"]')).toContainText("Move 1 subagent to background")
@@ -78,10 +91,15 @@ test("moves blocking work to the background with Ctrl+B", async ({ page }) => {
await request await request
}) })
test("waits for completion before labeling requested background work", async ({ page }) => {
await setupTimeline(page, { sessionMessages: [user, assistant(false, true, undefined, true)] })
await expect(page.locator('[data-component="task-tool-card"]')).not.toContainText("(background)")
})
test("navigates from a running subagent card and hides background controls in the child", async ({ page }) => { test("navigates from a running subagent card and hides background controls in the child", async ({ page }) => {
const childID = "ses_running_child" const childID = "ses_running_child"
await setupTimeline(page, { await setupTimeline(page, {
currentMessages: [user, assistant(false, true, childID)], sessionMessages: [user, assistant(false, true, childID)],
sessions: [session(), session({ id: childID, parentID: sessionID, title: "Sleep for 5 minutes" })], sessions: [session(), session({ id: childID, parentID: sessionID, title: "Sleep for 5 minutes" })],
sessionStatus: { [sessionID]: { type: "busy" }, [childID]: { type: "busy" } }, sessionStatus: { [sessionID]: { type: "busy" }, [childID]: { type: "busy" } },
}) })
@@ -95,7 +113,7 @@ test("navigates from a running subagent card and hides background controls in th
test("shows a badge for active background work", async ({ page }) => { test("shows a badge for active background work", async ({ page }) => {
const childID = "ses_background_child" const childID = "ses_background_child"
await setupTimeline(page, { await setupTimeline(page, {
currentMessages: [user, assistant(true)], sessionMessages: [user, assistant(true)],
sessions: [session(), session({ id: childID, parentID: sessionID })], sessions: [session(), session({ id: childID, parentID: sessionID })],
sessionStatus: { [childID]: { type: "busy" } }, sessionStatus: { [childID]: { type: "busy" } },
}) })
@@ -106,8 +124,8 @@ test("shows a badge for active background work", async ({ page }) => {
test("separates blocking and already-backgrounded work into two rows", async ({ page }) => { test("separates blocking and already-backgrounded work into two rows", async ({ page }) => {
const backgroundID = "ses_background_existing" const backgroundID = "ses_background_existing"
const blockingID = "ses_background_blocking" const blockingID = "ses_background_blocking"
await setupTimeline(page, { const timeline = await setupTimeline(page, {
currentMessages: [ sessionMessages: [
user, user,
{ {
id: "msg_backgrounded", id: "msg_backgrounded",
@@ -176,9 +194,21 @@ test("separates blocking and already-backgrounded work into two rows", async ({
}) })
const dock = page.locator('[data-component="session-background-dock"]') const dock = page.locator('[data-component="session-background-dock"]')
const backgroundCard = page.locator('[data-timeline-part-id="call_backgrounded"]')
await expect(dock).toContainText("Move 1 subagent to background") await expect(dock).toContainText("Move 1 subagent to background")
await expect(dock.getByText("Running 1 shell and 1 subagent in background", { exact: true })).toBeVisible() await expect(dock.getByText("Running 1 shell and 1 subagent in background", { exact: true })).toBeVisible()
await expect(backgroundCard).toContainText("Background task (background)")
await expect(backgroundCard.locator('[data-component="session-progress-indicator-v2"]')).toBeVisible()
await expect( await expect(
page.locator('[data-timeline-part-id="call_shell_backgrounded"] [data-component="text-shimmer"]'), page.locator('[data-timeline-part-id="call_shell_backgrounded"] [data-component="text-shimmer"]'),
).toHaveAttribute("data-active", "true") ).toHaveAttribute("data-active", "true")
await timeline.transport.send({
id: "evt_background_succeeded",
created: Date.now(),
type: "session.execution.succeeded",
data: { sessionID: backgroundID },
} as never)
await expect(backgroundCard.locator('[data-component="session-progress-indicator-v2"]')).toHaveCount(0)
await expect(backgroundCard).toContainText("Background task (background)")
}) })
@@ -71,33 +71,20 @@ test.describe("session timeline projection", () => {
test("projects gaps, dividers, assistant parts, and errors together", async ({ page }) => { test("projects gaps, dividers, assistant parts, and errors together", async ({ page }) => {
const firstUser = userMessage( const firstUser = userMessage(
[ [
userText("The user made the following comment regarding lines 4 through 8 of src/a.ts: Keep this stable", { userText("Keep this stable", { id: "prt_comment" }),
id: "prt_comment",
synthetic: true,
metadata: {
opencodeComment: {
path: "src/a.ts",
selection: { startLine: 4, startChar: 0, endLine: 8, endChar: 0 },
comment: "Keep this stable",
},
},
}),
userText("Continue after the comment", { id: "prt_visible_user" }), userText("Continue after the comment", { id: "prt_visible_user" }),
], ],
{ summary: { diffs: Array.from({ length: 11 }, (_, index) => summaryDiff(index)) } }, { summary: { diffs: Array.from({ length: 11 }, (_, index) => summaryDiff(index)) } },
) )
const aborted = assistantMessage([{ id: "prt_before_abort", type: "text", text: "Before interruption" }], { const aborted = assistantMessage([{ id: "prt_before_abort", type: "text", text: "Before interruption" }], {
id: "msg_1001_assistant_aborted", id: "msg_1001_assistant_aborted",
error: { name: "MessageAbortedError", data: { message: "Stopped" } }, error: { type: "MessageAbortedError", message: "Stopped" },
}) })
const failed = assistantMessage([{ id: "prt_after_abort", type: "text", text: "After interruption" }], { const failed = assistantMessage([{ id: "prt_after_abort", type: "text", text: "After interruption" }], {
id: "msg_1002_assistant_failed", id: "msg_1002_assistant_failed",
error: { error: {
name: "APIError", type: "APIError",
data: { message: "Visible provider failure",
message: JSON.stringify({ error: { type: "provider_error", message: "Visible provider failure" } }),
isRetryable: false,
},
}, },
created: 1700000003000, created: 1700000003000,
}) })
@@ -122,52 +109,11 @@ test.describe("session timeline projection", () => {
await expect(page.locator('[data-timeline-row="TurnGap"]')).toBeVisible() await expect(page.locator('[data-timeline-row="TurnGap"]')).toBeVisible()
}) })
test("renders legacy synthetic comments as ordinary V2 user text", async ({ page }) => {
const user = userMessage(
[
userText("The user made the following comment regarding lines 4 through 8 of src/a.ts: Keep this stable", {
id: "prt_comment_only",
synthetic: true,
metadata: {
opencodeComment: {
path: "src/a.ts",
selection: { startLine: 4, startChar: 0, endLine: 8, endChar: 0 },
comment: "Keep this stable",
},
},
}),
userText("Continue after the comment", { id: "prt_comment_visible" }),
],
{ summary: { diffs: Array.from({ length: 11 }, (_, index) => summaryDiff(index)) } },
)
const nextUser = userMessage(undefined, { id: "msg_2000_diff_next_user", created: 1700000010000 })
const nextAssistant = assistantMessage([], {
id: "msg_2001_diff_next_assistant",
parentID: "msg_2000_diff_next_user",
created: 1700000011000,
})
await setupTimeline(page, {
messages: [user, assistantMessage(), nextUser, nextAssistant],
settings: { newLayoutDesigns: false },
})
const scroller = page.locator(".scroll-view__viewport", { has: page.locator("[data-timeline-row]") })
await scroller.evaluate((element) => (element.scrollTop = 0))
await expect(
page.getByText(
"The user made the following comment regarding lines 4 through 8 of src/a.ts: Keep this stable Continue after the comment",
{ exact: true },
),
).toBeVisible()
await expect(page.locator('[data-timeline-row="CommentStrip"]')).toHaveCount(0)
await expect(page.locator('[data-timeline-row="DiffSummary"]')).toHaveCount(0)
})
test("renders interruption independently when the turn is not compacted", async ({ page }) => { test("renders interruption independently when the turn is not compacted", async ({ page }) => {
const user = userMessage() const user = userMessage()
const before = assistantMessage([{ id: "prt_before", type: "text", text: "Before" }], { const before = assistantMessage([{ id: "prt_before", type: "text", text: "Before" }], {
id: "msg_1001_before", id: "msg_1001_before",
error: { name: "MessageAbortedError", data: { message: "Stopped" } }, error: { type: "MessageAbortedError", message: "Stopped" },
}) })
const after = assistantMessage([{ id: "prt_after", type: "text", text: "After" }], { const after = assistantMessage([{ id: "prt_after", type: "text", text: "After" }], {
id: "msg_1002_after", id: "msg_1002_after",
@@ -280,6 +226,7 @@ function summaryDiff(index: number) {
file: `src/diff-${index}.ts`, file: `src/diff-${index}.ts`,
additions: 1, additions: 1,
deletions: 1, deletions: 1,
status: "modified" as const,
patch: `@@ -1 +1 @@\n-export const value = ${index}\n+export const value = ${index + 1}`, patch: `@@ -1 +1 @@\n-export const value = ${index}\n+export const value = ${index + 1}`,
} }
} }
@@ -4,6 +4,7 @@ import {
completedAssistantInfo, completedAssistantInfo,
messageUpdated, messageUpdated,
partUpdated, partUpdated,
renderedPartID,
setupTimeline, setupTimeline,
shell, shell,
status, status,
@@ -23,11 +24,10 @@ test("groups singleton and separated context operations at correct boundaries",
] ]
await setupTimeline(page, { messages: [userMessage(), assistantMessage(parts)] }) await setupTimeline(page, { messages: [userMessage(), assistantMessage(parts)] })
await expect( await expect(page.locator('[data-timeline-part-ids="prt_boundary_01_read"]')).toBeVisible()
page.locator('[data-timeline-part-ids="prt_boundary_01_read,prt_boundary_03_glob,prt_boundary_04_grep"]'), await expect(page.locator('[data-timeline-part-ids="prt_boundary_03_glob,prt_boundary_04_grep"]')).toBeVisible()
).toBeVisible()
await expect(page.locator('[data-timeline-part-ids="prt_boundary_06_list"]')).toBeVisible() await expect(page.locator('[data-timeline-part-ids="prt_boundary_06_list"]')).toBeVisible()
await expect(page.locator('[data-timeline-row="AssistantPart"]')).toHaveCount(4) await expect(page.locator('[data-timeline-row="AssistantPart"]')).toHaveCount(5)
}) })
test("reducer-hardening: converges when idle arrives before final part and message completion", async ({ page }) => { test("reducer-hardening: converges when idle arrives before final part and message completion", async ({ page }) => {
@@ -37,8 +37,10 @@ test("reducer-hardening: converges when idle arrives before final part and messa
await timeline.send(status("busy"), 100) await timeline.send(status("busy"), 100)
await timeline.send(status("idle"), 100) await timeline.send(status("idle"), 100)
await timeline.send(partUpdated(textPart(textID, "Final after early idle")), 120) await timeline.send(partUpdated(textPart(textID, "Final after early idle")), 120)
await timeline.send(messageUpdated(completedAssistantInfo(assistant.info)), 250) await timeline.send(messageUpdated(completedAssistantInfo(assistant)), 250)
await expect(page.locator('[data-timeline-row="Thinking"]')).toHaveCount(0) await expect(page.locator('[data-timeline-row="Thinking"]')).toHaveCount(0)
await expect(page.locator(`[data-timeline-part-id="${textID}"]`)).toContainText("Final after early idle") await expect(page.locator(`[data-timeline-part-id="${renderedPartID(textID)}"]`)).toContainText(
"Final after early idle",
)
}) })
@@ -13,7 +13,7 @@ for (const deviceScaleFactor of [1.25, 1.5]) {
const shellID = "prt_shell_outline" const shellID = "prt_shell_outline"
const timeline = await setupTimeline(page, { const timeline = await setupTimeline(page, {
messages: [userMessage(), assistantMessage([shell(shellID, "completed", "shell output")])], messages: [userMessage(), assistantMessage([shell(shellID, "completed", "shell output")])],
settings: { newLayoutDesigns: true, shellToolPartsExpanded: true }, settings: { shellToolPartsExpanded: true },
reducedMotion: true, reducedMotion: true,
deviceScaleFactor, deviceScaleFactor,
}) })
@@ -82,7 +82,7 @@ test("keeps the patch card inside a fractionally short virtual row", async ({ pa
toolPart(patchID, "apply_patch", "completed", { files: [file.filePath] }, { metadata: { files: [file] } }), toolPart(patchID, "apply_patch", "completed", { files: [file.filePath] }, { metadata: { files: [file] } }),
]), ]),
], ],
settings: { editToolPartsExpanded: true, newLayoutDesigns: true }, settings: { editToolPartsExpanded: true },
reducedMotion: true, reducedMotion: true,
}) })
const part = page.locator(`[data-timeline-part-id="${patchID}"]`) const part = page.locator(`[data-timeline-part-id="${patchID}"]`)
@@ -131,6 +131,7 @@ test("allows paint rounding for every framed row but not fixed turn gaps", async
file: "src/summary.ts", file: "src/summary.ts",
additions: 1, additions: 1,
deletions: 1, deletions: 1,
status: "modified",
patch: "@@ -1 +1 @@\n-export const value = 1\n+export const value = 2", patch: "@@ -1 +1 @@\n-export const value = 1\n+export const value = 2",
}, },
], ],
@@ -83,6 +83,66 @@ test("labels all web search provider variants", async ({ page }) => {
await expect(page.getByRole("button", { name: /^Web Search/ })).toBeVisible() await expect(page.getByRole("button", { name: /^Web Search/ })).toBeVisible()
}) })
test("labels completed searches with result counts", async ({ page }) => {
const glob = "prt_glob_count"
const grep = "prt_grep_count"
await setupTimeline(page, {
messages: [
userMessage(),
assistantMessage([
toolPart(glob, "glob", "completed", { path: ".", pattern: "**/*.ts" }, { metadata: { count: 1 } }),
toolPart(grep, "grep", "completed", { path: ".", pattern: "value" }, { metadata: { matches: 12 } }),
]),
],
})
const group = page.locator(`[data-timeline-part-ids="${glob},${grep}"]`)
await group.locator('[data-slot="collapsible-trigger"]').click()
const rows = group.locator('[data-component="tool-trigger"]')
await expect(rows.nth(0)).toContainText("(1 match)")
await expect(rows.nth(1)).toContainText("(12 matches)")
})
test("labels V2 read tools from their path input", async ({ page }) => {
const id = "prt_read_path"
await setupTimeline(page, {
messages: [userMessage(), assistantMessage([toolPart(id, "read", "completed", { path: "src/a.ts" })])],
})
const group = page.locator(`[data-timeline-part-ids="${id}"]`)
await group.locator('[data-slot="collapsible-trigger"]').click()
await expect(group.locator('[data-slot="basic-tool-tool-subtitle"]')).toHaveText("a.ts")
})
test("labels V2 skill tools from IDs and result metadata", async ({ page }) => {
const pending = "prt_skill_id"
const completed = "prt_skill_name"
await setupTimeline(page, {
messages: [
userMessage(),
assistantMessage([
toolPart(pending, "skill", "running", { id: "sample-skill" }),
toolPart(completed, "skill", "completed", { id: "opencode" }, { metadata: { name: "OpenCode" } }),
]),
],
})
await expect(page.locator(`[data-timeline-part-id="${pending}"] [data-component="text-shimmer"]`)).toHaveAttribute(
"aria-label",
"sample-skill",
)
await expect(page.locator(`[data-timeline-part-id="${completed}"] [data-component="text-shimmer"]`)).toHaveAttribute(
"aria-label",
"OpenCode",
)
for (const id of [pending, completed]) {
const skill = page.locator(`[data-timeline-part-id="${id}"]`)
await expect(skill.locator('[data-slot="skill-tool-label"]')).toHaveText("Skill")
await expect(skill.locator('[data-slot="skill-tool-separator"]')).toHaveText("·")
await expect(skill.locator('use[href="#opencode-icon-post-skill"]')).toBeVisible()
}
})
function questionInput() { function questionInput() {
return { questions: [{ header: "Stability", question: "Keep it stable?", options: [] }] } return { questions: [{ header: "Stability", question: "Keep it stable?", options: [] }] }
} }
@@ -1,77 +0,0 @@
import { expect, test } from "@playwright/test"
import {
assistantMessage,
partUpdated,
setupTimeline,
toolPart,
userMessage,
} from "../performance/timeline-stability/fixture"
test("updates expanded web search links without resetting expansion", async ({ page }) => {
const searchID = "prt_websearch_mutation"
const input = { query: "timeline stability" }
const timeline = await setupTimeline(page, {
messages: [
userMessage(),
assistantMessage([toolPart(searchID, "websearch", "completed", input, { output: "https://example.com/one" })]),
],
})
const wrapper = page.locator(`[data-timeline-part-id="${searchID}"]`)
const trigger = wrapper.locator('[data-slot="collapsible-trigger"]')
await trigger.click()
await expect(trigger).toHaveAttribute("aria-expanded", "true")
await timeline.send(
partUpdated(
toolPart(searchID, "websearch", "completed", input, {
output: "https://example.com/one\nhttps://example.com/two",
}),
),
300,
)
await expect(trigger).toHaveAttribute("aria-expanded", "true")
await expect(wrapper.locator('a[href="https://example.com/two"]')).toBeVisible()
})
test("preserves an expanded tool error card across duplicate delivery", async ({ page }) => {
const toolID = "prt_duplicate_error"
const failed = toolPart(toolID, "bash", "error", { command: "exit 1" }, { error: "Command failed visibly" })
const timeline = await setupTimeline(page, { messages: [userMessage(), assistantMessage([failed])] })
const wrapper = page.locator(`[data-timeline-part-id="${toolID}"]`)
const trigger = wrapper.locator('[data-slot="collapsible-trigger"]')
await trigger.click()
await expect(trigger).toHaveAttribute("aria-expanded", "true")
await timeline.send(partUpdated(failed), 150)
await timeline.send(partUpdated(failed), 250)
await expect(trigger).toHaveAttribute("aria-expanded", "true")
await expect(wrapper).toContainText("Command failed visibly")
})
test("renders multiple question answers and preserves open state on answer updates", async ({ page }) => {
const questionID = "prt_multi_question"
const input = {
questions: [
{ header: "First", question: "First choice?", options: [] },
{ header: "Second", question: "Second choice?", options: [], multiple: true },
],
}
const timeline = await setupTimeline(page, {
messages: [
userMessage(),
assistantMessage([
toolPart(questionID, "question", "completed", input, { metadata: { answers: [["A"], ["B", "C"]] } }),
]),
],
})
const wrapper = page.locator(`[data-timeline-part-id="${questionID}"]`)
const trigger = wrapper.locator('[data-slot="collapsible-trigger"]')
await expect(trigger).toHaveAttribute("aria-expanded", "true")
await timeline.send(
partUpdated(
toolPart(questionID, "question", "completed", input, { metadata: { answers: [["Updated"], ["B", "C"]] } }),
),
300,
)
await expect(trigger).toHaveAttribute("aria-expanded", "true")
await expect(wrapper).toContainText("Updated")
await expect(wrapper).toContainText("B, C")
})
@@ -1,43 +1,44 @@
import { expect, test, type Page } from "@playwright/test" import { expect, test, type Page } from "@playwright/test"
import { partUpdated, setupTimeline, textPart } from "../performance/timeline-stability/fixture" import { partUpdated, renderedPartID, setupTimeline, textPart } from "../performance/timeline-stability/fixture"
test("keeps one connection open while delivering multiple events", async ({ page }) => { test("keeps one connection open while delivering multiple events", async ({ page }) => {
const timeline = await setupTimeline(page) const timeline = await setupTimeline(page)
const first = await timeline.transport.send(partUpdated(textPart("prt_transport_first", "first event"))) const first = (await timeline.transport.burst(partUpdated(textPart("prt_transport_first", "first event")))).at(-1)!
const second = await timeline.transport.send(partUpdated(textPart("prt_transport_second", "second event"))) const second = (await timeline.transport.burst(partUpdated(textPart("prt_transport_second", "second event")))).at(-1)!
await timeline.waitForPart("prt_transport_first") await timeline.waitForPart("prt_transport_first")
await timeline.waitForPart("prt_transport_second") await timeline.waitForPart("prt_transport_second")
expect(first.connectionID).toBe(second.connectionID) expect(first.connectionID).toBe(second.connectionID)
await expect.poll(async () => (await timeline.transport.connections()).length).toBe(1) await expect.poll(async () => (await timeline.transport.connections()).length).toBe(1)
expect(await timeline.transport.acknowledgements()).toHaveLength(2) expect(await timeline.transport.acknowledgements()).toHaveLength(4)
}) })
test("delivers a burst from one stream chunk", async ({ page }) => { test("delivers a burst from one stream chunk", async ({ page }) => {
const timeline = await setupTimeline(page) const timeline = await setupTimeline(page)
const acknowledgements = await timeline.transport.burst([ const acknowledgements = await timeline.transport.burst([
partUpdated(textPart("prt_transport_burst_a", "burst a")), ...partUpdated(textPart("prt_transport_burst_a", "burst a")),
partUpdated(textPart("prt_transport_burst_b", "burst b")), ...partUpdated(textPart("prt_transport_burst_b", "burst b")),
]) ])
await timeline.waitForPart("prt_transport_burst_a") await timeline.waitForPart("prt_transport_burst_a")
await timeline.waitForPart("prt_transport_burst_b") await timeline.waitForPart("prt_transport_burst_b")
expect(acknowledgements.map((item) => item.chunkCount)).toEqual([1, 1]) expect(acknowledgements.map((item) => item.chunkCount)).toEqual([1, 1, 1, 1])
expect(new Set(acknowledgements.map((item) => item.deliveryID)).size).toBe(2) expect(new Set(acknowledgements.map((item) => item.deliveryID)).size).toBe(4)
}) })
test("parses split JSON and a split multibyte code point", async ({ page }) => { test("parses split JSON and a split multibyte code point", async ({ page }) => {
const timeline = await setupTimeline(page) const timeline = await setupTimeline(page)
const payload = partUpdated(textPart("prt_transport_split", "split snowman \u2603\u2603\u2603")) const [started, payload] = partUpdated(textPart("prt_transport_split", "split snowman \u2603\u2603\u2603"))
await timeline.transport.send(started!)
const encoded = new TextEncoder().encode(`data: ${JSON.stringify(payload)}\n\n`) const encoded = new TextEncoder().encode(`data: ${JSON.stringify(payload)}\n\n`)
const snowman = new TextEncoder().encode("\u2603")[0]! const snowman = new TextEncoder().encode("\u2603")[0]!
const multibyte = encoded.indexOf(snowman) const multibyte = encoded.indexOf(snowman)
const acknowledgement = await timeline.transport.split(payload, [9, multibyte + 1, multibyte + 2]) const acknowledgement = await timeline.transport.split(payload!, [9, multibyte + 1, multibyte + 2])
await timeline.waitForPart("prt_transport_split") await timeline.waitForPart("prt_transport_split")
await expect(page.locator('[data-timeline-part-id="prt_transport_split"]')).toContainText( await expect(page.locator(`[data-timeline-part-id="${renderedPartID("prt_transport_split")}"]`)).toContainText(
"split snowman \u2603\u2603\u2603", "split snowman \u2603\u2603\u2603",
) )
expect(acknowledgement.chunkCount).toBe(4) expect(acknowledgement.chunkCount).toBe(4)
@@ -46,12 +47,11 @@ test("parses split JSON and a split multibyte code point", async ({ page }) => {
test("delivers server heartbeat without mutating the timeline", async ({ page }) => { test("delivers server heartbeat without mutating the timeline", async ({ page }) => {
const timeline = await setupTimeline(page) const timeline = await setupTimeline(page)
const partID = "prt_transport_heartbeat_sentinel" const partID = "prt_transport_heartbeat_sentinel"
const sentinel = await timeline.transport.send(partUpdated(textPart(partID, "heartbeat sentinel"))) const sentinel = (await timeline.transport.burst(partUpdated(textPart(partID, "heartbeat sentinel")))).at(-1)!
await timeline.waitForPart(partID) await timeline.waitForPart(partID)
await expect(page.locator(`[data-timeline-part-id="${partID}"] [data-component="markdown"]`)).toHaveAttribute( await expect(
"data-markdown-ready", page.locator(`[data-timeline-part-id="${renderedPartID(partID)}"] [data-component="markdown"]`),
"", ).toHaveAttribute("data-markdown-ready", "")
)
const before = await timelineRows(page) const before = await timelineRows(page)
const heartbeat = await timeline.transport.heartbeat() const heartbeat = await timeline.transport.heartbeat()
@@ -66,7 +66,7 @@ test("reconnects after a clean close", async ({ page }) => {
await timeline.transport.close() await timeline.transport.close()
const second = await timeline.transport.waitForConnection({ after: first.id }) const second = await timeline.transport.waitForConnection({ after: first.id })
await timeline.transport.send(partUpdated(textPart("prt_transport_close", "after close"))) await timeline.transport.burst(partUpdated(textPart("prt_transport_close", "after close")))
await timeline.waitForPart("prt_transport_close") await timeline.waitForPart("prt_transport_close")
expect(second.id).toBeGreaterThan(first.id) expect(second.id).toBeGreaterThan(first.id)
@@ -79,7 +79,7 @@ test("reconnects after a stream error", async ({ page }) => {
await timeline.transport.error("contract failure") await timeline.transport.error("contract failure")
const second = await timeline.transport.waitForConnection({ after: first.id }) const second = await timeline.transport.waitForConnection({ after: first.id })
await timeline.transport.send(partUpdated(textPart("prt_transport_error", "after error"))) await timeline.transport.burst(partUpdated(textPart("prt_transport_error", "after error")))
await timeline.waitForPart("prt_transport_error") await timeline.waitForPart("prt_transport_error")
await expect.poll(async () => (await timeline.transport.connections()).length).toBe(2) await expect.poll(async () => (await timeline.transport.connections()).length).toBe(2)
@@ -89,9 +89,13 @@ test("reconnects after a stream error", async ({ page }) => {
test("does not request replay when reconnecting the volatile V2 event stream", async ({ page }) => { test("does not request replay when reconnecting the volatile V2 event stream", async ({ page }) => {
const timeline = await setupTimeline(page, { eventRetry: 10 }) const timeline = await setupTimeline(page, { eventRetry: 10 })
const first = await timeline.transport.send(partUpdated(textPart("prt_transport_id", "event with id")), { const events = partUpdated(textPart("prt_transport_id", "event with id"))
id: "timeline-event-7", const first = (
}) await timeline.transport.burst(
events,
events.map((_, index) => (index === events.length - 1 ? { id: "timeline-event-7" } : {})),
)
).at(-1)!
await timeline.waitForPart("prt_transport_id") await timeline.waitForPart("prt_transport_id")
await timeline.transport.error("retry with event id") await timeline.transport.error("retry with event id")
@@ -1,170 +0,0 @@
import { base64Encode } from "@opencode-ai/core/util/encode"
import { expect, test, type Page } from "@playwright/test"
import { mockOpenCodeServer } from "../utils/mock-server"
import { expectSessionTitle } from "../utils/waits"
const directory = "C:/OpenCode/TodoDockNavigation"
const projectID = "proj_todo_dock_navigation"
const sourceID = "ses_todo_dock_source"
const otherID = "ses_todo_dock_other"
const sourceTitle = "Todo dock animation"
const otherTitle = "Separate session"
const activeTodos = [
{ id: "todo-1", content: "Receive todos in the active session", status: "completed", priority: "high" },
{ id: "todo-2", content: "Keep the dock visible across tabs", status: "completed", priority: "high" },
{ id: "todo-3", content: "Close after the final todo", status: "in_progress", priority: "high" },
]
type EventPayload = {
directory: string
payload: Record<string, unknown>
}
test.use({ viewport: { width: 1440, height: 900 }, reducedMotion: "no-preference" })
test("animates todo opening without replaying it across session tabs", async ({ page }) => {
test.setTimeout(90_000)
const events: EventPayload[] = []
const todos: Record<string, typeof activeTodos> = { [sourceID]: [], [otherID]: [] }
const sessionStatus: Record<string, { type: "busy" | "idle" }> = {}
await mockOpenCodeServer(page, {
directory,
project: {
id: projectID,
worktree: directory,
vcs: "git",
name: "todo-dock-navigation",
time: { created: 1700000000000, updated: 1700000000000 },
sandboxes: [],
},
provider: {
all: [
{
id: "opencode",
name: "OpenCode",
models: {
"claude-opus-4-6": {
id: "claude-opus-4-6",
name: "Claude Opus 4.6",
limit: { context: 200_000 },
},
},
},
],
connected: ["opencode"],
default: { providerID: "opencode", modelID: "claude-opus-4-6" },
},
sessions: [session(sourceID, sourceTitle, 1700000000000), session(otherID, otherTitle, 1700000001000)],
sessionStatus: { [sourceID]: { type: "busy" } },
pageMessages: () => ({ items: [] }),
events: () => events.splice(0, 1),
eventRetry: 16,
sessionStatus: () => sessionStatus,
todos: (sessionID) => todos[sessionID] ?? [],
})
await configurePage(page)
await page.goto(sessionHref(sourceID))
await expectSessionTitle(page, sourceTitle)
const dock = page.locator('[data-component="session-todo-dock"]')
await expect(dock).toHaveCount(0)
sessionStatus[sourceID] = { type: "busy" }
events.push(statusEvent(sourceID, "busy"))
await expect(page.getByRole("button", { name: "Stop" })).toBeVisible()
await page.waitForTimeout(700)
const opening = sampleDock(page, 1_000)
todos[sourceID] = activeTodos
events.push(todoEvent(sourceID, activeTodos))
await expect(dock).toBeVisible()
await expect(dock.locator('[data-state="in_progress"]')).toHaveCount(1)
expect((await opening).some((sample) => sample.opacity > 0.05 && sample.opacity < 0.95)).toBe(true)
await switchSession(page, otherID, otherTitle)
await expect(dock).toHaveCount(0)
await switchSession(page, sourceID, sourceTitle)
await expect(dock).toHaveCount(0)
})
function session(id: string, title: string, created: number) {
return {
id,
slug: id,
projectID,
directory,
title,
version: "dev",
time: { created, updated: created },
}
}
function statusEvent(sessionID: string, type: "busy" | "idle"): EventPayload {
return {
directory,
payload: { type: "session.status", properties: { sessionID, status: { type } } },
}
}
function todoEvent(sessionID: string, next: typeof activeTodos): EventPayload {
return {
directory,
payload: { type: "todo.updated", properties: { sessionID, todos: next } },
}
}
async function configurePage(page: Page) {
const server = `http://${process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"}:${process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"}`
await page.addInitScript(
({ directory, dirBase64, server, sessionIDs }) => {
localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } }))
localStorage.setItem(
"opencode.global.dat:server",
JSON.stringify({
projects: { local: [{ worktree: directory, expanded: true }] },
lastProject: { local: directory },
}),
)
localStorage.setItem(
"opencode.window.browser.dat:tabs",
JSON.stringify(sessionIDs.map((sessionId) => ({ type: "session", server, dirBase64, sessionId }))),
)
},
{ directory, dirBase64: base64Encode(directory), server, sessionIDs: [sourceID, otherID] },
)
}
function sessionHref(sessionID: string) {
const server = `http://${process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"}:${process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"}`
return `/server/${base64Encode(server)}/session/${sessionID}`
}
async function switchSession(page: Page, sessionID: string, title: string) {
const href = sessionHref(sessionID)
const tab = page.locator(`[data-slot="titlebar-tabs"] a[href="${href}"]`).first()
await expect(tab).toBeVisible()
await tab.click()
await expectSessionTitle(page, title)
}
function sampleDock(page: Page, duration: number) {
return page.evaluate(async (duration) => {
const samples: { present: boolean; height: number; opacity: number }[] = []
const start = performance.now()
while (performance.now() - start < duration) {
const dock = document.querySelector<HTMLElement>('[data-component="session-todo-dock"]')
const clip = dock?.parentElement?.parentElement
const label = dock?.querySelector<HTMLElement>('[data-action="session-todo-toggle"] span[aria-label]')
samples.push({
present: !!dock,
height: clip?.getBoundingClientRect().height ?? 0,
opacity: label ? Number.parseFloat(getComputedStyle(label).opacity) : 0,
})
await new Promise(requestAnimationFrame)
}
return samples
}, duration)
}
@@ -1,4 +1,5 @@
import { base64Encode } from "@opencode-ai/core/util/encode" import { base64Encode } from "@opencode-ai/core/util/encode"
import type { OpenCodeEvent, SessionMessageInfo } from "@opencode-ai/client/promise"
import { expect, test, type Page } from "@playwright/test" import { expect, test, type Page } from "@playwright/test"
import { currentSession, mockOpenCodeServer } from "../utils/mock-server" import { currentSession, mockOpenCodeServer } from "../utils/mock-server"
import { expectSessionTitle } from "../utils/waits" import { expectSessionTitle } from "../utils/waits"
@@ -12,8 +13,6 @@ const childTitle = "Subagent child session"
// Child session pages derive their heading from the task part that spawned them. // Child session pages derive their heading from the task part that spawned them.
const taskDescription = "Inspect child navigation" const taskDescription = "Inspect child navigation"
type EventPayload = { directory: string; payload: Record<string, unknown> }
test.use({ viewport: { width: 1440, height: 900 } }) test.use({ viewport: { width: 1440, height: 900 } })
test("navigates to a subagent child session missing from the session list", async ({ page }) => { test("navigates to a subagent child session missing from the session list", async ({ page }) => {
@@ -27,7 +26,7 @@ test("navigates to a subagent child session missing from the session list", asyn
await expect(titlebarRight.getByRole("button", { name: "Toggle review" })).toHaveCount(1) await expect(titlebarRight.getByRole("button", { name: "Toggle review" })).toHaveCount(1)
}) })
test("keeps the parent visible while child lineage resolves", async ({ page }) => { test("keeps the parent visible while the child session resolves", async ({ page }) => {
await setup(page) await setup(page)
const requested = Promise.withResolvers<void>() const requested = Promise.withResolvers<void>()
const release = Promise.withResolvers<void>() const release = Promise.withResolvers<void>()
@@ -52,14 +51,18 @@ test("keeps the parent visible while child lineage resolves", async ({ page }) =
}) })
test("shows the not found fallback when the viewed session is deleted", async ({ page }) => { test("shows the not found fallback when the viewed session is deleted", async ({ page }) => {
const events: EventPayload[] = [] const events: OpenCodeEvent[] = []
await setup(page, () => events.splice(0, 1)) await setup(page, () => events.splice(0, 1))
await openChildFromParent(page) await openChildFromParent(page)
await expectSessionTitle(page, taskDescription) await expectSessionTitle(page, taskDescription)
events.push({ events.push({
directory, id: "evt_session_deleted",
payload: { type: "session.deleted", properties: { info: childSession() } }, created: 1700000003000,
type: "session.deleted",
durable: { aggregateID: childID, seq: 1, version: 2 },
location: { directory },
data: { sessionID: childID },
}) })
await expect(page.getByText("This session cannot be found")).toBeVisible() await expect(page.getByText("This session cannot be found")).toBeVisible()
@@ -67,7 +70,7 @@ test("shows the not found fallback when the viewed session is deleted", async ({
await expect(page.getByRole("heading", { name: taskDescription })).toHaveCount(0) await expect(page.getByRole("heading", { name: taskDescription })).toHaveCount(0)
}) })
async function setup(page: Page, events?: () => EventPayload[]) { async function setup(page: Page, events?: () => OpenCodeEvent[]) {
await mockOpenCodeServer(page, { await mockOpenCodeServer(page, {
directory, directory,
project: { project: {
@@ -142,60 +145,36 @@ function childSession() {
return session(childID, childTitle, 1700000001000, { parentID }) return session(childID, childTitle, 1700000001000, { parentID })
} }
function parentMessages() { function parentMessages(): SessionMessageInfo[] {
const userID = "msg_user_0001" const userID = "msg_user_0001"
const assistantID = "msg_assistant_0001" const assistantID = "msg_assistant_0001"
return [ return [
{ {
info: { id: userID,
id: userID, type: "user",
sessionID: parentID, time: { created: 1700000000000 },
role: "user", text: "Delegate work to a subagent",
time: { created: 1700000000000 },
agent: "build",
model: { providerID: "opencode", modelID: "claude-opus-4-6" },
},
parts: [
{
id: "prt_user_text_0001",
sessionID: parentID,
messageID: userID,
type: "text",
text: "Delegate work to a subagent",
},
],
}, },
{ {
info: { id: assistantID,
id: assistantID, type: "assistant",
sessionID: parentID, time: { created: 1700000001000, completed: 1700000002000 },
role: "assistant", model: { id: "claude-opus-4-6", providerID: "opencode" },
time: { created: 1700000001000, completed: 1700000002000 }, agent: "build",
parentID: userID, cost: 0.01,
modelID: "claude-opus-4-6", tokens: { input: 100, output: 200, reasoning: 0, cache: { read: 0, write: 0 } },
providerID: "opencode", finish: "stop",
mode: "build", content: [
agent: "build",
path: { cwd: directory, root: directory },
cost: 0.01,
tokens: { input: 100, output: 200, reasoning: 0, cache: { read: 0, write: 0 } },
finish: "stop",
},
parts: [
{ {
id: "prt_tool_task_0001",
sessionID: parentID,
messageID: assistantID,
type: "tool", type: "tool",
callID: "call_task_0001", id: "call_task_0001",
tool: "task", name: "task",
time: { created: 1700000001000, ran: 1700000001000, completed: 1700000002000 },
state: { state: {
status: "completed", status: "completed",
input: { description: taskDescription, subagent_type: "explore" }, input: { description: taskDescription, subagent_type: "explore" },
output: "Subagent finished", content: [{ type: "text", text: "Subagent finished" }],
title: taskDescription,
metadata: { sessionId: childID }, metadata: { sessionId: childID },
time: { start: 1700000001000, end: 1700000002000 },
}, },
}, },
], ],
@@ -207,7 +186,6 @@ async function configurePage(page: Page) {
const server = `http://${process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"}:${process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"}` const server = `http://${process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"}:${process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"}`
await page.addInitScript( await page.addInitScript(
({ directory, server, sessionId }) => { ({ directory, server, sessionId }) => {
localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } }))
localStorage.setItem( localStorage.setItem(
"opencode.global.dat:server", "opencode.global.dat:server",
JSON.stringify({ JSON.stringify({
@@ -12,7 +12,6 @@ test("pressing mouse down on a tab navigates before mouse up", async ({ page })
await mockServer(page) await mockServer(page)
await page.addInitScript( await page.addInitScript(
({ server, sessionA, sessionB }) => { ({ server, sessionA, sessionB }) => {
localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } }))
localStorage.setItem( localStorage.setItem(
"opencode.window.browser.dat:tabs", "opencode.window.browser.dat:tabs",
JSON.stringify([ JSON.stringify([
@@ -46,7 +45,6 @@ test("keyboard navigation follows the visible tab order", async ({ page }) => {
await mockServer(page) await mockServer(page)
await page.addInitScript( await page.addInitScript(
({ server, sessionA, unresolved, sessionC }) => { ({ server, sessionA, unresolved, sessionC }) => {
localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } }))
localStorage.setItem( localStorage.setItem(
"opencode.window.browser.dat:tabs", "opencode.window.browser.dat:tabs",
JSON.stringify([ JSON.stringify([
@@ -87,50 +85,38 @@ async function mockServer(page: Page) {
await page.route("**/*", async (route) => { await page.route("**/*", async (route) => {
const url = new URL(route.request().url()) const url = new URL(route.request().url())
if (url.origin !== server) return route.fallback() if (url.origin !== server) return route.fallback()
if ([`/api/session/${unresolvedSessionID}`, `/session/${unresolvedSessionID}`].includes(url.pathname)) if (url.pathname === `/api/session/${unresolvedSessionID}`) return new Promise(() => {})
return new Promise(() => {})
if (url.pathname === "/api/event") return sse(route) if (url.pathname === "/api/event") return sse(route)
if (url.pathname === "/api/session") return json(route, { data: sessions.map(currentSession), cursor: {} }) if (url.pathname === "/api/session")
return json(route, { data: sessions.map((session) => currentSession(session)), cursor: {} })
if (url.pathname === "/api/session/active") return json(route, { data: {} }) if (url.pathname === "/api/session/active") return json(route, { data: {} })
const currentSessionInfo = sessions.find((item) => url.pathname === `/api/session/${item.id}`) const currentSessionInfo = sessions.find((item) => url.pathname === `/api/session/${item.id}`)
if (currentSessionInfo) return json(route, { data: currentSession(currentSessionInfo) }) if (currentSessionInfo) return json(route, { data: currentSession(currentSessionInfo) })
if (sessions.some((item) => url.pathname === `/api/session/${item.id}/message`)) if (sessions.some((item) => url.pathname === `/api/session/${item.id}/message`))
return json(route, { data: [], cursor: {} }) return json(route, { data: [], cursor: {} })
if (/^\/session\/[^/]+$/.test(url.pathname)) return json(route, { name: "NotFoundError" }, 404) if (["/api/agent", "/api/provider", "/api/model", "/api/command", "/api/reference"].includes(url.pathname))
if (/^\/session\/[^/]+\/message$/.test(url.pathname)) return json(route, []) return json(route, { location: { directory: sessionA.directory }, data: [] })
if (/^\/session\/[^/]+\/(children|todo|diff)$/.test(url.pathname)) return json(route, []) if (url.pathname === "/api/model/default")
if (["/skill", "/command", "/lsp", "/formatter", "/permission", "/question", "/vcs/diff"].includes(url.pathname)) return json(route, { location: { directory: sessionA.directory }, data: null })
return json(route, []) if (url.pathname === "/api/permission/request" || url.pathname === "/api/question/request")
if (url.pathname === "/provider") return json(route, { location: { directory: sessionA.directory }, data: [] })
return json(route, { all: [], connected: [], default: { providerID: "", modelID: "" } }) if (url.pathname === "/api/mcp") return json(route, { location: { directory: sessionA.directory }, data: [] })
if (url.pathname === "/agent") return json(route, [{ name: "build", mode: "primary" }]) if (url.pathname === "/api/mcp/resource")
if (url.pathname === "/project" || url.pathname === "/project/current") { return json(route, { location: { directory: sessionA.directory }, data: { resources: [], templates: [] } })
if (url.pathname === "/api/project" || url.pathname === "/api/project/current") {
const project = { const project = {
id: sessionA.projectID, id: sessionA.projectID,
worktree: sessionA.directory, canonical: sessionA.directory,
vcs: "git", vcs: "git",
time: { created: 1, updated: 1 }, time: { created: 1, updated: 1 },
sandboxes: [], sandboxes: [],
} }
return json(route, url.pathname === "/project" ? [project] : project) return json(
route,
url.pathname === "/api/project" ? [project] : { id: project.id, directory: sessionA.directory },
)
} }
if (url.pathname === "/path") if (url.pathname === "/api/location") return json(route, { directory: sessionA.directory })
return json(route, {
state: sessionA.directory,
config: sessionA.directory,
worktree: sessionA.directory,
directory: sessionA.directory,
home: sessionA.directory,
})
if (url.pathname === "/api/path")
return json(route, {
state: sessionA.directory,
config: sessionA.directory,
worktree: sessionA.directory,
directory: sessionA.directory,
home: sessionA.directory,
})
if (url.pathname === "/vcs") return json(route, { branch: "main", default_branch: "main" })
if (url.pathname === "/api/vcs") if (url.pathname === "/api/vcs")
return json(route, { return json(route, {
location: { directory: sessionA.directory }, location: { directory: sessionA.directory },
@@ -1,4 +1,4 @@
import { base64Encode } from "@opencode-ai/core/util/encode" import { base64Encode, checksum } from "@opencode-ai/core/util/encode"
import { expect, test, type Page } from "@playwright/test" import { expect, test, type Page } from "@playwright/test"
import { mockOpenCodeServer } from "../utils/mock-server" import { mockOpenCodeServer } from "../utils/mock-server"
import { expectSessionTitle } from "../utils/waits" import { expectSessionTitle } from "../utils/waits"
@@ -8,12 +8,12 @@ const projectID = "proj_terminal_composer_focus"
const sessionID = "ses_terminal_composer_focus" const sessionID = "ses_terminal_composer_focus"
const ptyID = "pty_terminal_composer_focus" const ptyID = "pty_terminal_composer_focus"
const newPtyID = "pty_terminal_composer_focus_new" const newPtyID = "pty_terminal_composer_focus_new"
const server = `http://${process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"}:${process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"}`
test.use({ viewport: { width: 1440, height: 900 } }) test.use({ viewport: { width: 1440, height: 900 } })
test.beforeEach(async ({ page }) => { test.beforeEach(async ({ page }) => {
await mockOpenCodeServer(page, { await mockOpenCodeServer(page, {
protocol: "v2",
directory, directory,
project: { project: {
id: projectID, id: projectID,
@@ -71,13 +71,10 @@ test.beforeEach(async ({ page }) => {
}), }),
) )
await page.routeWebSocket(new RegExp(`/api/pty/${ptyID}/connect`), () => undefined) await page.routeWebSocket(new RegExp(`/api/pty/${ptyID}/connect`), () => undefined)
await page.addInitScript(() => {
localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } }))
})
}) })
test("routes typing to the composer unless the open terminal is focused", async ({ page }) => { test("routes typing to the composer unless the open terminal is focused", async ({ page }) => {
await page.goto(`/${base64Encode(directory)}/session/${sessionID}`) await page.goto(`/server/${base64Encode(server)}/session/${sessionID}`)
await expectSessionTitle(page, "Terminal composer focus") await expectSessionTitle(page, "Terminal composer focus")
const composer = page.locator('[data-component="prompt-input"]') const composer = page.locator('[data-component="prompt-input"]')
@@ -116,7 +113,7 @@ test("keeps composer focus when a cached terminal finishes mounting", async ({ p
}) })
await seedCachedTerminal(page) await seedCachedTerminal(page)
await page.goto(`/${base64Encode(directory)}/session/${sessionID}`, { waitUntil: "commit" }) await page.goto(`/server/${base64Encode(server)}/session/${sessionID}`, { waitUntil: "commit" })
await expectSessionTitle(page, "Terminal composer focus") await expectSessionTitle(page, "Terminal composer focus")
const composer = page.locator('[data-component="prompt-input"]') const composer = page.locator('[data-component="prompt-input"]')
@@ -142,7 +139,7 @@ test("keeps newer composer focus while an explicit terminal open finishes", asyn
await route.continue() await route.continue()
}) })
await page.goto(`/${base64Encode(directory)}/session/${sessionID}`) await page.goto(`/server/${base64Encode(server)}/session/${sessionID}`)
await expectSessionTitle(page, "Terminal composer focus") await expectSessionTitle(page, "Terminal composer focus")
const composer = page.locator('[data-component="prompt-input"]') const composer = page.locator('[data-component="prompt-input"]')
@@ -187,7 +184,7 @@ test("focuses a terminal created from the new-terminal button", async ({ page })
) )
await page.routeWebSocket(new RegExp(`/api/pty/${newPtyID}/connect`), () => undefined) await page.routeWebSocket(new RegExp(`/api/pty/${newPtyID}/connect`), () => undefined)
await page.goto(`/${base64Encode(directory)}/session/${sessionID}`) await page.goto(`/server/${base64Encode(server)}/session/${sessionID}`)
await expectSessionTitle(page, "Terminal composer focus") await expectSessionTitle(page, "Terminal composer focus")
const composer = page.locator('[data-component="prompt-input"]') const composer = page.locator('[data-component="prompt-input"]')
@@ -214,12 +211,18 @@ function seedCachedTerminal(page: Page) {
}), }),
) )
}, },
{ terminalKey: `${base64Encode(directory)}/terminal.v1`, ptyID }, { terminalKey: terminalStorageKey(), ptyID },
) )
} }
function terminalStorageKey() {
const dir = base64Encode(directory)
const head = dir.slice(0, 12).replace(/[^a-zA-Z0-9._-]/g, "-")
return `opencode.workspace.${head}.${checksum(dir) ?? "0"}.dat:workspace:terminal`
}
function ptyLocation() { function ptyLocation() {
return { directory, project: { id: projectID, directory } } return { directory, project: { id: projectID, directory, canonical: directory } }
} }
function ptyInfo(id: string, title: string) { function ptyInfo(id: string, title: string) {
@@ -6,6 +6,7 @@ const directory = "C:/OpenCode/HiddenTerminalRegression"
const projectID = "proj_hidden_terminal_regression" const projectID = "proj_hidden_terminal_regression"
const sessionID = "ses_hidden_terminal_regression" const sessionID = "ses_hidden_terminal_regression"
const title = "Hidden terminal regression" const title = "Hidden terminal regression"
const server = `http://${process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"}:${process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"}`
test("unmounts the terminal panel while it is hidden", async ({ page }) => { test("unmounts the terminal panel while it is hidden", async ({ page }) => {
await page.setViewportSize({ width: 1400, height: 900 }) await page.setViewportSize({ width: 1400, height: 900 })
@@ -91,7 +92,7 @@ test("unmounts the terminal panel while it is hidden", async ({ page }) => {
) )
await page.routeWebSocket("**/api/pty/pty_hidden_terminal/connect", () => undefined) await page.routeWebSocket("**/api/pty/pty_hidden_terminal/connect", () => undefined)
await page.goto(`/${base64Encode(directory)}/session/${sessionID}`) await page.goto(`/server/${base64Encode(server)}/session/${sessionID}`)
await expectSessionTitle(page, title) await expectSessionTitle(page, title)
await page.keyboard.press("Control+Backquote") await page.keyboard.press("Control+Backquote")
@@ -121,7 +121,6 @@ async function setup(page: Page) {
await page.addInitScript( await page.addInitScript(
({ directory, server, sessions }) => { ({ directory, server, sessions }) => {
localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } }))
localStorage.setItem( localStorage.setItem(
"opencode.global.dat:server", "opencode.global.dat:server",
JSON.stringify({ JSON.stringify({
@@ -21,16 +21,29 @@ const words = [
"vector", "vector",
] ]
const serverKey = `http://127.0.0.1:${process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"}` const serverKey = `http://${process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"}:${process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"}`
const sourceID = "ses_smoke_source" const sourceID = "ses_smoke_source"
const targetID = "ses_smoke_target" const targetID = "ses_smoke_target"
const directory = "C:/OpenCode/SmokeProject" const directory = "C:/OpenCode/SmokeProject"
const projectID = "proj_smoke_timeline" const projectID = "proj_smoke_timeline"
const model = { providerID: "opencode", modelID: "claude-opus-4-6", variant: "max" } const model = { providerID: "opencode", modelID: "claude-opus-4-6", variant: "max" }
type MessageInfo = Record<string, unknown> & { id: string; role: "user" | "assistant" } type MessagePart = {
type MessagePart = Record<string, unknown> & { id: string; type: string; text?: string; tool?: string } id: string
type Message = { info: MessageInfo; parts: MessagePart[] } type: "text" | "reasoning" | "tool"
text?: string
time?: { start: number; end?: number }
callID?: string
tool?: string
state?: {
status: "completed"
input: Record<string, unknown>
output: string
title: unknown
metadata: Record<string, unknown>
time: { start: number; end: number }
}
}
function lorem(seed: number, length: number) { function lorem(seed: number, length: number) {
let out = "" let out = ""
@@ -48,54 +61,59 @@ function id(prefix: string, value: number) {
return `${prefix}_smoke_${String(value).padStart(4, "0")}` return `${prefix}_smoke_${String(value).padStart(4, "0")}`
} }
function userMessage(sessionID: string, index: number, textLength: number, diffs: unknown[] = []): Message { function userMessage(_sessionID: string, index: number, textLength: number, diffs: unknown[] = []): SessionMessageInfo {
const messageID = id("msg_user", index) const messageID = id("msg_user", index)
return { return {
info: { id: messageID,
id: messageID, type: "user",
sessionID, time: { created: 1700000000000 + index * 10_000 },
role: "user", text: lorem(index, textLength),
time: { created: 1700000000000 + index * 10_000 }, metadata: diffs.length ? { diffs: diffs as JsonValue } : undefined,
summary: { diffs },
agent: "build",
model,
},
parts: [
{
id: id("prt_user_text", index),
sessionID,
messageID,
type: "text",
text: lorem(index, textLength),
},
],
} }
} }
function assistantMessage(sessionID: string, index: number, parentID: string, parts: MessagePart[]): Message { function assistantMessage(
_sessionID: string,
index: number,
_parentID: string,
parts: MessagePart[],
): SessionMessageInfo {
const messageID = id("msg_assistant", index) const messageID = id("msg_assistant", index)
return { return {
info: { id: messageID,
id: messageID, type: "assistant",
sessionID, time: { created: 1700000000000 + index * 10_000 + 1_000, completed: 1700000000000 + index * 10_000 + 8_000 },
role: "assistant", model: { id: model.modelID, providerID: model.providerID, variant: model.variant },
time: { created: 1700000000000 + index * 10_000 + 1_000, completed: 1700000000000 + index * 10_000 + 8_000 }, agent: "build",
parentID, cost: 0.01,
modelID: model.modelID, tokens: { input: 100, output: 200, reasoning: 0, cache: { read: 0, write: 0 } },
providerID: model.providerID, finish: "stop",
mode: "build", content: parts.map(messageContent),
agent: "build", }
path: { cwd: directory, root: directory }, }
cost: 0.01,
tokens: { input: 100, output: 200, reasoning: 0, cache: { read: 0, write: 0 } }, function messageContent(part: MessagePart): SessionMessageAssistant["content"][number] {
variant: "max", if (part.type === "text") return { type: "text", text: part.text ?? "" }
finish: "stop", if (part.type === "reasoning")
return {
type: "reasoning",
text: part.text ?? "",
time: part.time
? { created: part.time.start, ...(part.time.end === undefined ? {} : { completed: part.time.end }) }
: undefined,
}
const state = part.state!
return {
type: "tool",
id: part.callID ?? part.id,
name: part.tool!,
time: { created: state.time.start, ran: state.time.start, completed: state.time.end },
state: {
status: "completed",
input: state.input as Record<string, JsonValue>,
content: [{ type: "text", text: state.output }],
metadata: state.metadata as Record<string, JsonValue>,
}, },
parts: parts.map((part) => ({
...part,
sessionID,
messageID,
})),
} }
} }
@@ -180,7 +198,7 @@ function code(seed: number, lines: number) {
) )
} }
function turn(index: number): Message[] { function turn(index: number): SessionMessageInfo[] {
const diff = index % 9 === 0 ? [fileDiff(`src/generated/summary-${index}.ts`, index)] : [] const diff = index % 9 === 0 ? [fileDiff(`src/generated/summary-${index}.ts`, index)] : []
const user = userMessage(targetID, index, 100 + (index % 4) * 80, diff) const user = userMessage(targetID, index, 100 + (index % 4) * 80, diff)
const parts = [ const parts = [
@@ -219,33 +237,26 @@ function turn(index: number): Message[] {
? [toolPart(index, 12, "task", { description: "Inspect generated fixture", subagent_type: "explore" }, 160)] ? [toolPart(index, 12, "task", { description: "Inspect generated fixture", subagent_type: "explore" }, 160)]
: []), : []),
] ]
return [user, assistantMessage(targetID, index, user.info.id, parts)] return [user, assistantMessage(targetID, index, user.id, parts)]
} }
const targetMessages = Array.from({ length: 72 }, (_, index) => turn(index)).flat() const targetMessages = Array.from({ length: 101 }, (_, index) => turn(index)).flat()
const sourceMessages = Array.from({ length: 12 }, (_, index) => [ const sourceMessages = Array.from({ length: 12 }, (_, index) => [
userMessage(sourceID, index + 1000, 120), userMessage(sourceID, index + 1000, 120),
assistantMessage(sourceID, index + 1000, id("msg_user", index + 1000), [textPart(index + 1000, 0, 240)]), assistantMessage(sourceID, index + 1000, id("msg_user", index + 1000), [textPart(index + 1000, 0, 240)]),
]).flat() ]).flat()
const messages: Record<string, SessionMessageInfo[]> = { [sourceID]: sourceMessages, [targetID]: targetMessages }
function renderable(part: MessagePart) { function currentPartIDs(message: SessionMessageInfo) {
if (part.type === "tool" && part.tool === "todowrite") return false if (message.type === "user") return message.text.trim() ? [`${message.id}:text:0`] : []
if (part.type === "text") return !!part.text.trim() if (message.type !== "assistant") return []
if (part.type === "reasoning") return !!part.text.trim()
return part.type !== "step-start" && part.type !== "step-finish" && part.type !== "patch"
}
function currentPartIDs(message: Message) {
const ordinals = { text: 0, reasoning: 0 } const ordinals = { text: 0, reasoning: 0 }
return message.parts return message.content.flatMap((part) => {
.flatMap((part) => { if (part.type === "text") return part.text.trim() ? [`${message.id}:text:${ordinals.text++}`] : []
if (!renderable(part)) return [] if (part.type === "reasoning") return part.text.trim() ? [`${message.id}:reasoning:${ordinals.reasoning++}`] : []
if (part.type === "text") return [`${message.info.id}:text:${ordinals.text++}`] if (part.type === "tool") return [part.id]
if (part.type === "reasoning") return [`${message.info.id}:reasoning:${ordinals.reasoning++}`] return []
if (part.type === "tool") return [typeof part.callID === "string" ? part.callID : part.id] })
return []
})
.sort()
} }
export const fixture = { export const fixture = {
@@ -292,30 +303,30 @@ export const fixture = {
], ],
sourceID, sourceID,
targetID, targetID,
messages: { [sourceID]: sourceMessages, [targetID]: targetMessages }, messages,
expected: { expected: {
sourceTitle: "Uncommitted changes inquiry", sourceTitle: "Uncommitted changes inquiry",
targetTitle: "Example Game: sample jump movement & sample physics analysis", targetTitle: "Example Game: sample jump movement & sample physics analysis",
targetMessageIDs: targetMessages targetMessageIDs: targetMessages.filter((message) => message.type === "user").map((message) => message.id),
.filter((message) => message.info.role === "user")
.map((message) => message.info.id),
targetPartIDs: targetMessages.flatMap(currentPartIDs), targetPartIDs: targetMessages.flatMap(currentPartIDs),
expandedShellPartID: targetMessages.flatMap((message) => message.parts).find((part) => part.tool === "bash")! expandedShellPartID: targetMessages
.callID, .flatMap((message) => (message.type === "assistant" ? message.content : []))
.flatMap((part) => (part.type === "tool" && part.name === "bash" ? [part.id] : []))[0],
}, },
} }
export function pageMessages(sessionID: string, limit: number, before?: string) { export function pageMessages(sessionID: string, limit: number, before?: string) {
const messages = fixture.messages[sessionID as keyof typeof fixture.messages] ?? [] const messages = fixture.messages[sessionID] ?? []
const end = before const end = before
? Math.max( ? Math.max(
0, 0,
messages.findIndex((message) => message.info.id === before), messages.findIndex((message) => message.id === before),
) )
: messages.length : messages.length
const start = Math.max(0, end - limit) const start = Math.max(0, end - limit)
return { return {
items: messages.slice(start, end), items: messages.slice(start, end),
cursor: start > 0 ? messages[start]!.info.id : undefined, cursor: start > 0 ? messages[start].id : undefined,
} }
} }
import type { JsonValue, SessionMessageAssistant, SessionMessageInfo } from "@opencode-ai/client/promise"
+18 -19
View File
@@ -33,7 +33,6 @@ test.describe("smoke: session timeline", () => {
test("keeps the visible message fixed while prepending history", async ({ page }) => { test("keeps the visible message fixed while prepending history", async ({ page }) => {
const requests: { before?: string; phase: "start" | "end"; at: number }[] = [] const requests: { before?: string; phase: "start" | "end"; at: number }[] = []
await mockOpenCodeServer(page, { await mockOpenCodeServer(page, {
protocol: "v2",
sessions: fixture.sessions, sessions: fixture.sessions,
provider: fixture.provider, provider: fixture.provider,
directory: fixture.directory, directory: fixture.directory,
@@ -44,7 +43,7 @@ test.describe("smoke: session timeline", () => {
}) })
await configureSmokePage(page, fixture.directory) await configureSmokePage(page, fixture.directory)
await navigateToSession(page, fixture.directory, fixture.targetID, fixture.expected.targetTitle) await navigateToSession(page, fixture.targetID, fixture.expected.targetTitle)
await waitForTimelineStable(page) await waitForTimelineStable(page)
const scroller = timelineScroller(page) const scroller = timelineScroller(page)
await pointAtTimeline(page) await pointAtTimeline(page)
@@ -92,7 +91,6 @@ test.describe("smoke: session timeline", () => {
test("preserves the timeline gap above the composer", async ({ page }) => { test("preserves the timeline gap above the composer", async ({ page }) => {
await mockOpenCodeServer(page, { await mockOpenCodeServer(page, {
protocol: "v2",
sessions: fixture.sessions, sessions: fixture.sessions,
provider: fixture.provider, provider: fixture.provider,
directory: fixture.directory, directory: fixture.directory,
@@ -101,7 +99,7 @@ test.describe("smoke: session timeline", () => {
}) })
await configureSmokePage(page, fixture.directory) await configureSmokePage(page, fixture.directory)
await navigateToSession(page, fixture.directory, fixture.targetID, fixture.expected.targetTitle) await navigateToSession(page, fixture.targetID, fixture.expected.targetTitle)
await waitForTimelineStable(page) await waitForTimelineStable(page)
const scroller = timelineScroller(page) const scroller = timelineScroller(page)
await scroller.evaluate((element) => { await scroller.evaluate((element) => {
@@ -119,12 +117,11 @@ test.describe("smoke: session timeline", () => {
test("paints cached session tabs at the latest message", async ({ page }) => { test("paints cached session tabs at the latest message", async ({ page }) => {
await mockOpenCodeServer(page, { await mockOpenCodeServer(page, {
protocol: "v2",
sessions: fixture.sessions, sessions: fixture.sessions,
provider: fixture.provider, provider: fixture.provider,
directory: fixture.directory, directory: fixture.directory,
project: fixture.project, project: fixture.project,
pageMessages: (sessionID) => ({ items: fixture.messages[sessionID as keyof typeof fixture.messages] ?? [] }), pageMessages: (sessionID) => ({ items: fixture.messages[sessionID] ?? [] }),
}) })
await configureSmokePage(page, fixture.directory) await configureSmokePage(page, fixture.directory)
await page.addInitScript( await page.addInitScript(
@@ -143,11 +140,11 @@ test.describe("smoke: session timeline", () => {
{ server: fixture.serverKey, sourceID: fixture.sourceID, targetID: fixture.targetID }, { server: fixture.serverKey, sourceID: fixture.sourceID, targetID: fixture.targetID },
) )
await page.goto(`/${base64Encode(fixture.directory)}/session/${fixture.targetID}`) await page.goto(`/server/${base64Encode(fixture.serverKey)}/session/${fixture.targetID}`)
await expectSessionTitle(page, fixture.expected.targetTitle) await expectSessionTitle(page, fixture.expected.targetTitle)
await switchTitlebarSession(page, fixture.sourceID, fixture.expected.sourceTitle) await switchTitlebarSession(page, fixture.sourceID, fixture.expected.sourceTitle)
const destination = fixture.messages[fixture.targetID].map((message) => message.info.id) const destination = fixture.messages[fixture.targetID].map((message) => message.id)
const last = fixture.expected.targetMessageIDs.at(-1)! const last = fixture.expected.targetMessageIDs.at(-1)!
await page.evaluate( await page.evaluate(
({ destination, last }) => { ({ destination, last }) => {
@@ -188,7 +185,11 @@ test.describe("smoke: session timeline", () => {
const bottom = root const bottom = root
.querySelector<HTMLElement>('[data-timeline-row="bottom-spacer"]') .querySelector<HTMLElement>('[data-timeline-row="bottom-spacer"]')
?.getBoundingClientRect() ?.getBoundingClientRect()
samples.push({ ids: visible, last: visible.includes(last), bottomError: bottom?.bottom - view.bottom }) samples.push({
ids: visible,
last: visible.includes(last),
bottomError: bottom ? bottom.bottom - view.bottom : undefined,
})
if ( if (
!firstPaint && !firstPaint &&
visible.includes(last) && visible.includes(last) &&
@@ -258,12 +259,11 @@ test.describe("smoke: session timeline", () => {
test("paints a cold session tab at the latest message", async ({ page }) => { test("paints a cold session tab at the latest message", async ({ page }) => {
await mockOpenCodeServer(page, { await mockOpenCodeServer(page, {
protocol: "v2",
sessions: fixture.sessions, sessions: fixture.sessions,
provider: fixture.provider, provider: fixture.provider,
directory: fixture.directory, directory: fixture.directory,
project: fixture.project, project: fixture.project,
pageMessages: (sessionID) => ({ items: fixture.messages[sessionID as keyof typeof fixture.messages] ?? [] }), pageMessages: (sessionID) => ({ items: fixture.messages[sessionID] ?? [] }),
}) })
await configureSmokePage(page, fixture.directory) await configureSmokePage(page, fixture.directory)
await page.addInitScript( await page.addInitScript(
@@ -281,10 +281,10 @@ test.describe("smoke: session timeline", () => {
}, },
{ server: fixture.serverKey, sourceID: fixture.sourceID, targetID: fixture.targetID }, { server: fixture.serverKey, sourceID: fixture.sourceID, targetID: fixture.targetID },
) )
await page.goto(`/${base64Encode(fixture.directory)}/session/${fixture.sourceID}`) await page.goto(`/server/${base64Encode(fixture.serverKey)}/session/${fixture.sourceID}`)
await expectSessionTitle(page, fixture.expected.sourceTitle) await expectSessionTitle(page, fixture.expected.sourceTitle)
const last = fixture.expected.targetMessageIDs.at(-1)! const last = fixture.expected.targetMessageIDs.at(-1)!
const destination = fixture.messages[fixture.targetID].map((message) => message.info.id) const destination = fixture.messages[fixture.targetID].map((message) => message.id)
await page.evaluate( await page.evaluate(
({ destination, last }) => { ({ destination, last }) => {
const ids = new Set(destination) const ids = new Set(destination)
@@ -337,7 +337,6 @@ test.describe("smoke: session timeline", () => {
test("renders seeded timeline in order while paging through history", async ({ page }) => { test("renders seeded timeline in order while paging through history", async ({ page }) => {
const errors = trackPageErrors(page) const errors = trackPageErrors(page)
await mockOpenCodeServer(page, { await mockOpenCodeServer(page, {
protocol: "v2",
sessions: fixture.sessions, sessions: fixture.sessions,
provider: fixture.provider, provider: fixture.provider,
directory: fixture.directory, directory: fixture.directory,
@@ -347,9 +346,9 @@ test.describe("smoke: session timeline", () => {
await configureSmokePage(page, fixture.directory) await configureSmokePage(page, fixture.directory)
await selectHomeProject(page, fixture.project.name) await selectHomeProject(page, fixture.project.name)
await navigateToSession(page, fixture.directory, fixture.sourceID, fixture.expected.sourceTitle) await navigateToSession(page, fixture.sourceID, fixture.expected.sourceTitle)
await expectSessionReady(page) await expectSessionReady(page)
await navigateToSession(page, fixture.directory, fixture.targetID, fixture.expected.targetTitle) await navigateToSession(page, fixture.targetID, fixture.expected.targetTitle)
const expectedPartIDs = fixture.expected.targetPartIDs const expectedPartIDs = fixture.expected.targetPartIDs
const expectedMessageIDs = fixture.expected.targetMessageIDs const expectedMessageIDs = fixture.expected.targetMessageIDs
await expectSessionTimelineReady(page, expectedPartIDs, expectedMessageIDs, errors) await expectSessionTimelineReady(page, expectedPartIDs, expectedMessageIDs, errors)
@@ -723,7 +722,7 @@ function expectCompleteScroll(
).toEqual([]) ).toEqual([])
expect(new Set(expectedPartIDs).size).toBe(expectedPartIDs.length) expect(new Set(expectedPartIDs).size).toBe(expectedPartIDs.length)
expect(new Set(expectedMessageIDs).size).toBe(expectedMessageIDs.length) expect(new Set(expectedMessageIDs).size).toBe(expectedMessageIDs.length)
expect(expectedPartIDs.length).toBe(331) expect(expectedPartIDs.length).toBe(465)
} }
async function selectHomeProject(page: Page, projectName: string) { async function selectHomeProject(page: Page, projectName: string) {
@@ -738,8 +737,8 @@ async function selectHomeProject(page: Page, projectName: string) {
await expect(page).toHaveURL(/\/$/) await expect(page).toHaveURL(/\/$/)
} }
async function navigateToSession(page: Page, directory: string, sessionId: string, expectedTitle: string) { async function navigateToSession(page: Page, sessionId: string, expectedTitle: string) {
await page.goto(`/${base64Encode(directory)}/session/${sessionId}`) await page.goto(`/server/${base64Encode(fixture.serverKey)}/session/${sessionId}`)
await expectSessionTitle(page, expectedTitle) await expectSessionTitle(page, expectedTitle)
} }
+3 -13
View File
@@ -1,21 +1,11 @@
{ {
"extends": "../tsconfig.json", "extends": "../tsconfig.json",
"compilerOptions": { "compilerOptions": {
"composite": false,
"emitDeclarationOnly": false,
"noEmit": true, "noEmit": true,
"rootDir": "..", "rootDir": "..",
"types": ["node", "bun"] "types": ["node", "bun"]
}, },
"include": [ "include": ["./**/*.ts", "./**/*.tsx", "../src/types.ts"]
"./performance/timeline-stability/**/*.spec.ts",
"./performance/timeline-stability/fixture.test.ts",
"./performance/timeline-stability/fixture.ts",
"./performance/unit/visual-stability.test.ts",
"./reproduction/timeline-suspense/**/*.ts",
"./reproduction/timeline-suspense/**/*.tsx",
"../src/types.ts",
"../src/pages/session/timeline/observe-element-offset.ts",
"./regression/new-session-panel-corner.spec.ts",
"./regression/session-timeline-context-resize.spec.ts",
"./utils/**/*.ts"
]
} }
@@ -6,7 +6,6 @@ const directory = "C:/OpenCode/NewProject"
test("creates a session in a new project and selects its model", async ({ page }) => { test("creates a session in a new project and selects its model", async ({ page }) => {
await mockOpenCodeServer(page, { await mockOpenCodeServer(page, {
protocol: "v2",
directory, directory,
project: { project: {
id: "proj_model_selection_flow", id: "proj_model_selection_flow",
@@ -53,7 +52,6 @@ test("creates a session in a new project and selects its model", async ({ page }
findFiles: () => ["NewProject"], findFiles: () => ["NewProject"],
}) })
await page.addInitScript(() => { await page.addInitScript(() => {
localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } }))
localStorage.setItem("opencode.global.dat:server", JSON.stringify({ projects: { local: [] } })) localStorage.setItem("opencode.global.dat:server", JSON.stringify({ projects: { local: [] } }))
localStorage.setItem( localStorage.setItem(
"opencode.global.dat:model", "opencode.global.dat:model",
@@ -72,7 +70,12 @@ test("creates a session in a new project and selects its model", async ({ page }
const addProject = page.locator('[data-action="home-add-project-row"]') const addProject = page.locator('[data-action="home-add-project-row"]')
await expectAppVisible(addProject) await expectAppVisible(addProject)
await addProject.click() await addProject.click()
await page.locator("[data-directory-path]").click() const directoryItem = page.getByRole("treeitem", { name: "NewProject" })
await expect(directoryItem).toBeVisible()
await directoryItem.click()
const selectFolder = page.getByRole("button", { name: "Select folder" })
await expect(selectFolder).toBeEnabled()
await selectFolder.click()
await page.locator('[data-action="home-new-session"]').click() await page.locator('[data-action="home-new-session"]').click()
await expectAppVisible(page.locator('[data-component="prompt-input-v2"]')) await expectAppVisible(page.locator('[data-component="prompt-input-v2"]'))
+3
View File
@@ -6,6 +6,9 @@ export function trackPageErrors(page: Page) {
if (message.type() === "error") errors.push(message.text()) if (message.type() === "error") errors.push(message.text())
}) })
page.on("pageerror", (error) => errors.push(error.stack ?? error.message)) page.on("pageerror", (error) => errors.push(error.stack ?? error.message))
page.on("response", (response) => {
if (response.status() >= 400) errors.push(`${response.status()} ${response.url()}`)
})
return errors return errors
} }
+97 -377
View File
@@ -1,37 +1,30 @@
import type { Page, Route } from "@playwright/test" import type { Page, Route } from "@playwright/test"
import type { import type { JsonValue, OpenCodeEvent, SessionMessageInfo } from "@opencode-ai/client/promise"
JsonValue,
PromptAgentAttachment,
PromptFileAttachment,
SessionMessageAssistant,
SessionMessageInfo,
SessionStructuredError,
} from "@opencode-ai/client/promise"
const emptyList = new Set(["/skill", "/command", "/lsp", "/formatter", "/vcs/status", "/vcs/diff"])
const emptyObject = new Set(["/global/config", "/config", "/provider/auth", "/mcp", "/experimental/resource"])
export interface MockServerConfig { export interface MockServerConfig {
protocol?: "v1" | "v2"
provider: unknown | (() => unknown) provider: unknown | (() => unknown)
integrationMethods?: Record<string, unknown[]> integrationMethods?: Record<string, unknown[]>
onConnectKey?: (input: { integrationID: string; body: unknown }) => void onConnectKey?: (input: { integrationID: string; body: unknown }) => void
onInstanceDispose?: () => void
directory: string directory: string
project: unknown project: unknown
sessions: ({ id: string } & Record<string, unknown>)[] sessions: ({ id: string } & Record<string, unknown>)[]
pageMessages: (sessionId: string, limit: number, before?: string) => { items: unknown[]; cursor?: string } pageMessages: (
sessionId: string,
limit: number,
before?: string,
) => {
items: SessionMessageInfo[]
cursor?: string
}
vcsDiff?: unknown[] vcsDiff?: unknown[]
messageDelay?: number messageDelay?: number
beforeMessagesResponse?: (input: { sessionID: string; before?: string }) => Promise<void> beforeMessagesResponse?: (input: { sessionID: string; before?: string }) => Promise<void>
onMessages?: (input: { sessionID: string; before?: string; phase: "start" | "end" }) => void onMessages?: (input: { sessionID: string; before?: string; phase: "start" | "end" }) => void
message?: (sessionID: string, messageID: string) => unknown message?: (sessionID: string, messageID: string) => SessionMessageInfo | undefined
onMessage?: (input: { sessionID: string; messageID: string }) => void onMessage?: (input: { sessionID: string; messageID: string }) => void
events?: () => unknown[] events?: () => OpenCodeEvent[]
eventRetry?: number eventRetry?: number
todos?: (sessionID: string) => unknown[]
permissions?: unknown[] | (() => unknown[]) permissions?: unknown[] | (() => unknown[])
questions?: unknown[] | (() => unknown[])
forms?: unknown[] | (() => unknown[]) forms?: unknown[] | (() => unknown[])
fileList?: (path: string) => unknown | Promise<unknown> fileList?: (path: string) => unknown | Promise<unknown>
fileContent?: (path: string) => unknown | Promise<unknown> fileContent?: (path: string) => unknown | Promise<unknown>
@@ -46,10 +39,11 @@ type MockStreamWindow = Window & {
export async function mockOpenCodeServer(page: Page, config: MockServerConfig) { export async function mockOpenCodeServer(page: Page, config: MockServerConfig) {
const cursors = new Map<string, string>() const cursors = new Map<string, string>()
const server = `http://${process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"}:${process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"}`
let nextCursor = 0 let nextCursor = 0
await page.addInitScript( await page.addInitScript(
({ port, retry }) => { ({ server, retry }) => {
const host = window as MockStreamWindow const host = window as MockStreamWindow
if (host.__testSseTransport || host.__mockServerStream) return if (host.__testSseTransport || host.__mockServerStream) return
const originalFetch = window.fetch.bind(window) const originalFetch = window.fetch.bind(window)
@@ -74,7 +68,7 @@ export async function mockOpenCodeServer(page: Page, config: MockServerConfig) {
const fetch = (input: RequestInfo | URL, init?: RequestInit) => { const fetch = (input: RequestInfo | URL, init?: RequestInit) => {
const request = new Request(input, init) const request = new Request(input, init)
const url = new URL(request.url) const url = new URL(request.url)
if (url.port !== port || url.pathname !== "/api/event") return originalFetch(request) if (url.origin !== server || url.pathname !== "/api/event") return originalFetch(request)
state.connections += 1 state.connections += 1
const id = state.connections const id = state.connections
let ended = false let ended = false
@@ -114,7 +108,7 @@ export async function mockOpenCodeServer(page: Page, config: MockServerConfig) {
} }
Object.defineProperty(window, "fetch", { configurable: true, writable: true, value: fetch }) Object.defineProperty(window, "fetch", { configurable: true, writable: true, value: fetch })
}, },
{ port: process.env.PLAYWRIGHT_SERVER_PORT ?? "4096", retry: config.eventRetry }, { server, retry: config.eventRetry },
) )
if (config.events) { if (config.events) {
@@ -125,10 +119,7 @@ export async function mockOpenCodeServer(page: Page, config: MockServerConfig) {
if (batch.length === 0) return if (batch.length === 0) return
pump.busy = true pump.busy = true
void page void page
.evaluate( .evaluate((payloads) => (window as MockStreamWindow).__mockServerStream?.push(payloads), batch as unknown[])
(payloads) => (window as MockStreamWindow).__mockServerStream?.push(payloads),
batch.map(currentEvent),
)
.catch(() => {}) .catch(() => {})
.finally(() => { .finally(() => {
pump.busy = false pump.busy = false
@@ -136,87 +127,33 @@ export async function mockOpenCodeServer(page: Page, config: MockServerConfig) {
}, 50) }, 50)
page.on("close", () => clearInterval(timer)) page.on("close", () => clearInterval(timer))
} }
const staticRoutes: Record<string, unknown> = {
"/path": {
state: config.directory,
config: config.directory,
worktree: config.directory,
directory: config.directory,
home: "C:/OpenCode",
},
"/project": [config.project],
"/project/current": config.project,
"/agent": [{ name: "build", mode: "primary" }],
"/vcs": { branch: "main", default_branch: "main" },
"/session": config.sessions,
}
await page.route("**/*", async (route) => { await page.route("**/*", async (route) => {
const url = new URL(route.request().url()) const url = new URL(route.request().url())
const targetPort = process.env.PLAYWRIGHT_SERVER_PORT ?? "4096" const targetPort = process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"
const appPort = new URL( const appPort = new URL(
process.env.PLAYWRIGHT_BASE_URL ?? `http://127.0.0.1:${process.env.PLAYWRIGHT_PORT ?? "3000"}`, process.env.PLAYWRIGHT_BASE_URL ?? `http://127.0.0.1:${process.env.PLAYWRIGHT_PORT ?? "3000"}`,
).port ).port
if (url.port !== targetPort && url.port !== appPort) return route.fallback() if (url.origin !== server && url.port !== appPort) return route.fallback()
const path = url.pathname const path = url.pathname
if (path === "/global/event" || path === "/event" || path === "/api/event") { if (path === "/api/event") {
const events = config.events?.() const events = config.events?.()
return sse( return sse(
route, route,
path === "/api/event" [{ id: "evt_mock_connected", type: "server.connected", data: {} }, ...(events ?? [])],
? [{ id: "evt_mock_connected", type: "server.connected", data: {} }, ...(events?.map(currentEvent) ?? [])]
: [
...(path === "/global/event"
? [{ payload: { id: "evt_mock_connected", type: "server.connected", properties: {} } }]
: []),
...(events ?? []),
],
config.eventRetry, config.eventRetry,
) )
} }
if (path === "/global/health")
return config.protocol === "v2" ? json(route, {}, undefined, 404) : json(route, { healthy: true })
if (path === "/api/health") return json(route, { healthy: true, version: "2.0.0", pid: 1 }) if (path === "/api/health") return json(route, { healthy: true, version: "2.0.0", pid: 1 })
if (path === "/experimental/capabilities") return json(route, { backgroundSubagents: true })
if (path === "/provider") return json(route, providerConfig(config))
if (path === "/provider/auth") return json(route, config.integrationMethods ?? {})
const legacyAuth = path.match(/^\/auth\/([^/]+)$/)?.[1]
if (legacyAuth && route.request().method() === "PUT") {
config.onConnectKey?.({ integrationID: legacyAuth, body: route.request().postDataJSON() })
return json(route, true)
}
if (path === "/instance/dispose" && route.request().method() === "POST") {
config.onInstanceDispose?.()
return json(route, true)
}
if (path === "/permission")
return json(route, typeof config.permissions === "function" ? config.permissions() : (config.permissions ?? []))
if (path === "/question")
return json(route, typeof config.questions === "function" ? config.questions() : (config.questions ?? []))
if (path === "/session/status")
return json(
route,
typeof config.sessionStatus === "function" ? config.sessionStatus() : (config.sessionStatus ?? {}),
)
if (path === "/vcs/diff" && config.vcsDiff) return json(route, config.vcsDiff)
if (path === "/file" && config.fileList)
return json(route, await config.fileList(url.searchParams.get("path") ?? ""))
if (path === "/file/content" && config.fileContent)
return json(route, await config.fileContent(url.searchParams.get("path") ?? ""))
if (path === "/find/file" && config.findFiles)
return json(
route,
await config.findFiles({
query: url.searchParams.get("query") ?? "",
dirs: url.searchParams.get("dirs") ?? undefined,
limit: url.searchParams.has("limit") ? Number(url.searchParams.get("limit")) : undefined,
}),
)
if (path === "/api/reference") if (path === "/api/reference")
return json(route, { return json(route, {
location: { location: {
directory: config.directory, directory: config.directory,
project: { id: (config.project as { id?: string }).id, directory: config.directory }, project: {
id: (config.project as { id?: string }).id,
directory: config.directory,
canonical: config.directory,
},
}, },
data: [], data: [],
}) })
@@ -245,6 +182,7 @@ export async function mockOpenCodeServer(page: Page, config: MockServerConfig) {
return json(route, { location: location(config), data: currentDefaultModel(providerConfig(config)) }) return json(route, { location: location(config), data: currentDefaultModel(providerConfig(config)) })
if (path === "/api/integration") return json(route, { location: location(config), data: [] }) if (path === "/api/integration") return json(route, { location: location(config), data: [] })
if (path === "/api/command") return json(route, { location: location(config), data: [] }) if (path === "/api/command") return json(route, { location: location(config), data: [] })
if (path === "/api/skill") return json(route, { location: location(config), data: [] })
if (path === "/api/plugin") return json(route, { location: location(config), data: [] }) if (path === "/api/plugin") return json(route, { location: location(config), data: [] })
if (path === "/api/mcp") return json(route, { location: location(config), data: [] }) if (path === "/api/mcp") return json(route, { location: location(config), data: [] })
if (path === "/api/mcp/resource") if (path === "/api/mcp/resource")
@@ -277,7 +215,11 @@ export async function mockOpenCodeServer(page: Page, config: MockServerConfig) {
]) ])
} }
if (path === "/api/project/current") if (path === "/api/project/current")
return json(route, { id: (config.project as { id?: string }).id, directory: config.directory }) return json(route, {
id: (config.project as { id?: string }).id,
directory: config.directory,
canonical: config.directory,
})
const worktree = path.match(/^\/api\/worktree\/([^/]+)$/)?.[1] const worktree = path.match(/^\/api\/worktree\/([^/]+)$/)?.[1]
if (worktree && route.request().method() === "GET") if (worktree && route.request().method() === "GET")
return json(route, [ return json(route, [
@@ -303,18 +245,13 @@ export async function mockOpenCodeServer(page: Page, config: MockServerConfig) {
currentPermission, currentPermission,
), ),
}) })
if (path === "/api/question/request")
return json(route, {
location: location(config),
data: typeof config.questions === "function" ? config.questions() : (config.questions ?? []),
})
if (path === "/api/form/request") if (path === "/api/form/request")
return json(route, { return json(route, {
location: location(config), location: location(config),
data: typeof config.forms === "function" ? config.forms() : (config.forms ?? []), data: typeof config.forms === "function" ? config.forms() : (config.forms ?? []),
}) })
if (path === "/api/vcs") if (path === "/api/vcs")
return json(route, { location: location(config), data: { branch: "main", defaultBranch: "main" } }) return json(route, { location: location(config), data: { branch: { current: "main", default: "main" } } })
if (path === "/api/vcs/status") return json(route, { location: location(config), data: [] }) if (path === "/api/vcs/status") return json(route, { location: location(config), data: [] })
if (path === "/api/vcs/diff") return json(route, { location: location(config), data: config.vcsDiff ?? [] }) if (path === "/api/vcs/diff") return json(route, { location: location(config), data: config.vcsDiff ?? [] })
if (path === "/api/fs/list" && config.fileList) if (path === "/api/fs/list" && config.fileList)
@@ -352,10 +289,26 @@ export async function mockOpenCodeServer(page: Page, config: MockServerConfig) {
: entries, : entries,
}) })
} }
if (path === "/api/pty/shells") return json(route, { location: location(config), data: [] }) if (path === "/api/shell" && route.request().method() === "GET")
return json(route, { location: location(config), data: [] })
if (/^\/api\/pty\/[^/]+\/connect-token$/.test(path)) if (/^\/api\/pty\/[^/]+\/connect-token$/.test(path))
return json(route, { location: location(config), data: { ticket: "e2e-ticket", expires_in: 60 } }) return json(route, { location: location(config), data: { ticket: "e2e-ticket", expires_in: 60 } })
if (path === "/api/session") { if (path === "/api/session") {
if (route.request().method() === "POST") {
const payload = route.request().postDataJSON() as Record<string, unknown>
const created = currentSession(
{
id: "ses_mock_created",
projectID: (config.project as { id?: string }).id,
title: typeof payload.title === "string" ? payload.title : "New session",
parentID: typeof payload.parentID === "string" ? payload.parentID : undefined,
},
config.directory,
)
config.sessions.push(created)
return json(route, { data: created })
}
if (route.request().method() !== "GET") return route.fallback()
const directory = url.searchParams.get("directory") const directory = url.searchParams.get("directory")
const parentID = url.searchParams.get("parentID") const parentID = url.searchParams.get("parentID")
const limit = Number(url.searchParams.get("limit") ?? 50) const limit = Number(url.searchParams.get("limit") ?? 50)
@@ -365,7 +318,11 @@ export async function mockOpenCodeServer(page: Page, config: MockServerConfig) {
const location = session.location as { directory?: string } | undefined const location = session.location as { directory?: string } | undefined
return !directory || location?.directory === directory || session.directory === directory return !directory || location?.directory === directory || session.directory === directory
}) })
.filter((session) => parentID !== "null" || session.parentID === undefined) .filter((session) => {
if (parentID === null) return true
if (parentID === "null") return session.parentID === undefined
return session.parentID === parentID
})
.filter((session) => { .filter((session) => {
const search = url.searchParams.get("search")?.toLowerCase() const search = url.searchParams.get("search")?.toLowerCase()
return ( return (
@@ -375,7 +332,7 @@ export async function mockOpenCodeServer(page: Page, config: MockServerConfig) {
.includes(search) .includes(search)
) )
}) })
const ordered = url.searchParams.get("order") === "asc" ? sessions.toReversed() : sessions const ordered = url.searchParams.get("order") === "asc" ? sessions : sessions.toReversed()
const data = ordered.slice(offset, offset + limit) const data = ordered.slice(offset, offset + limit)
const next = offset + limit < ordered.length ? String(offset + limit) : undefined const next = offset + limit < ordered.length ? String(offset + limit) : undefined
return json(route, { return json(route, {
@@ -398,9 +355,6 @@ export async function mockOpenCodeServer(page: Page, config: MockServerConfig) {
if (/^\/api\/session\/[^/]+\/shell$/.test(path) && route.request().method() === "POST") { if (/^\/api\/session\/[^/]+\/shell$/.test(path) && route.request().method() === "POST") {
return route.fulfill({ status: 204, headers: { "access-control-allow-origin": "*" } }) return route.fulfill({ status: 204, headers: { "access-control-allow-origin": "*" } })
} }
if (/^\/api\/session\/[^/]+\/question\/[^/]+\/(reply|reject)$/.test(path) && route.request().method() === "POST") {
return route.fulfill({ status: 204, headers: { "access-control-allow-origin": "*" } })
}
const sessionForm = path.match(/^\/api\/session\/([^/]+)\/form$/)?.[1] const sessionForm = path.match(/^\/api\/session\/([^/]+)\/form$/)?.[1]
if (sessionForm && route.request().method() === "GET") { if (sessionForm && route.request().method() === "GET") {
const forms = typeof config.forms === "function" ? config.forms() : (config.forms ?? []) const forms = typeof config.forms === "function" ? config.forms() : (config.forms ?? [])
@@ -413,15 +367,18 @@ export async function mockOpenCodeServer(page: Page, config: MockServerConfig) {
return route.fulfill({ status: 204, headers: { "access-control-allow-origin": "*" } }) return route.fulfill({ status: 204, headers: { "access-control-allow-origin": "*" } })
if (/^\/api\/session\/[^/]+\/inbox$/.test(path) && route.request().method() === "GET") if (/^\/api\/session\/[^/]+\/inbox$/.test(path) && route.request().method() === "GET")
return json(route, { data: [] }) return json(route, { data: [] })
const sessionPermission = path.match(/^\/api\/session\/([^/]+)\/permission$/)?.[1]
if (sessionPermission && route.request().method() === "GET") {
const permissions = typeof config.permissions === "function" ? config.permissions() : (config.permissions ?? [])
return json(route, {
data: permissions.map(currentPermission).filter((permission) => permission.sessionID === sessionPermission),
})
}
if (/^\/api\/session\/[^/]+\/permission\/[^/]+\/reply$/.test(path) && route.request().method() === "POST") { if (/^\/api\/session\/[^/]+\/permission\/[^/]+\/reply$/.test(path) && route.request().method() === "POST") {
return route.fulfill({ status: 204, headers: { "access-control-allow-origin": "*" } }) return route.fulfill({ status: 204, headers: { "access-control-allow-origin": "*" } })
} }
if (/^\/question\/[^/]+\/(reply|reject)$/.test(path) && route.request().method() === "POST")
return json(route, true)
if (/^\/session\/[^/]+\/permissions\/[^/]+$/.test(path) && route.request().method() === "POST")
return json(route, true)
if ( if (
/^\/api\/session\/[^/]+\/(archive|rename|interrupt|revert\/clear|revert\/commit)$/.test(path) && /^\/api\/session\/[^/]+\/(rename|interrupt|revert\/clear|revert\/commit)$/.test(path) &&
route.request().method() === "POST" route.request().method() === "POST"
) { ) {
return route.fulfill({ status: 204, headers: { "access-control-allow-origin": "*" } }) return route.fulfill({ status: 204, headers: { "access-control-allow-origin": "*" } })
@@ -429,10 +386,6 @@ export async function mockOpenCodeServer(page: Page, config: MockServerConfig) {
if (/^\/api\/session\/[^/]+$/.test(path) && route.request().method() === "DELETE") { if (/^\/api\/session\/[^/]+$/.test(path) && route.request().method() === "DELETE") {
return route.fulfill({ status: 204, headers: { "access-control-allow-origin": "*" } }) return route.fulfill({ status: 204, headers: { "access-control-allow-origin": "*" } })
} }
if (emptyObject.has(path)) return json(route, {})
if (emptyList.has(path)) return json(route, [])
if (path in staticRoutes) return json(route, staticRoutes[path])
const currentSessionMatch = path.match(/^\/api\/session\/([^/]+)$/) const currentSessionMatch = path.match(/^\/api\/session\/([^/]+)$/)
if (currentSessionMatch) { if (currentSessionMatch) {
const session = config.sessions.find((item) => item.id === currentSessionMatch[1]) const session = config.sessions.find((item) => item.id === currentSessionMatch[1])
@@ -442,69 +395,37 @@ export async function mockOpenCodeServer(page: Page, config: MockServerConfig) {
}) })
} }
const currentMessageMatch = path.match(/^\/api\/session\/([^/]+)\/message\/([^/]+)$/) const messageMatch = path.match(/^\/api\/session\/([^/]+)\/message\/([^/]+)$/)
if (currentMessageMatch) {
config.onMessage?.({ sessionID: currentMessageMatch[1]!, messageID: currentMessageMatch[2]! })
if (config.messageDelay !== undefined) await new Promise((resolve) => setTimeout(resolve, config.messageDelay))
const message = config.message?.(currentMessageMatch[1]!, currentMessageMatch[2]!)
if (message === undefined) return json(route, { error: "Message not found" }, undefined, 404)
return json(route, { data: currentMessage(message) })
}
const sessionMatch = path.match(/^\/session\/([^/]+)$/)
if (sessionMatch) return json(route, config.sessions.find((session) => session.id === sessionMatch[1]) ?? {})
const projectMatch = path.match(/^\/project\/([^/]+)$/)
if (projectMatch) return json(route, config.project)
const messageMatch = path.match(/^\/session\/([^/]+)\/message\/([^/]+)$/)
if (messageMatch) { if (messageMatch) {
config.onMessage?.({ sessionID: messageMatch[1]!, messageID: messageMatch[2]! }) config.onMessage?.({ sessionID: messageMatch[1]!, messageID: messageMatch[2]! })
if (config.messageDelay !== undefined) await new Promise((resolve) => setTimeout(resolve, config.messageDelay)) if (config.messageDelay !== undefined) await new Promise((resolve) => setTimeout(resolve, config.messageDelay))
const message = config.message?.(messageMatch[1]!, messageMatch[2]!) const message =
config.message?.(messageMatch[1]!, messageMatch[2]!) ??
config.pageMessages(messageMatch[1]!, Number.MAX_SAFE_INTEGER).items.find((item) => item.id === messageMatch[2])
if (message === undefined) return json(route, { error: "Message not found" }, undefined, 404) if (message === undefined) return json(route, { error: "Message not found" }, undefined, 404)
return json(route, message) return json(route, { data: message })
} }
const todoMatch = path.match(/^\/session\/([^/]+)\/todo$/) const messagesMatch = path.match(/^\/api\/session\/([^/]+)\/message$/)
if (todoMatch) return json(route, config.todos?.(todoMatch[1]!) ?? [])
if (/^\/session\/[^/]+\/(children|diff)$/.test(path)) return json(route, [])
const currentMessagesMatch = path.match(/^\/api\/session\/([^/]+)\/message$/)
if (currentMessagesMatch) {
const token = url.searchParams.get("cursor") ?? undefined
const before = token ? cursors.get(token) : undefined
if (token && !before) return json(route, { error: "Invalid cursor" }, undefined, 400)
config.onMessages?.({ sessionID: currentMessagesMatch[1], before, phase: "start" })
await config.beforeMessagesResponse?.({ sessionID: currentMessagesMatch[1]!, before })
if (config.messageDelay !== undefined) await new Promise((resolve) => setTimeout(resolve, config.messageDelay))
const pageData = config.pageMessages(currentMessagesMatch[1], Number(url.searchParams.get("limit") ?? 50), before)
config.onMessages?.({ sessionID: currentMessagesMatch[1], before, phase: "end" })
const cursor = pageData.cursor ? `cursor_${++nextCursor}` : undefined
if (cursor) cursors.set(cursor, pageData.cursor!)
return json(route, {
data: pageData.items.map(currentMessage).reverse(),
cursor: { next: cursor },
})
}
const messagesMatch = path.match(/^\/session\/([^/]+)\/message$/)
if (messagesMatch) { if (messagesMatch) {
const token = url.searchParams.get("before") ?? undefined const token = url.searchParams.get("cursor") ?? undefined
const before = token ? cursors.get(token) : undefined const before = token ? cursors.get(token) : undefined
if (token && !before) return json(route, { error: "Invalid cursor" }, undefined, 400) if (token && !before) return json(route, { error: "Invalid cursor" }, undefined, 400)
config.onMessages?.({ sessionID: messagesMatch[1], before, phase: "start" }) config.onMessages?.({ sessionID: messagesMatch[1], before, phase: "start" })
await config.beforeMessagesResponse?.({ sessionID: messagesMatch[1]!, before }) await config.beforeMessagesResponse?.({ sessionID: messagesMatch[1]!, before })
if (config.messageDelay !== undefined) await new Promise((resolve) => setTimeout(resolve, config.messageDelay)) if (config.messageDelay !== undefined) await new Promise((resolve) => setTimeout(resolve, config.messageDelay))
const pageData = config.pageMessages(messagesMatch[1], Number(url.searchParams.get("limit") ?? 80), before) const pageData = config.pageMessages(messagesMatch[1], Number(url.searchParams.get("limit") ?? 50), before)
config.onMessages?.({ sessionID: messagesMatch[1], before, phase: "end" }) config.onMessages?.({ sessionID: messagesMatch[1], before, phase: "end" })
if (!pageData.cursor) return json(route, pageData.items) const cursor = pageData.cursor ? `cursor_${++nextCursor}` : undefined
const cursor = `cursor_${++nextCursor}` if (cursor) cursors.set(cursor, pageData.cursor!)
cursors.set(cursor, pageData.cursor) return json(route, {
return json(route, pageData.items, { "x-next-cursor": cursor }) data: url.searchParams.get("order") === "asc" ? pageData.items : pageData.items.toReversed(),
cursor: { next: cursor },
})
} }
if (url.port === targetPort && targetPort !== appPort) return json(route, {}) if (url.port === targetPort && targetPort !== appPort)
return json(route, { error: `Unhandled mock route: ${path}` }, undefined, 404)
return route.fallback() return route.fallback()
}) })
} }
@@ -522,13 +443,21 @@ function providerConfig(config: MockServerConfig) {
function currentProviders(value: unknown) { function currentProviders(value: unknown) {
if (!record(value) || !Array.isArray(value.all)) return Array.isArray(value) ? value : [] if (!record(value) || !Array.isArray(value.all)) return Array.isArray(value) ? value : []
return value.all const connected = new Set(
.filter(record) Array.isArray(value.connected) ? value.connected.filter((id) => typeof id === "string") : [],
.flatMap((provider) => )
typeof provider.id === "string" && typeof provider.name === "string" return value.all.filter(record).flatMap((provider) =>
? [{ id: provider.id, name: provider.name, package: provider.id }] typeof provider.id === "string" && typeof provider.name === "string"
: [], ? [
) {
id: provider.id,
name: provider.name,
package: provider.id,
activation: connected.has(provider.id) ? "enabled" : "auto",
},
]
: [],
)
} }
function currentModels(value: unknown) { function currentModels(value: unknown) {
@@ -584,7 +513,7 @@ function currentDefaultModel(value: unknown) {
function currentPermission(value: unknown) { function currentPermission(value: unknown) {
const permission = value as Record<string, unknown> const permission = value as Record<string, unknown>
if (permission.action) return permission if (permission.action) return permission
const tool = permission.tool as { messageID?: string; callID?: string } | undefined const tool = permission.tool as { messageID?: string; callID?: string; id?: string } | undefined
return { return {
id: permission.id, id: permission.id,
sessionID: permission.sessionID, sessionID: permission.sessionID,
@@ -593,7 +522,9 @@ function currentPermission(value: unknown) {
save: permission.always, save: permission.always,
metadata: permission.metadata, metadata: permission.metadata,
source: source:
tool?.messageID && tool.callID ? { type: "tool", messageID: tool.messageID, callID: tool.callID } : undefined, tool?.messageID && (tool.id || tool.callID)
? { type: "tool", messageID: tool.messageID, id: tool.id ?? tool.callID }
: undefined,
} }
} }
@@ -634,202 +565,6 @@ export function currentSession(session: { id: string } & Record<string, unknown>
} }
} }
export function currentMessage(value: unknown): SessionMessageInfo {
if (isCurrentMessage(value)) return value
if (!record(value) || !record(value.info) || !Array.isArray(value.parts)) throw new Error("Invalid message fixture")
const info = value.info
const parts = value.parts.filter(record)
if (typeof info.id !== "string" || !record(info.time) || typeof info.time.created !== "number")
throw new Error("Invalid legacy message fixture")
const time = {
created: info.time.created,
...(typeof info.time.completed === "number" ? { completed: info.time.completed } : {}),
}
if (info.role === "user") {
return {
id: info.id,
type: "user",
time: { created: time.created },
text: parts
.flatMap((part) => (part.type === "text" && typeof part.text === "string" ? [part.text] : []))
.join("\n"),
files: parts.flatMap((part) => (part.type === "file" ? legacyFile(part) : [])),
agents: parts.flatMap((part) => (part.type === "agent" ? legacyAgent(part) : [])),
}
}
if (info.role !== "assistant") throw new Error("Invalid legacy message role")
return {
id: info.id,
type: "assistant",
time,
agent: typeof info.agent === "string" ? info.agent : typeof info.mode === "string" ? info.mode : "build",
model: {
id: typeof info.modelID === "string" ? info.modelID : "model",
providerID: typeof info.providerID === "string" ? info.providerID : "provider",
...(typeof info.variant === "string" ? { variant: info.variant } : {}),
},
content: parts.flatMap((part) => legacyAssistantContent(part, time.created)),
...(typeof info.cost === "number" ? { cost: info.cost } : {}),
...(tokens(info.tokens) ? { tokens: tokens(info.tokens) } : {}),
...(structuredError(info.error) ? { error: structuredError(info.error) } : {}),
...(finish(info.finish) ? { finish: finish(info.finish) } : {}),
}
}
function isCurrentMessage(value: unknown): value is SessionMessageInfo {
return record(value) && typeof value.id === "string" && typeof value.type === "string" && !record(value.info)
}
function legacyFile(part: Record<string, unknown>): PromptFileAttachment[] {
if (typeof part.mime !== "string" || typeof part.url !== "string") return []
const data = part.url.match(/^data:[^,]*;base64,(.*)$/)?.[1] ?? ""
const source = record(part.source) ? part.source : undefined
const sourceText = source && record(source.text) ? source.text : undefined
const mention = mentionFrom(sourceText)
const uri = source?.type === "resource" && typeof source.uri === "string" ? source.uri : part.url
return [
{
data,
mime: part.mime,
source: part.url.startsWith("data:") ? { type: "inline" } : { type: "uri", uri },
...(typeof part.filename === "string" ? { name: part.filename } : {}),
...(mention ? { mention } : {}),
},
]
}
function legacyAgent(part: Record<string, unknown>): PromptAgentAttachment[] {
if (typeof part.name !== "string") return []
const mention = mentionFrom(record(part.source) ? part.source : undefined)
return [{ name: part.name, ...(mention ? { mention } : {}) }]
}
function mentionFrom(value: Record<string, unknown> | undefined) {
if (!value || typeof value.value !== "string" || typeof value.start !== "number" || typeof value.end !== "number")
return
return { text: value.value, start: value.start, end: value.end }
}
function legacyAssistantContent(part: Record<string, unknown>, created: number): SessionMessageAssistant["content"] {
if (part.type === "text" && typeof part.text === "string")
return [
{ type: "text", text: part.text, ...(jsonRecord(part.metadata) ? { state: jsonRecord(part.metadata) } : {}) },
]
if (part.type === "reasoning" && typeof part.text === "string") {
const time = record(part.time) ? part.time : undefined
return [
{
type: "reasoning",
text: part.text,
...(jsonRecord(part.metadata) ? { state: jsonRecord(part.metadata) } : {}),
...(time && typeof time.start === "number"
? {
time: {
created: time.start,
...(typeof time.end === "number" ? { completed: time.end } : {}),
},
}
: {}),
},
]
}
if (part.type !== "tool" || typeof part.id !== "string" || typeof part.tool !== "string" || !record(part.state))
return []
const state = part.state
const time = record(state.time) ? state.time : undefined
const toolTime = {
created: time && typeof time.start === "number" ? time.start : created,
...(time && typeof time.start === "number" ? { ran: time.start } : {}),
...(time && typeof time.end === "number" ? { completed: time.end } : {}),
}
const input = jsonRecord(state.input) ?? {}
const metadata = jsonRecord(state.metadata)
const base = {
type: "tool" as const,
id: typeof part.callID === "string" ? part.callID : part.id,
name: part.tool,
time: toolTime,
...(typeof part.executed === "boolean" ? { executed: part.executed } : {}),
...(jsonRecord(part.providerState) ? { providerState: jsonRecord(part.providerState) } : {}),
...(jsonRecord(part.providerResultState) ? { providerResultState: jsonRecord(part.providerResultState) } : {}),
}
if (state.status === "pending")
return [
{
...base,
state: { status: "streaming", input: typeof state.raw === "string" ? state.raw : JSON.stringify(input) },
},
]
if (state.status === "completed")
return [
{
...base,
state: {
status: "completed",
input,
content: [{ type: "text", text: typeof state.output === "string" ? state.output : "" }],
...(metadata ? { metadata } : {}),
},
},
]
if (state.status === "error")
return [
{
...base,
state: {
status: "error",
input,
error: structuredError(state.error) ?? { type: "ToolError", message: "Tool failed" },
...(metadata ? { metadata } : {}),
},
},
]
return [{ ...base, state: { status: "running", input, metadata: metadata ?? {} } }]
}
function structuredError(value: unknown): SessionStructuredError | undefined {
if (typeof value === "string") return { type: "Error", message: value }
if (!record(value)) return
if (typeof value.type === "string" && typeof value.message === "string")
return { type: value.type, message: value.message }
if (typeof value.name !== "string" || !record(value.data) || typeof value.data.message !== "string") return
return { type: value.name, message: value.data.message }
}
function tokens(value: unknown): SessionMessageAssistant["tokens"] | undefined {
if (!record(value) || !record(value.cache)) return
if (
typeof value.input !== "number" ||
typeof value.output !== "number" ||
typeof value.reasoning !== "number" ||
typeof value.cache.read !== "number" ||
typeof value.cache.write !== "number"
)
return
return {
input: value.input,
output: value.output,
reasoning: value.reasoning,
cache: { read: value.cache.read, write: value.cache.write },
}
}
function finish(value: unknown): SessionMessageAssistant["finish"] | undefined {
if (
value === "stop" ||
value === "length" ||
value === "tool-calls" ||
value === "content-filter" ||
value === "error" ||
value === "unknown"
)
return value
}
function jsonRecord(value: unknown): Record<string, JsonValue> | undefined { function jsonRecord(value: unknown): Record<string, JsonValue> | undefined {
if (!record(value)) return if (!record(value)) return
return Object.fromEntries( return Object.fromEntries(
@@ -871,18 +606,3 @@ function sse(route: Route, events?: unknown[], retry?: number) {
body: `${retry === undefined ? "" : `retry: ${retry}\n\n`}${events?.map((event) => `data: ${JSON.stringify(event)}\n\n`).join("") || ": ok\n\n"}`, body: `${retry === undefined ? "" : `retry: ${retry}\n\n`}${events?.map((event) => `data: ${JSON.stringify(event)}\n\n`).join("") || ": ok\n\n"}`,
}) })
} }
function currentEvent(input: unknown) {
if (!input || typeof input !== "object" || !("payload" in input)) return input
const envelope = input as { directory?: string; payload?: unknown }
if (!envelope.payload || typeof envelope.payload !== "object") return input
const payload = envelope.payload as { id?: string; type?: string; properties?: unknown }
if (!payload.type) return input
return {
id: payload.id ?? `evt_mock_${Date.now()}`,
created: Date.now(),
type: payload.type,
data: payload.properties ?? {},
location: envelope.directory && envelope.directory !== "global" ? { directory: envelope.directory } : undefined,
}
}
+13 -30
View File
@@ -1,4 +1,5 @@
import type { Page } from "@playwright/test" import type { Page } from "@playwright/test"
import type { OpenCodeEvent } from "@opencode-ai/client/promise"
export type SseConnectionRecord = { export type SseConnectionRecord = {
id: number id: number
@@ -27,7 +28,7 @@ export type SseEventOptions = {
marker?: string marker?: string
} }
export type SseTransport<T> = { export type SseTransport<T extends OpenCodeEvent> = {
server: string server: string
waitForConnection(options?: { after?: number; timeout?: number }): Promise<SseConnectionRecord> waitForConnection(options?: { after?: number; timeout?: number }): Promise<SseConnectionRecord>
send(payload: T, options?: SseEventOptions): Promise<SseDeliveryAcknowledgement> send(payload: T, options?: SseEventOptions): Promise<SseDeliveryAcknowledgement>
@@ -55,7 +56,7 @@ type BrowserTransport = Window & {
} }
} }
export async function installSseTransport<T>( export async function installSseTransport<T extends OpenCodeEvent = OpenCodeEvent>(
page: Page, page: Page,
options: { server: string; retry?: number }, options: { server: string; retry?: number },
): Promise<SseTransport<T>> { ): Promise<SseTransport<T>> {
@@ -93,21 +94,6 @@ export async function installSseTransport<T>(
eventOptions.retry === undefined ? "" : `retry: ${eventOptions.retry}\n`, eventOptions.retry === undefined ? "" : `retry: ${eventOptions.retry}\n`,
`data: ${JSON.stringify(payload)}\n\n`, `data: ${JSON.stringify(payload)}\n\n`,
].join("") ].join("")
const currentEvent = (input: unknown) => {
if (!input || typeof input !== "object" || !("payload" in input)) return input
const envelope = input as { directory?: string; payload?: unknown }
if (!envelope.payload || typeof envelope.payload !== "object") return input
const payload = envelope.payload as { id?: string; type?: string; properties?: unknown }
if (!payload.type) return input
return {
id: payload.id ?? `evt_mock_${Date.now()}`,
created: Date.now(),
type: payload.type,
data: payload.properties ?? {},
location:
envelope.directory && envelope.directory !== "global" ? { directory: envelope.directory } : undefined,
}
}
const acknowledge = ( const acknowledge = (
connection: Connection, connection: Connection,
bytes: number, bytes: number,
@@ -155,13 +141,14 @@ export async function installSseTransport<T>(
output.forEach((chunk) => connection.controller.enqueue(chunk)) output.forEach((chunk) => connection.controller.enqueue(chunk))
return acknowledge(connection, input.bytes.length, output.length) return acknowledge(connection, input.bytes.length, output.length)
} }
const encoded = input.deliveries.map((delivery) => { const encoded = input.deliveries.map((delivery) => ({
const payload = connection.path === "/api/event" ? currentEvent(delivery.payload) : delivery.payload delivery,
return { delivery, payload, bytes: encoder.encode(frame(payload, delivery.options)) } payload: delivery.payload,
}) bytes: encoder.encode(frame(delivery.payload, delivery.options)),
}))
encoded.forEach((item) => marker(item.delivery.options?.marker)) encoded.forEach((item) => marker(item.delivery.options?.marker))
if (input.burst) { if (input.burst) {
const bytes = encoder.encode(encoded.map((item) => frame(item.payload, item.delivery.options)).join("")) const bytes = encoder.encode(encoded.map((item) => new TextDecoder().decode(item.bytes)).join(""))
connection.controller.enqueue(bytes) connection.controller.enqueue(bytes)
return encoded.map((item) => acknowledge(connection, item.bytes.byteLength, 1, item.delivery.options?.id)) return encoded.map((item) => acknowledge(connection, item.bytes.byteLength, 1, item.delivery.options?.id))
} }
@@ -266,15 +253,11 @@ export async function installSseTransport<T>(
return command({ type: "send", deliveries: [{ payload, options: eventOptions }], burst: false, cuts: [...cuts] }) return command({ type: "send", deliveries: [{ payload, options: eventOptions }], burst: false, cuts: [...cuts] })
}, },
heartbeat(eventOptions) { heartbeat(eventOptions) {
const bytes = new TextEncoder().encode(": heartbeat\n\n")
return command({ return command({
type: "send", type: "raw",
deliveries: [ bytes: Array.from(bytes),
{ marker: eventOptions?.marker,
payload: { directory: "global", payload: { type: "server.heartbeat", properties: {} } } as T,
options: eventOptions,
},
],
burst: false,
}) })
}, },
writeRaw(value, cuts, marker) { writeRaw(value, cuts, marker) {
+7 -10
View File
@@ -1,6 +1,6 @@
import type { Page, TestInfo } from "@playwright/test" import type { Page, TestInfo } from "@playwright/test"
import { analyzeVisualObservations, analyzeVisualTraceByMarker } from "./visual-stability/analyzer" import { analyzeVisualObservations, analyzeVisualTraceByMarker } from "./visual-stability/analyzer"
import { legacyVisualPlan, type LegacyVisualStabilityOptions } from "./visual-stability/invariant" import { stabilityPlan, type VisualStabilityOptions } from "./visual-stability/invariant"
import type { CapturedFrame, VisualStabilityTrace } from "./visual-stability/model" import type { CapturedFrame, VisualStabilityTrace } from "./visual-stability/model"
import { markVisualProbe, startVisualProbe, stopVisualProbe } from "./visual-stability/probe" import { markVisualProbe, startVisualProbe, stopVisualProbe } from "./visual-stability/probe"
import type { VisualRegionDefinition } from "./visual-stability/regions" import type { VisualRegionDefinition } from "./visual-stability/regions"
@@ -25,22 +25,19 @@ export async function markVisualStability(page: Page, label: string) {
await markVisualProbe(page, label) await markVisualProbe(page, label)
} }
export function analyzeVisualStability(trace: VisualStabilityTrace, options: LegacyVisualStabilityOptions = {}) { export function analyzeVisualStability(trace: VisualStabilityTrace, options: VisualStabilityOptions = {}) {
return analyzeVisualObservations(trace.samples, legacyVisualPlan(options)) return analyzeVisualObservations(trace.samples, stabilityPlan(options))
} }
export function analyzeVisualStabilityByMarker( export function analyzeVisualStabilityByMarker(trace: VisualStabilityTrace, options: VisualStabilityOptions = {}) {
trace: VisualStabilityTrace, return analyzeVisualTraceByMarker(trace, stabilityPlan(options))
options: LegacyVisualStabilityOptions = {},
) {
return analyzeVisualTraceByMarker(trace, legacyVisualPlan(options))
} }
export async function expectVisualStability( export async function expectVisualStability(
testInfo: TestInfo, testInfo: TestInfo,
name: string, name: string,
trace: VisualStabilityTrace, trace: VisualStabilityTrace,
options: LegacyVisualStabilityOptions = {}, options: VisualStabilityOptions = {},
) { ) {
await reportVisualStability( await reportVisualStability(
testInfo, testInfo,
@@ -49,6 +46,6 @@ export async function expectVisualStability(
...trace, ...trace,
frames: (trace as VisualStabilityTrace & { [capturedFrames]?: CapturedFrame[] })[capturedFrames] ?? [], frames: (trace as VisualStabilityTrace & { [capturedFrames]?: CapturedFrame[] })[capturedFrames] ?? [],
}, },
legacyVisualPlan(options), stabilityPlan(options),
) )
} }
@@ -30,7 +30,7 @@ export type VisualPlan<RegionName extends string = string> = {
aggregateMotion?: boolean aggregateMotion?: boolean
} }
export type LegacyVisualStabilityOptions<RegionName extends string = string> = { export type VisualStabilityOptions<RegionName extends string = string> = {
flow?: RegionName[] flow?: RegionName[]
motionTolerance?: number motionTolerance?: number
opacityFloor?: number opacityFloor?: number
@@ -58,8 +58,8 @@ export function visualPlan<const Regions extends Record<string, VisualRegionDefi
return { ...options, regionNames: Object.keys(regions) as Extract<keyof Regions, string>[], invariants } return { ...options, regionNames: Object.keys(regions) as Extract<keyof Regions, string>[], invariants }
} }
export function legacyVisualPlan<RegionName extends string>( export function stabilityPlan<RegionName extends string>(
options: LegacyVisualStabilityOptions<RegionName> = {}, options: VisualStabilityOptions<RegionName> = {},
): VisualPlan<RegionName> { ): VisualPlan<RegionName> {
const inferred = const inferred =
options.inferRequired === false options.inferRequired === false
-1
View File
@@ -21,7 +21,6 @@
<script id="oc-theme-preload-script" src="/oc-theme-preload.js"></script> <script id="oc-theme-preload-script" src="/oc-theme-preload.js"></script>
</head> </head>
<body <body
data-new-layout
class="antialiased overscroll-none font-(family-name:--font-family-text) text-[13px] font-[440] overflow-hidden bg-v2-background-bg-deep" class="antialiased overscroll-none font-(family-name:--font-family-text) text-[13px] font-[440] overflow-hidden bg-v2-background-bg-deep"
> >
<noscript>You need to enable JavaScript to run this app.</noscript> <noscript>You need to enable JavaScript to run this app.</noscript>
-1
View File
@@ -74,7 +74,6 @@
"@solidjs/router": "catalog:", "@solidjs/router": "catalog:",
"@tanstack/solid-query": "5.91.4", "@tanstack/solid-query": "5.91.4",
"@tanstack/solid-virtual": "catalog:", "@tanstack/solid-virtual": "catalog:",
"@thisbeyond/solid-dnd": "0.7.5",
"diff": "catalog:", "diff": "catalog:",
"effect": "catalog:", "effect": "catalog:",
"fuzzysort": "catalog:", "fuzzysort": "catalog:",
-7
View File
@@ -2,13 +2,6 @@
var key = "opencode-theme-id" var key = "opencode-theme-id"
var themeId = localStorage.getItem(key) || "oc-2" var themeId = localStorage.getItem(key) || "oc-2"
if (themeId === "oc-1") {
themeId = "oc-2"
localStorage.setItem(key, themeId)
localStorage.removeItem("opencode-theme-css-light")
localStorage.removeItem("opencode-theme-css-dark")
}
var scheme = localStorage.getItem("opencode-color-scheme") || "system" var scheme = localStorage.getItem("opencode-color-scheme") || "system"
var isDark = scheme === "dark" || (scheme === "system" && matchMedia("(prefers-color-scheme: dark)").matches) var isDark = scheme === "dark" || (scheme === "system" && matchMedia("(prefers-color-scheme: dark)").matches)
var mode = isDark ? "dark" : "light" var mode = isDark ? "dark" : "light"

Some files were not shown because too many files have changed in this diff Show More