Compare commits

..

2 Commits

227 changed files with 10326 additions and 8258 deletions

View File

@@ -1,38 +0,0 @@
name: storybook
on:
push:
branches: [dev]
paths:
- ".github/workflows/storybook.yml"
- "package.json"
- "bun.lock"
- "packages/storybook/**"
- "packages/ui/**"
pull_request:
branches: [dev]
paths:
- ".github/workflows/storybook.yml"
- "package.json"
- "bun.lock"
- "packages/storybook/**"
- "packages/ui/**"
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
name: storybook build
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Bun
uses: ./.github/actions/setup-bun
- name: Build Storybook
run: bun --cwd packages/storybook build

View File

@@ -5,11 +5,6 @@
"options": {},
},
},
"permission": {
"edit": {
"packages/opencode/migration/*": "deny",
},
},
"mcp": {},
"tools": {
"github-triage": false,

View File

@@ -5,8 +5,16 @@ import DESCRIPTION from "./github-triage.txt"
const TEAM = {
desktop: ["adamdotdevin", "iamdavidhill", "Brendonovich", "nexxeln"],
zen: ["fwang", "MrMushrooooom"],
tui: ["thdxr", "kommander", "rekram1-node"],
core: ["thdxr", "rekram1-node", "jlongster"],
tui: [
"thdxr",
"kommander",
// "rekram1-node" (on vacation)
],
core: [
"thdxr",
// "rekram1-node", (on vacation)
"jlongster",
],
docs: ["R44VC0RP"],
windows: ["Hona"],
} as const
@@ -42,10 +50,7 @@ async function githubFetch(endpoint: string, options: RequestInit = {}) {
export default tool({
description: DESCRIPTION,
args: {
assignee: tool.schema
.enum(ASSIGNEES as [string, ...string[]])
.describe("The username of the assignee")
.default("rekram1-node"),
assignee: tool.schema.enum(ASSIGNEES as [string, ...string[]]).describe("The username of the assignee"),
labels: tool.schema
.array(tool.schema.enum(["nix", "opentui", "perf", "web", "desktop", "zen", "docs", "windows", "core"]))
.describe("The labels(s) to add to the issue")
@@ -68,7 +73,8 @@ export default tool({
results.push("Dropped label: nix (issue does not mention nix)")
}
const assignee = nix ? "rekram1-node" : web ? pick(TEAM.desktop) : args.assignee
// const assignee = nix ? "rekram1-node" : web ? pick(TEAM.desktop) : args.assignee
const assignee = web ? pick(TEAM.desktop) : args.assignee
if (labels.includes("zen") && !zen) {
throw new Error("Only add the zen label when issue title/body contains 'zen'")

View File

@@ -4,3 +4,5 @@ Choose labels and assignee using the current triage policy and ownership rules.
Pick the most fitting labels for the issue and assign one owner.
If unsure, choose the team/section with the most overlap with the issue and assign a member from that team at random.
(Note: rekram1-node is on vacation, do not assign issues to him.)

356
bun.lock
View File

@@ -26,7 +26,7 @@
},
"packages/app": {
"name": "@opencode-ai/app",
"version": "1.2.24",
"version": "1.2.21",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*",
@@ -76,7 +76,7 @@
},
"packages/console/app": {
"name": "@opencode-ai/console-app",
"version": "1.2.24",
"version": "1.2.21",
"dependencies": {
"@cloudflare/vite-plugin": "1.15.2",
"@ibm/plex": "6.4.1",
@@ -110,7 +110,7 @@
},
"packages/console/core": {
"name": "@opencode-ai/console-core",
"version": "1.2.24",
"version": "1.2.21",
"dependencies": {
"@aws-sdk/client-sts": "3.782.0",
"@jsx-email/render": "1.1.1",
@@ -137,7 +137,7 @@
},
"packages/console/function": {
"name": "@opencode-ai/console-function",
"version": "1.2.24",
"version": "1.2.21",
"dependencies": {
"@ai-sdk/anthropic": "2.0.0",
"@ai-sdk/openai": "2.0.2",
@@ -161,7 +161,7 @@
},
"packages/console/mail": {
"name": "@opencode-ai/console-mail",
"version": "1.2.24",
"version": "1.2.21",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
@@ -185,7 +185,7 @@
},
"packages/desktop": {
"name": "@opencode-ai/desktop",
"version": "1.2.24",
"version": "1.2.21",
"dependencies": {
"@opencode-ai/app": "workspace:*",
"@opencode-ai/ui": "workspace:*",
@@ -218,7 +218,7 @@
},
"packages/desktop-electron": {
"name": "@opencode-ai/desktop-electron",
"version": "1.2.24",
"version": "1.2.21",
"dependencies": {
"@opencode-ai/app": "workspace:*",
"@opencode-ai/ui": "workspace:*",
@@ -248,7 +248,7 @@
},
"packages/enterprise": {
"name": "@opencode-ai/enterprise",
"version": "1.2.24",
"version": "1.2.21",
"dependencies": {
"@opencode-ai/ui": "workspace:*",
"@opencode-ai/util": "workspace:*",
@@ -277,7 +277,7 @@
},
"packages/function": {
"name": "@opencode-ai/function",
"version": "1.2.24",
"version": "1.2.21",
"dependencies": {
"@octokit/auth-app": "8.0.1",
"@octokit/rest": "catalog:",
@@ -293,7 +293,7 @@
},
"packages/opencode": {
"name": "opencode",
"version": "1.2.24",
"version": "1.2.21",
"bin": {
"opencode": "./bin/opencode",
},
@@ -335,8 +335,8 @@
"@opencode-ai/sdk": "workspace:*",
"@opencode-ai/util": "workspace:*",
"@openrouter/ai-sdk-provider": "1.5.4",
"@opentui/core": "0.1.87",
"@opentui/solid": "0.1.87",
"@opentui/core": "0.1.86",
"@opentui/solid": "0.1.86",
"@parcel/watcher": "2.5.1",
"@pierre/diffs": "catalog:",
"@solid-primitives/event-bus": "1.1.2",
@@ -366,7 +366,6 @@
"opentui-spinner": "0.0.6",
"partial-json": "0.1.7",
"remeda": "catalog:",
"semver": "^7.6.3",
"solid-js": "catalog:",
"strip-ansi": "7.1.2",
"tree-sitter-bash": "0.25.0",
@@ -396,7 +395,6 @@
"@types/babel__core": "7.20.5",
"@types/bun": "catalog:",
"@types/mime-types": "3.0.1",
"@types/semver": "^7.5.8",
"@types/turndown": "5.0.5",
"@types/which": "3.0.4",
"@types/yargs": "17.0.33",
@@ -411,7 +409,7 @@
},
"packages/plugin": {
"name": "@opencode-ai/plugin",
"version": "1.2.24",
"version": "1.2.21",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"zod": "catalog:",
@@ -425,17 +423,13 @@
},
"packages/script": {
"name": "@opencode-ai/script",
"dependencies": {
"semver": "^7.6.3",
},
"devDependencies": {
"@types/bun": "catalog:",
"@types/semver": "^7.5.8",
},
},
"packages/sdk/js": {
"name": "@opencode-ai/sdk",
"version": "1.2.24",
"version": "1.2.21",
"devDependencies": {
"@hey-api/openapi-ts": "0.90.10",
"@tsconfig/node22": "catalog:",
@@ -446,7 +440,7 @@
},
"packages/slack": {
"name": "@opencode-ai/slack",
"version": "1.2.24",
"version": "1.2.21",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"@slack/bolt": "^3.17.1",
@@ -481,7 +475,7 @@
},
"packages/ui": {
"name": "@opencode-ai/ui",
"version": "1.2.24",
"version": "1.2.21",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*",
@@ -527,7 +521,7 @@
},
"packages/util": {
"name": "@opencode-ai/util",
"version": "1.2.24",
"version": "1.2.21",
"dependencies": {
"zod": "catalog:",
},
@@ -538,7 +532,7 @@
},
"packages/web": {
"name": "@opencode-ai/web",
"version": "1.2.24",
"version": "1.2.21",
"dependencies": {
"@astrojs/cloudflare": "12.6.3",
"@astrojs/markdown-remark": "6.3.1",
@@ -1092,8 +1086,6 @@
"@fontsource/inter": ["@fontsource/inter@5.2.8", "", {}, "sha512-P6r5WnJoKiNVV+zvW2xM13gNdFhAEpQ9dQJHt3naLvfg+LkF2ldgSLiF4T41lf1SQCM9QmkqPTn4TH568IRagg=="],
"@gar/promisify": ["@gar/promisify@1.1.3", "", {}, "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw=="],
"@gitlab/gitlab-ai-provider": ["@gitlab/gitlab-ai-provider@3.6.0", "", { "dependencies": { "@anthropic-ai/sdk": "^0.71.0", "@anycable/core": "^0.9.2", "graphql-request": "^6.1.0", "isomorphic-ws": "^5.0.0", "openai": "^6.16.0", "socket.io-client": "^4.8.1", "vscode-jsonrpc": "^8.2.1", "zod": "^3.25.76" }, "peerDependencies": { "@ai-sdk/provider": ">=2.0.0", "@ai-sdk/provider-utils": ">=3.0.0" } }, "sha512-8LmcIQ86xkMtC7L4P1/QYVEC+yKMTRerfPeniaaQGalnzXKtX6iMHLjLPOL9Rxp55lOXi6ed0WrFuJzZx+fNRg=="],
"@gitlab/opencode-gitlab-auth": ["@gitlab/opencode-gitlab-auth@1.3.3", "", { "dependencies": { "@fastify/rate-limit": "^10.2.0", "@opencode-ai/plugin": "*", "fastify": "^5.2.0", "open": "^10.0.0" } }, "sha512-FT+KsCmAJjtqWr1YAq0MywGgL9kaLQ4apmsoowAXrPqHtoYf2i/nY10/A+L06kNj22EATeEDRpbB1NWXMto/SA=="],
@@ -1332,9 +1324,7 @@
"@npmcli/agent": ["@npmcli/agent@3.0.0", "", { "dependencies": { "agent-base": "^7.1.0", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.1", "lru-cache": "^10.0.1", "socks-proxy-agent": "^8.0.3" } }, "sha512-S79NdEgDQd/NGCay6TCoVzXSj74skRZIKJcpJjC5lOq34SZzyI6MqtiiWoiVWoVrTcGjNeC4ipbh1VIHlpfF5Q=="],
"@npmcli/fs": ["@npmcli/fs@1.1.1", "", { "dependencies": { "@gar/promisify": "^1.0.1", "semver": "^7.3.5" } }, "sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ=="],
"@npmcli/move-file": ["@npmcli/move-file@1.1.2", "", { "dependencies": { "mkdirp": "^1.0.4", "rimraf": "^3.0.2" } }, "sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg=="],
"@npmcli/fs": ["@npmcli/fs@4.0.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-/xGlezI6xfGO9NwuJlnwz/K14qD1kCSAGtacBHnGzeAIuJGazcp45KP5NuyARXoKb7cwulAGWVsbeSxdG/cb0Q=="],
"@octokit/auth-app": ["@octokit/auth-app@8.0.1", "", { "dependencies": { "@octokit/auth-oauth-app": "^9.0.1", "@octokit/auth-oauth-user": "^6.0.0", "@octokit/request": "^10.0.2", "@octokit/request-error": "^7.0.0", "@octokit/types": "^14.0.0", "toad-cache": "^3.7.0", "universal-github-app-jwt": "^2.2.0", "universal-user-agent": "^7.0.0" } }, "sha512-P2J5pB3pjiGwtJX4WqJVYCtNkcZ+j5T2Wm14aJAEIC3WJOrv12jvBley3G1U/XI8q9o1A7QMG54LiFED2BiFlg=="],
@@ -1422,21 +1412,21 @@
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
"@opentui/core": ["@opentui/core@0.1.87", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "marked": "17.0.1", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.87", "@opentui/core-darwin-x64": "0.1.87", "@opentui/core-linux-arm64": "0.1.87", "@opentui/core-linux-x64": "0.1.87", "@opentui/core-win32-arm64": "0.1.87", "@opentui/core-win32-x64": "0.1.87", "bun-webgpu": "0.1.5", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-dhsmMv0IqKftwG7J/pBrLBj2armsYIg5R3LBvciRQI/6X89GufP4l1u0+QTACAx6iR4SYJJNVNQ2tdX8LM9rMw=="],
"@opentui/core": ["@opentui/core@0.1.86", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "marked": "17.0.1", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.86", "@opentui/core-darwin-x64": "0.1.86", "@opentui/core-linux-arm64": "0.1.86", "@opentui/core-linux-x64": "0.1.86", "@opentui/core-win32-arm64": "0.1.86", "@opentui/core-win32-x64": "0.1.86", "bun-webgpu": "0.1.5", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-3tRLbI9ADrQE1jEEn4x2aJexEOQZkv9Emk2BixMZqxfVhz2zr2SxtpimDAX0vmZK3+GnWAwBWxuaCAsxZpY4+w=="],
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.87", "", { "os": "darwin", "cpu": "arm64" }, "sha512-G8oq85diOfkU6n0T1CxCle7oDmpKxwhcdhZ9khBMU5IrfLx9ZDuCM3F6MsiRQWdvPPCq2oomNbd64bYkPamYgw=="],
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.86", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Zp7q64+d+Dcx6YrH3mRcnHq8EOBnrfc1RvjgSWLhpXr49hY6LzuhqpfZM57aGErPYlR+ff8QM6e5FUkFnDfyjw=="],
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.87", "", { "os": "darwin", "cpu": "x64" }, "sha512-MYTFQfOHm6qO7YaY4GHK9u/oJlXY6djaaxl5I+k4p2mk3vvuFIl/AP1ypITwBFjyV5gyp7PRWFp4nGfY9oN8bw=="],
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.86", "", { "os": "darwin", "cpu": "x64" }, "sha512-NcxfjCJm1kLnTMVOpAPdRYNi8W8XdAXNa6N7i9khiVFrl2v5KRQfUjbrSOUYVxFJNc3jKFG6rsn3jEApvn92qA=="],
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.87", "", { "os": "linux", "cpu": "arm64" }, "sha512-he8o1h5M6oskRJ7wE+xKJgmWnv5ZwN6gB3M/Z+SeHtOMPa5cZmi3TefTjG54llEgFfx0F9RcqHof7TJ/GNxRkw=="],
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.86", "", { "os": "linux", "cpu": "arm64" }, "sha512-EDHAvqSOr8CXzbDvo1aE5blJ6wu1aSbR2LqoXtoeXHemr2T2W42D2TdIWewG6K+/BuRbzZnqt9wnYFBksLW6lw=="],
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.87", "", { "os": "linux", "cpu": "x64" }, "sha512-aiUwjPlH4yDcB8/6YDKSmMkaoGAAltL0Xo0AzXyAtJXWK5tkCSaYjEVwzJ/rYRkr4Magnad+Mjth4AQUWdR2AA=="],
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.86", "", { "os": "linux", "cpu": "x64" }, "sha512-VBaBkVdQDxYV4WcKjb+jgyMS5PiVHepvfaoKWpz1Bq+J01xXW4XPcXyPGkgR1+2R93KzaugEnLscTW4mWtLHlQ=="],
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.87", "", { "os": "win32", "cpu": "arm64" }, "sha512-cmP0pOyREjWGniHqbDmaMY7U+1AyagrD8VseJbU0cGpNgVpG2/gbrJUGdfdLB0SNb+mzLdx6SOjdxtrElwRCQA=="],
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.86", "", { "os": "win32", "cpu": "arm64" }, "sha512-xKbT7sEKYKGwUPkoqmLfHjbJU+vwHPDwf/r/mIunL41JXQBB35CSZ3/QgIwpp2kkteu7oE1tdBdg15ogUU4OMg=="],
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.87", "", { "os": "win32", "cpu": "x64" }, "sha512-N2GErAAP8iODf2RPp86pilPaVKiD6G4pkpZL5nLGbKsl0bndrVTpSqZcn8+/nQwFZDPD/AsiRTYNOfWOblhzOw=="],
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.86", "", { "os": "win32", "cpu": "x64" }, "sha512-HRfgAUlcu71/MrtgfX4Gj7PsDtfXZiuC506Pkn1OnRN1Xomcu10BVRDweUa0/g8ldU9i9kLjMGGnpw6/NjaBFg=="],
"@opentui/solid": ["@opentui/solid@0.1.87", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.87", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.9", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.9" } }, "sha512-lRT9t30l8+FtgOjjWJcdb2MT6hP8/RKqwGgYwTI7fXrOqdhxxwdP2SM+rH2l3suHeASheiTdlvPAo230iUcsvg=="],
"@opentui/solid": ["@opentui/solid@0.1.86", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.86", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.9", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.9" } }, "sha512-pOZC9dlZIH+bpstVVZ2AvYukBnslZTKSl/y5H8FWcMTHGv/BzpGxXBxstL65E/IQASqPFbvFcs7yMRzdLhynmA=="],
"@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="],
@@ -2008,8 +1998,6 @@
"@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="],
"@tootallnate/once": ["@tootallnate/once@1.1.2", "", {}, "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw=="],
"@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=="],
@@ -2116,8 +2104,6 @@
"@types/scheduler": ["@types/scheduler@0.26.0", "", {}, "sha512-WFHp9YUJQ6CKshqoC37iOlHnQSmxNc795UhB26CyBBttrN9svdIrUjl/NjnNmfcwtncN0h/0PPAFWv9ovP8mLA=="],
"@types/semver": ["@types/semver@7.7.1", "", {}, "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA=="],
"@types/send": ["@types/send@1.2.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ=="],
"@types/serve-static": ["@types/serve-static@1.15.10", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*", "@types/send": "<1" } }, "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw=="],
@@ -2226,8 +2212,6 @@
"agentkeepalive": ["agentkeepalive@4.6.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="],
"aggregate-error": ["aggregate-error@3.1.0", "", { "dependencies": { "clean-stack": "^2.0.0", "indent-string": "^4.0.0" } }, "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA=="],
"ai": ["ai@5.0.124", "", { "dependencies": { "@ai-sdk/gateway": "2.0.30", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Li6Jw9F9qsvFJXZPBfxj38ddP2iURCnMs96f9Q3OeQzrDVcl1hvtwSEAuxA/qmfh6SDV2ERqFUOFzigvr0697g=="],
"ai-gateway-provider": ["ai-gateway-provider@2.3.1", "", { "dependencies": { "@ai-sdk/provider": "^2.0.0", "@ai-sdk/provider-utils": "^3.0.19", "ai": "^5.0.116" }, "optionalDependencies": { "@ai-sdk/amazon-bedrock": "^3.0.71", "@ai-sdk/anthropic": "^2.0.56", "@ai-sdk/azure": "^2.0.90", "@ai-sdk/cerebras": "^1.0.33", "@ai-sdk/cohere": "^2.0.21", "@ai-sdk/deepgram": "^1.0.21", "@ai-sdk/deepseek": "^1.0.32", "@ai-sdk/elevenlabs": "^1.0.21", "@ai-sdk/fireworks": "^1.0.30", "@ai-sdk/google": "^2.0.51", "@ai-sdk/google-vertex": "3.0.90", "@ai-sdk/groq": "^2.0.33", "@ai-sdk/mistral": "^2.0.26", "@ai-sdk/openai": "^2.0.88", "@ai-sdk/perplexity": "^2.0.22", "@ai-sdk/xai": "^2.0.42", "@openrouter/ai-sdk-provider": "^1.5.3" }, "peerDependencies": { "@ai-sdk/openai-compatible": "^1.0.29" } }, "sha512-PqI6TVNEDNwr7kOhy7XUGnA8XJB1SpeA9aLqGjr0CyWkKgH+y+ofPm8MZGZ74DOwVejDF+POZq0Qs9jKEKUeYg=="],
@@ -2260,16 +2244,12 @@
"app-builder-lib": ["app-builder-lib@26.8.1", "", { "dependencies": { "@develar/schema-utils": "~2.6.5", "@electron/asar": "3.4.1", "@electron/fuses": "^1.8.0", "@electron/get": "^3.0.0", "@electron/notarize": "2.5.0", "@electron/osx-sign": "1.3.3", "@electron/rebuild": "^4.0.3", "@electron/universal": "2.0.3", "@malept/flatpak-bundler": "^0.4.0", "@types/fs-extra": "9.0.13", "async-exit-hook": "^2.0.1", "builder-util": "26.8.1", "builder-util-runtime": "9.5.1", "chromium-pickle-js": "^0.2.0", "ci-info": "4.3.1", "debug": "^4.3.4", "dotenv": "^16.4.5", "dotenv-expand": "^11.0.6", "ejs": "^3.1.8", "electron-publish": "26.8.1", "fs-extra": "^10.1.0", "hosted-git-info": "^4.1.0", "isbinaryfile": "^5.0.0", "jiti": "^2.4.2", "js-yaml": "^4.1.0", "json5": "^2.2.3", "lazy-val": "^1.0.5", "minimatch": "^10.0.3", "plist": "3.1.0", "proper-lockfile": "^4.1.2", "resedit": "^1.7.0", "semver": "~7.7.3", "tar": "^7.5.7", "temp-file": "^3.4.0", "tiny-async-pool": "1.3.0", "which": "^5.0.0" }, "peerDependencies": { "dmg-builder": "26.8.1", "electron-builder-squirrel-windows": "26.8.1" } }, "sha512-p0Im/Dx5C4tmz8QEE1Yn4MkuPC8PrnlRneMhWJj7BBXQfNTJUshM/bp3lusdEsDbvvfJZpXWnYesgSLvwtM2Zw=="],
"aproba": ["aproba@2.1.0", "", {}, "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew=="],
"archiver": ["archiver@7.0.1", "", { "dependencies": { "archiver-utils": "^5.0.2", "async": "^3.2.4", "buffer-crc32": "^1.0.0", "readable-stream": "^4.0.0", "readdir-glob": "^1.1.2", "tar-stream": "^3.0.0", "zip-stream": "^6.0.1" } }, "sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ=="],
"archiver-utils": ["archiver-utils@5.0.2", "", { "dependencies": { "glob": "^10.0.0", "graceful-fs": "^4.2.0", "is-stream": "^2.0.1", "lazystream": "^1.0.0", "lodash": "^4.17.15", "normalize-path": "^3.0.0", "readable-stream": "^4.0.0" } }, "sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA=="],
"arctic": ["arctic@2.3.4", "", { "dependencies": { "@oslojs/crypto": "1.0.1", "@oslojs/encoding": "1.1.0", "@oslojs/jwt": "0.2.0" } }, "sha512-+p30BOWsctZp+CVYCt7oAean/hWGW42sH5LAcRQX56ttEkFJWbzXBhmSpibbzwSJkRrotmsA+oAoJoVsU0f5xA=="],
"are-we-there-yet": ["are-we-there-yet@3.0.1", "", { "dependencies": { "delegates": "^1.0.0", "readable-stream": "^3.6.0" } }, "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg=="],
"arg": ["arg@5.0.2", "", {}, "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="],
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
@@ -2372,8 +2352,6 @@
"binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="],
"bindings": ["bindings@1.5.0", "", { "dependencies": { "file-uri-to-path": "1.0.0" } }, "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ=="],
"bl": ["bl@6.1.6", "", { "dependencies": { "@types/readable-stream": "^4.0.0", "buffer": "^6.0.3", "inherits": "^2.0.4", "readable-stream": "^4.2.0" } }, "sha512-jLsPgN/YSvPUg9UX0Kd73CXpm2Psg9FxMeCSXnk3WBO3CMT10JMwijubhGfHCnFu6TPn1ei3b975dxv7K2pWVg=="],
"blake3-wasm": ["blake3-wasm@2.1.5", "", {}, "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g=="],
@@ -2442,7 +2420,7 @@
"cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="],
"cacache": ["cacache@15.3.0", "", { "dependencies": { "@npmcli/fs": "^1.0.0", "@npmcli/move-file": "^1.0.1", "chownr": "^2.0.0", "fs-minipass": "^2.0.0", "glob": "^7.1.4", "infer-owner": "^1.0.4", "lru-cache": "^6.0.0", "minipass": "^3.1.1", "minipass-collect": "^1.0.2", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.2", "mkdirp": "^1.0.3", "p-map": "^4.0.0", "promise-inflight": "^1.0.1", "rimraf": "^3.0.2", "ssri": "^8.0.1", "tar": "^6.0.2", "unique-filename": "^1.1.1" } }, "sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ=="],
"cacache": ["cacache@19.0.1", "", { "dependencies": { "@npmcli/fs": "^4.0.0", "fs-minipass": "^3.0.0", "glob": "^10.2.2", "lru-cache": "^10.0.1", "minipass": "^7.0.3", "minipass-collect": "^2.0.1", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "p-map": "^7.0.2", "ssri": "^12.0.0", "tar": "^7.4.3", "unique-filename": "^4.0.0" } }, "sha512-hdsUxulXCi5STId78vRVYEtDAjq99ICAUktLTeTYsLoTE6Z8dS0c8pWNCxwdrk9YfJeobDZc2Y186hD/5ZQgFQ=="],
"cacheable-lookup": ["cacheable-lookup@5.0.4", "", {}, "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA=="],
@@ -2500,8 +2478,6 @@
"clean-css": ["clean-css@5.3.3", "", { "dependencies": { "source-map": "~0.6.0" } }, "sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg=="],
"clean-stack": ["clean-stack@2.2.0", "", {}, "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A=="],
"cli-boxes": ["cli-boxes@3.0.0", "", {}, "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g=="],
"cli-cursor": ["cli-cursor@3.1.0", "", { "dependencies": { "restore-cursor": "^3.1.0" } }, "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw=="],
@@ -2558,8 +2534,6 @@
"consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="],
"console-control-strings": ["console-control-strings@1.1.0", "", {}, "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ=="],
"content-disposition": ["content-disposition@0.5.4", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ=="],
"content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
@@ -2626,8 +2600,6 @@
"deep-eql": ["deep-eql@5.0.2", "", {}, "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q=="],
"deep-extend": ["deep-extend@0.6.0", "", {}, "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="],
"deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="],
"default-browser": ["default-browser@5.5.0", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw=="],
@@ -2648,8 +2620,6 @@
"delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="],
"delegates": ["delegates@1.0.0", "", {}, "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ=="],
"denque": ["denque@2.1.0", "", {}, "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw=="],
"depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
@@ -2852,8 +2822,6 @@
"exit-hook": ["exit-hook@2.2.1", "", {}, "sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw=="],
"expand-template": ["expand-template@2.0.3", "", {}, "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg=="],
"expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="],
"exponential-backoff": ["exponential-backoff@3.1.3", "", {}, "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA=="],
@@ -2908,8 +2876,6 @@
"file-type": ["file-type@16.5.4", "", { "dependencies": { "readable-web-to-node-stream": "^3.0.0", "strtok3": "^6.2.4", "token-types": "^4.1.1" } }, "sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw=="],
"file-uri-to-path": ["file-uri-to-path@1.0.0", "", {}, "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw=="],
"filelist": ["filelist@1.0.6", "", { "dependencies": { "minimatch": "^5.0.1" } }, "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA=="],
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
@@ -2952,11 +2918,9 @@
"fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="],
"fs-constants": ["fs-constants@1.0.0", "", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="],
"fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="],
"fs-minipass": ["fs-minipass@2.1.0", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg=="],
"fs-minipass": ["fs-minipass@3.0.3", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw=="],
"fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="],
@@ -2970,8 +2934,6 @@
"fuzzysort": ["fuzzysort@3.1.0", "", {}, "sha512-sR9BNCjBg6LNgwvxlBd0sBABvQitkLzoVY9MYYROQVX/FvfJ4Mai9LsGhDgd8qYdds0bY77VzYd5iuB+v5rwQQ=="],
"gauge": ["gauge@4.0.4", "", { "dependencies": { "aproba": "^1.0.3 || ^2.0.0", "color-support": "^1.1.3", "console-control-strings": "^1.1.0", "has-unicode": "^2.0.1", "signal-exit": "^3.0.7", "string-width": "^4.2.3", "strip-ansi": "^6.0.1", "wide-align": "^1.1.5" } }, "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg=="],
"gaxios": ["gaxios@7.1.3", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "node-fetch": "^3.3.2", "rimraf": "^5.0.1" } }, "sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ=="],
"gcp-metadata": ["gcp-metadata@8.1.2", "", { "dependencies": { "gaxios": "^7.0.0", "google-logging-utils": "^1.0.0", "json-bigint": "^1.0.0" } }, "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg=="],
@@ -3006,8 +2968,6 @@
"giget": ["giget@2.0.0", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.0", "defu": "^6.1.4", "node-fetch-native": "^1.6.6", "nypm": "^0.6.0", "pathe": "^2.0.3" }, "bin": { "giget": "dist/cli.mjs" } }, "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA=="],
"github-from-package": ["github-from-package@0.0.0", "", {}, "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw=="],
"github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="],
"glob": ["glob@13.0.5", "", { "dependencies": { "minimatch": "^10.2.1", "minipass": "^7.1.2", "path-scurry": "^2.0.0" } }, "sha512-BzXxZg24Ibra1pbQ/zE7Kys4Ua1ks7Bn6pKLkVPZ9FZe4JQS6/Q7ef3LG1H+k7lUf5l4T3PLSyYyYJVYUvfgTw=="],
@@ -3056,8 +3016,6 @@
"has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="],
"has-unicode": ["has-unicode@2.0.1", "", {}, "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ=="],
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
"hast-util-embedded": ["hast-util-embedded@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-is-element": "^3.0.0" } }, "sha512-naH8sld4Pe2ep03qqULEtvYr7EjrLK2QHY8KJR6RJkTUjPGObe1vnx585uzem2hGra+s1q08DZZpfgDVYRbaXA=="],
@@ -3164,8 +3122,6 @@
"indent-string": ["indent-string@4.0.0", "", {}, "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg=="],
"infer-owner": ["infer-owner@1.0.4", "", {}, "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A=="],
"inflight": ["inflight@1.0.6", "", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="],
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
@@ -3236,8 +3192,6 @@
"is-interactive": ["is-interactive@1.0.0", "", {}, "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w=="],
"is-lambda": ["is-lambda@1.0.1", "", {}, "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ=="],
"is-map": ["is-map@2.0.3", "", {}, "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw=="],
"is-negative-zero": ["is-negative-zero@2.0.3", "", {}, "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw=="],
@@ -3454,7 +3408,7 @@
"magicast": ["magicast@0.3.5", "", { "dependencies": { "@babel/parser": "^7.25.4", "@babel/types": "^7.25.4", "source-map-js": "^1.2.0" } }, "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ=="],
"make-fetch-happen": ["make-fetch-happen@9.1.0", "", { "dependencies": { "agentkeepalive": "^4.1.3", "cacache": "^15.2.0", "http-cache-semantics": "^4.1.0", "http-proxy-agent": "^4.0.1", "https-proxy-agent": "^5.0.0", "is-lambda": "^1.0.1", "lru-cache": "^6.0.0", "minipass": "^3.1.3", "minipass-collect": "^1.0.2", "minipass-fetch": "^1.3.2", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "negotiator": "^0.6.2", "promise-retry": "^2.0.1", "socks-proxy-agent": "^6.0.0", "ssri": "^8.0.0" } }, "sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg=="],
"make-fetch-happen": ["make-fetch-happen@14.0.3", "", { "dependencies": { "@npmcli/agent": "^3.0.0", "cacache": "^19.0.1", "http-cache-semantics": "^4.1.1", "minipass": "^7.0.2", "minipass-fetch": "^4.0.0", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "negotiator": "^1.0.0", "proc-log": "^5.0.0", "promise-retry": "^2.0.1", "ssri": "^12.0.0" } }, "sha512-QMjGbFTP0blj97EeidG5hk/QhKQ3T4ICckQGLgz38QF7Vgbk6e6FTARN8KhKxyBbWn8R0HU+bnw8aSoFPD4qtQ=="],
"markdown-extensions": ["markdown-extensions@2.0.0", "", {}, "sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q=="],
@@ -3618,9 +3572,9 @@
"minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="],
"minipass-collect": ["minipass-collect@1.0.2", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA=="],
"minipass-collect": ["minipass-collect@2.0.1", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw=="],
"minipass-fetch": ["minipass-fetch@1.4.1", "", { "dependencies": { "minipass": "^3.1.0", "minipass-sized": "^1.0.3", "minizlib": "^2.0.0" }, "optionalDependencies": { "encoding": "^0.1.12" } }, "sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw=="],
"minipass-fetch": ["minipass-fetch@4.0.1", "", { "dependencies": { "minipass": "^7.0.3", "minipass-sized": "^1.0.3", "minizlib": "^3.0.1" }, "optionalDependencies": { "encoding": "^0.1.13" } }, "sha512-j7U11C5HXigVuutxebFadoYBbd7VSdZWggSe64NVdvWNBqGAiXPL2QVCehjmw7lY1oF9gOllYbORh+hiNgfPgQ=="],
"minipass-flush": ["minipass-flush@1.0.5", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw=="],
@@ -3632,8 +3586,6 @@
"mkdirp": ["mkdirp@0.5.6", "", { "dependencies": { "minimist": "^1.2.6" }, "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw=="],
"mkdirp-classic": ["mkdirp-classic@0.5.3", "", {}, "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="],
"morphdom": ["morphdom@2.7.8", "", {}, "sha512-D/fR4xgGUyVRbdMGU6Nejea1RFzYxYtyurG4Fbv2Fi/daKlWKuXGLOdXtl+3eIwL110cI2hz1ZojGICjjFLgTg=="],
"motion": ["motion@12.34.5", "", { "dependencies": { "framer-motion": "^12.34.5", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-N06NLJ9IeBHeielRqIvYvjPfXuRdyTxa+9++BgpGa+hY2D7TcMkI6QzV3jaRuv0aZRXgMa7cPy9YcBUBisPzAQ=="],
@@ -3664,8 +3616,6 @@
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
"napi-build-utils": ["napi-build-utils@2.0.0", "", {}, "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA=="],
"native-duplexpair": ["native-duplexpair@1.0.0", "", {}, "sha512-E7QQoM+3jvNtlmyfqRZ0/U75VFgCls+fSkbml2MpgWkWyz3ox8Y58gNhfuziuQYGNNQAbFZJQck55LHCnCK6CA=="],
"negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="],
@@ -3680,7 +3630,7 @@
"no-case": ["no-case@3.0.4", "", { "dependencies": { "lower-case": "^2.0.2", "tslib": "^2.0.3" } }, "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg=="],
"node-abi": ["node-abi@3.87.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ=="],
"node-abi": ["node-abi@4.26.0", "", { "dependencies": { "semver": "^7.6.3" } }, "sha512-8QwIZqikRvDIkXS2S93LjzhsSPJuIbfaMETWH+Bx8oOT9Sa9UsUtBFQlc3gBNd1+QINjaTloitXr1W3dQLi9Iw=="],
"node-addon-api": ["node-addon-api@7.1.1", "", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="],
@@ -3692,7 +3642,7 @@
"node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="],
"node-gyp": ["node-gyp@8.4.1", "", { "dependencies": { "env-paths": "^2.2.0", "glob": "^7.1.4", "graceful-fs": "^4.2.6", "make-fetch-happen": "^9.1.0", "nopt": "^5.0.0", "npmlog": "^6.0.0", "rimraf": "^3.0.2", "semver": "^7.3.5", "tar": "^6.1.2", "which": "^2.0.2" }, "bin": { "node-gyp": "bin/node-gyp.js" } }, "sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w=="],
"node-gyp": ["node-gyp@11.5.0", "", { "dependencies": { "env-paths": "^2.2.0", "exponential-backoff": "^3.1.1", "graceful-fs": "^4.2.6", "make-fetch-happen": "^14.0.3", "nopt": "^8.0.0", "proc-log": "^5.0.0", "semver": "^7.3.5", "tar": "^7.4.3", "tinyglobby": "^0.2.12", "which": "^5.0.0" }, "bin": { "node-gyp": "bin/node-gyp.js" } }, "sha512-ra7Kvlhxn5V9Slyus0ygMa2h+UqExPqUIkfk7Pc8QTLT956JLSy51uWFwHtIYy0vI8cB4BDhc/S03+880My/LQ=="],
"node-gyp-build": ["node-gyp-build@4.8.4", "", { "bin": { "node-gyp-build": "bin.js", "node-gyp-build-optional": "optional.js", "node-gyp-build-test": "build-test.js" } }, "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ=="],
@@ -3710,8 +3660,6 @@
"npm-run-path": ["npm-run-path@5.3.0", "", { "dependencies": { "path-key": "^4.0.0" } }, "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ=="],
"npmlog": ["npmlog@6.0.2", "", { "dependencies": { "are-we-there-yet": "^3.0.0", "console-control-strings": "^1.1.0", "gauge": "^4.0.3", "set-blocking": "^2.0.0" } }, "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg=="],
"nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="],
"nypm": ["nypm@0.6.5", "", { "dependencies": { "citty": "^0.2.0", "pathe": "^2.0.3", "tinyexec": "^1.0.2" }, "bin": { "nypm": "dist/cli.mjs" } }, "sha512-K6AJy1GMVyfyMXRVB88700BJqNUkByijGJM8kEHpLdcAt+vSQAVfkWWHYzuRXHSY6xA2sNc5RjTj0p9rE2izVQ=="],
@@ -3780,7 +3728,7 @@
"p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="],
"p-map": ["p-map@4.0.0", "", { "dependencies": { "aggregate-error": "^3.0.0" } }, "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ=="],
"p-map": ["p-map@7.0.4", "", {}, "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ=="],
"p-queue": ["p-queue@8.1.1", "", { "dependencies": { "eventemitter3": "^5.0.1", "p-timeout": "^6.1.2" } }, "sha512-aNZ+VfjobsWryoiPnEApGGmf5WmNsCo9xu8dfaYamG5qaLP7ClhLN6NgsFe6SwJ2UbLEBK5dv9x8Mn5+RVhMWQ=="],
@@ -3912,8 +3860,6 @@
"powershell-utils": ["powershell-utils@0.1.0", "", {}, "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A=="],
"prebuild-install": ["prebuild-install@7.1.3", "", { "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", "github-from-package": "0.0.0", "minimist": "^1.2.3", "mkdirp-classic": "^0.5.3", "napi-build-utils": "^2.0.0", "node-abi": "^3.3.0", "pump": "^3.0.0", "rc": "^1.2.7", "simple-get": "^4.0.0", "tar-fs": "^2.0.0", "tunnel-agent": "^0.6.0" }, "bin": { "prebuild-install": "bin.js" } }, "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug=="],
"prettier": ["prettier@3.6.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="],
"pretty": ["pretty@2.0.0", "", { "dependencies": { "condense-newlines": "^0.2.1", "extend-shallow": "^2.0.1", "js-beautify": "^1.6.12" } }, "sha512-G9xUchgTEiNpormdYBl+Pha50gOUovT18IvAe7EYMZ1/f9W/WWMPRn+xI68yXNMUk3QXHDwo/1wV/4NejVNe1w=="],
@@ -3932,8 +3878,6 @@
"progress": ["progress@2.0.3", "", {}, "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA=="],
"promise-inflight": ["promise-inflight@1.0.1", "", {}, "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g=="],
"promise-retry": ["promise-retry@2.0.1", "", { "dependencies": { "err-code": "^2.0.2", "retry": "^0.12.0" } }, "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g=="],
"promise.allsettled": ["promise.allsettled@1.0.7", "", { "dependencies": { "array.prototype.map": "^1.0.5", "call-bind": "^1.0.2", "define-properties": "^1.2.0", "es-abstract": "^1.22.1", "get-intrinsic": "^1.2.1", "iterate-value": "^1.0.2" } }, "sha512-hezvKvQQmsFkOdrZfYxUxkyxl8mgFQeT259Ajj9PXdbg9VzBCWrItOev72JyWxkCD5VSSqAeHmlN3tWx4DlmsA=="],
@@ -3972,8 +3916,6 @@
"raw-body": ["raw-body@2.5.3", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "unpipe": "~1.0.0" } }, "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA=="],
"rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="],
"rc9": ["rc9@2.1.2", "", { "dependencies": { "defu": "^6.1.4", "destr": "^2.0.3" } }, "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg=="],
"react": ["react@18.2.0", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ=="],
@@ -4166,8 +4108,6 @@
"serve-static": ["serve-static@1.16.3", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "~0.19.1" } }, "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA=="],
"set-blocking": ["set-blocking@2.0.0", "", {}, "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="],
"set-cookie-parser": ["set-cookie-parser@2.7.2", "", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="],
"set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="],
@@ -4200,10 +4140,6 @@
"signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
"simple-concat": ["simple-concat@1.0.1", "", {}, "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q=="],
"simple-get": ["simple-get@4.0.1", "", { "dependencies": { "decompress-response": "^6.0.0", "once": "^1.3.1", "simple-concat": "^1.0.0" } }, "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA=="],
"simple-swizzle": ["simple-swizzle@0.2.4", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw=="],
"simple-update-notifier": ["simple-update-notifier@2.0.0", "", { "dependencies": { "semver": "^7.5.3" } }, "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w=="],
@@ -4228,7 +4164,7 @@
"socks": ["socks@2.8.7", "", { "dependencies": { "ip-address": "^10.0.1", "smart-buffer": "^4.2.0" } }, "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A=="],
"socks-proxy-agent": ["socks-proxy-agent@6.2.1", "", { "dependencies": { "agent-base": "^6.0.2", "debug": "^4.3.3", "socks": "^2.6.2" } }, "sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ=="],
"socks-proxy-agent": ["socks-proxy-agent@8.0.5", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", "socks": "^2.8.3" } }, "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw=="],
"solid-js": ["solid-js@1.9.10", "", { "dependencies": { "csstype": "^3.1.0", "seroval": "~1.3.0", "seroval-plugins": "~1.3.0" } }, "sha512-Coz956cos/EPDlhs6+jsdTxKuJDPT7B5SVIWgABwROyxjY7Xbr8wkzD68Et+NxnV7DLJ3nJdAC2r9InuV/4Jew=="],
@@ -4258,13 +4194,11 @@
"sprintf-js": ["sprintf-js@1.1.3", "", {}, "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="],
"sqlite3": ["sqlite3@5.1.7", "", { "dependencies": { "bindings": "^1.5.0", "node-addon-api": "^7.0.0", "prebuild-install": "^7.1.1", "tar": "^6.1.11" }, "optionalDependencies": { "node-gyp": "8.x" } }, "sha512-GGIyOiFaG+TUra3JIfkI/zGP8yZYLPQ0pl1bH+ODjiX57sPhrLU5sQJn1y9bDKZUFYkX1crlrPfSYt0BKKdkog=="],
"sqlstring": ["sqlstring@2.3.3", "", {}, "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg=="],
"srvx": ["srvx@0.9.8", "", { "bin": { "srvx": "bin/srvx.mjs" } }, "sha512-RZaxTKJEE/14HYn8COLuUOJAt0U55N9l1Xf6jj+T0GoA01EUH1Xz5JtSUOI+EHn+AEgPCVn7gk6jHJffrr06fQ=="],
"ssri": ["ssri@8.0.1", "", { "dependencies": { "minipass": "^3.1.1" } }, "sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ=="],
"ssri": ["ssri@12.0.0", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-S7iGNosepx9RadX82oimUkvr0Ct7IjJbEbs4mJcTxst8um95J3sDYU1RBEOvdu6oL1Wek2ODI5i4MAw+dZ6cAQ=="],
"sst": ["sst@3.18.10", "", { "dependencies": { "aws-sdk": "2.1692.0", "aws4fetch": "1.0.18", "jose": "5.2.3", "opencontrol": "0.0.6", "openid-client": "5.6.4" }, "optionalDependencies": { "sst-darwin-arm64": "3.18.10", "sst-darwin-x64": "3.18.10", "sst-linux-arm64": "3.18.10", "sst-linux-x64": "3.18.10", "sst-linux-x86": "3.18.10", "sst-win32-arm64": "3.18.10", "sst-win32-x64": "3.18.10", "sst-win32-x86": "3.18.10" }, "bin": { "sst": "bin/sst.mjs" } }, "sha512-SY+ldeJ9K5E9q+DhjXA3e2W3BEOzBwkE3IyLSD71uA3/5nRhUAST31iOWEpW36LbIvSQ9uOVDFcebztoLJ8s7w=="],
@@ -4332,8 +4266,6 @@
"strip-indent": ["strip-indent@3.0.0", "", { "dependencies": { "min-indent": "^1.0.0" } }, "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ=="],
"strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="],
"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=="],
@@ -4366,8 +4298,6 @@
"tar": ["tar@7.5.9", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-BTLcK0xsDh2+PUe9F6c2TlRp4zOOBMTkoQHQIWSIzI0R7KG46uEwq4OPk2W7bZcprBMsuaeFsqwYr7pjh6CuHg=="],
"tar-fs": ["tar-fs@2.1.4", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.1.4" } }, "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ=="],
"tar-stream": ["tar-stream@3.1.7", "", { "dependencies": { "b4a": "^1.6.4", "fast-fifo": "^1.2.0", "streamx": "^2.15.0" } }, "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ=="],
"tarn": ["tarn@3.0.2", "", {}, "sha512-51LAVKUSZSVfI05vjPESNc5vwqqZpbXCsU+/+wxlOrUjk2SnFTt97v9ZgQrD4YmxYW1Px6w2KjaDitCfkvgxMQ=="],
@@ -4460,8 +4390,6 @@
"tunnel": ["tunnel@0.0.6", "", {}, "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg=="],
"tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="],
"turbo": ["turbo@2.8.13", "", { "optionalDependencies": { "turbo-darwin-64": "2.8.13", "turbo-darwin-arm64": "2.8.13", "turbo-linux-64": "2.8.13", "turbo-linux-arm64": "2.8.13", "turbo-windows-64": "2.8.13", "turbo-windows-arm64": "2.8.13" }, "bin": { "turbo": "bin/turbo" } }, "sha512-nyM99hwFB9/DHaFyKEqatdayGjsMNYsQ/XBNO6MITc7roncZetKb97MpHxWf3uiU+LB9c9HUlU3Jp2Ixei2k1A=="],
"turbo-darwin-64": ["turbo-darwin-64@2.8.13", "", { "os": "darwin", "cpu": "x64" }, "sha512-PmOvodQNiOj77+Zwoqku70vwVjKzL34RTNxxoARjp5RU5FOj/CGiC6vcDQhNtFPUOWSAaogHF5qIka9TBhX4XA=="],
@@ -4524,9 +4452,9 @@
"unifont": ["unifont@0.5.2", "", { "dependencies": { "css-tree": "^3.0.0", "ofetch": "^1.4.1", "ohash": "^2.0.0" } }, "sha512-LzR4WUqzH9ILFvjLAUU7dK3Lnou/qd5kD+IakBtBK4S15/+x2y9VX+DcWQv6s551R6W+vzwgVS6tFg3XggGBgg=="],
"unique-filename": ["unique-filename@1.1.1", "", { "dependencies": { "unique-slug": "^2.0.0" } }, "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ=="],
"unique-filename": ["unique-filename@4.0.0", "", { "dependencies": { "unique-slug": "^5.0.0" } }, "sha512-XSnEewXmQ+veP7xX2dS5Q4yZAvO40cBN2MWkJ7D/6sW4Dg6wYBNwM1Vrnz1FhH5AdeLIlUXRI9e28z1YZi71NQ=="],
"unique-slug": ["unique-slug@2.0.2", "", { "dependencies": { "imurmurhash": "^0.1.4" } }, "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w=="],
"unique-slug": ["unique-slug@5.0.0", "", { "dependencies": { "imurmurhash": "^0.1.4" } }, "sha512-9OdaqO5kwqR+1kVgHAhsp5vPNU0hnxRa26rBFNfNgM7M6pNtgzeBn3s/xbyCQL3dcjzOatcef6UUHpB/6MaETg=="],
"unist-util-find-after": ["unist-util-find-after@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ=="],
@@ -4676,8 +4604,6 @@
"why-is-node-running": ["why-is-node-running@3.2.2", "", { "bin": { "why-is-node-running": "cli.js" } }, "sha512-NKUzAelcoCXhXL4dJzKIwXeR8iEVqsA0Lq6Vnd0UXvgaKbzVo4ZTHROF2Jidrv+SgxOQ03fMinnNhzZATxOD3A=="],
"wide-align": ["wide-align@1.1.5", "", { "dependencies": { "string-width": "^1.0.2 || 2 || 3 || 4" } }, "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg=="],
"widest-line": ["widest-line@5.0.0", "", { "dependencies": { "string-width": "^7.0.0" } }, "sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA=="],
"workerd": ["workerd@1.20251118.0", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20251118.0", "@cloudflare/workerd-darwin-arm64": "1.20251118.0", "@cloudflare/workerd-linux-64": "1.20251118.0", "@cloudflare/workerd-linux-arm64": "1.20251118.0", "@cloudflare/workerd-windows-64": "1.20251118.0" }, "bin": { "workerd": "bin/workerd" } }, "sha512-Om5ns0Lyx/LKtYI04IV0bjIrkBgoFNg0p6urzr2asekJlfP18RqFzyqMFZKf0i9Gnjtz/JfAS/Ol6tjCe5JJsQ=="],
@@ -5014,10 +4940,6 @@
"@electron/rebuild/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
"@electron/rebuild/node-abi": ["node-abi@4.26.0", "", { "dependencies": { "semver": "^7.6.3" } }, "sha512-8QwIZqikRvDIkXS2S93LjzhsSPJuIbfaMETWH+Bx8oOT9Sa9UsUtBFQlc3gBNd1+QINjaTloitXr1W3dQLi9Iw=="],
"@electron/rebuild/node-gyp": ["node-gyp@11.5.0", "", { "dependencies": { "env-paths": "^2.2.0", "exponential-backoff": "^3.1.1", "graceful-fs": "^4.2.6", "make-fetch-happen": "^14.0.3", "nopt": "^8.0.0", "proc-log": "^5.0.0", "semver": "^7.3.5", "tar": "^7.4.3", "tinyglobby": "^0.2.12", "which": "^5.0.0" }, "bin": { "node-gyp": "bin/node-gyp.js" } }, "sha512-ra7Kvlhxn5V9Slyus0ygMa2h+UqExPqUIkfk7Pc8QTLT956JLSy51uWFwHtIYy0vI8cB4BDhc/S03+880My/LQ=="],
"@electron/rebuild/yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
"@electron/universal/fs-extra": ["fs-extra@11.3.3", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg=="],
@@ -5094,12 +5016,6 @@
"@npmcli/agent/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
"@npmcli/agent/socks-proxy-agent": ["socks-proxy-agent@8.0.5", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", "socks": "^2.8.3" } }, "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw=="],
"@npmcli/move-file/mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="],
"@npmcli/move-file/rimraf": ["rimraf@3.0.2", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "bin.js" } }, "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA=="],
"@octokit/auth-app/@octokit/request": ["@octokit/request@10.0.7", "", { "dependencies": { "@octokit/endpoint": "^11.0.2", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "fast-content-type-parse": "^3.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-v93h0i1yu4idj8qFPZwjehoJx4j3Ntn+JhXsdJrG9pYaX6j/XRz2RmasMUHtNgQD39nrv/VwTWSqK0RNXR8upA=="],
"@octokit/auth-app/@octokit/request-error": ["@octokit/request-error@7.1.0", "", { "dependencies": { "@octokit/types": "^16.0.0" } }, "sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw=="],
@@ -5296,8 +5212,6 @@
"archiver-utils/is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="],
"are-we-there-yet/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
"astro/@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.6.1", "", {}, "sha512-l5Pqf6uZu31aG+3Lv8nl/3s4DbUzdlxTWDof4pEpto6GUJNhhCbelVi9dEyurOVyqaelwmS9oSyOWOENSfgo9A=="],
"astro/diff": ["diff@5.2.2", "", {}, "sha512-vtcDfH3TOjP8UekytvnHH1o1P4FcUdt4eQ1Y+Abap1tk/OB2MWQvcwS2ClCd1zuIhc3JKOx6p3kod8Vfys3E+A=="],
@@ -5336,19 +5250,9 @@
"c12/dotenv": ["dotenv@17.3.1", "", {}, "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA=="],
"cacache/chownr": ["chownr@2.0.0", "", {}, "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ=="],
"cacache/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="],
"cacache/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=="],
"cacache/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="],
"cacache/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
"cacache/mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="],
"cacache/rimraf": ["rimraf@3.0.2", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "bin.js" } }, "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA=="],
"cacache/tar": ["tar@6.2.1", "", { "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", "minipass": "^5.0.0", "minizlib": "^2.1.1", "mkdirp": "^1.0.3", "yallist": "^4.0.0" } }, "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A=="],
"cacache/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
"cli-truncate/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
@@ -5430,14 +5334,6 @@
"fs-extra/jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg=="],
"fs-minipass/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
"gauge/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
"gauge/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
"gauge/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"gaxios/node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="],
"glob/minimatch": ["minimatch@10.2.1", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-MClCe8IL5nRRmawL6ib/eT4oLyeKMGCghibcDWK+J0hh0Q8kqSdia6BvbRMVk6mPa6WqUa5uR2oxt6C5jd533A=="],
@@ -5470,13 +5366,7 @@
"log-symbols/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
"make-fetch-happen/http-proxy-agent": ["http-proxy-agent@4.0.1", "", { "dependencies": { "@tootallnate/once": "1", "agent-base": "6", "debug": "4" } }, "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg=="],
"make-fetch-happen/https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="],
"make-fetch-happen/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="],
"make-fetch-happen/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
"make-fetch-happen/negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
"matcher/escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
@@ -5492,12 +5382,6 @@
"miniflare/zod": ["zod@3.22.3", "", {}, "sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug=="],
"minipass-collect/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
"minipass-fetch/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
"minipass-fetch/minizlib": ["minizlib@2.1.2", "", { "dependencies": { "minipass": "^3.0.0", "yallist": "^4.0.0" } }, "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg=="],
"minipass-flush/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
"minipass-pipeline/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
@@ -5510,15 +5394,9 @@
"nitro/h3": ["h3@2.0.1-rc.5", "", { "dependencies": { "rou3": "^0.7.9", "srvx": "^0.9.1" }, "peerDependencies": { "crossws": "^0.4.1" }, "optionalPeers": ["crossws"] }, "sha512-qkohAzCab0nLzXNm78tBjZDvtKMTmtygS8BJLT3VPczAQofdqlFXDPkXdLMJN4r05+xqneG8snZJ0HgkERCZTg=="],
"node-gyp/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=="],
"node-gyp/nopt": ["nopt@8.1.0", "", { "dependencies": { "abbrev": "^3.0.0" }, "bin": { "nopt": "bin/nopt.js" } }, "sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A=="],
"node-gyp/nopt": ["nopt@5.0.0", "", { "dependencies": { "abbrev": "1" }, "bin": { "nopt": "bin/nopt.js" } }, "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ=="],
"node-gyp/rimraf": ["rimraf@3.0.2", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "bin.js" } }, "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA=="],
"node-gyp/tar": ["tar@6.2.1", "", { "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", "minipass": "^5.0.0", "minizlib": "^2.1.1", "mkdirp": "^1.0.3", "yallist": "^4.0.0" } }, "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A=="],
"node-gyp/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
"node-gyp/which": ["which@5.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ=="],
"npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="],
@@ -5576,8 +5454,6 @@
"postject/commander": ["commander@9.5.0", "", {}, "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ=="],
"prebuild-install/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
"pretty-format/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="],
@@ -5618,12 +5494,6 @@
"sitemap/sax": ["sax@1.4.4", "", {}, "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw=="],
"socks-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="],
"sqlite3/tar": ["tar@6.2.1", "", { "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", "minipass": "^5.0.0", "minizlib": "^2.1.1", "mkdirp": "^1.0.3", "yallist": "^4.0.0" } }, "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A=="],
"ssri/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
"sst/aws4fetch": ["aws4fetch@1.0.18", "", {}, "sha512-3Cf+YaUl07p24MoQ46rFwulAmiyCwH2+1zw1ZyPAX5OtJ34Hh185DwB8y/qRLb6cYYYtSFJ9pthyLc0MD4e8sQ=="],
"sst/jose": ["jose@5.2.3", "", {}, "sha512-KUXdbctm1uHVL8BYhnyHkgp3zDX5KW8ZhAKVFEfUbU2P8Alpzjb+48hHvjOdQIyPshoblhzsuqOwEEAbtHVirA=="],
@@ -5644,10 +5514,6 @@
"tar/yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="],
"tar-fs/chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="],
"tar-fs/tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="],
"tedious/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
"temp/rimraf": ["rimraf@2.6.3", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "./bin.js" } }, "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA=="],
@@ -5690,8 +5556,6 @@
"which-builtin-type/isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="],
"wide-align/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
"wrangler/esbuild": ["esbuild@0.25.4", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.4", "@esbuild/android-arm": "0.25.4", "@esbuild/android-arm64": "0.25.4", "@esbuild/android-x64": "0.25.4", "@esbuild/darwin-arm64": "0.25.4", "@esbuild/darwin-x64": "0.25.4", "@esbuild/freebsd-arm64": "0.25.4", "@esbuild/freebsd-x64": "0.25.4", "@esbuild/linux-arm": "0.25.4", "@esbuild/linux-arm64": "0.25.4", "@esbuild/linux-ia32": "0.25.4", "@esbuild/linux-loong64": "0.25.4", "@esbuild/linux-mips64el": "0.25.4", "@esbuild/linux-ppc64": "0.25.4", "@esbuild/linux-riscv64": "0.25.4", "@esbuild/linux-s390x": "0.25.4", "@esbuild/linux-x64": "0.25.4", "@esbuild/netbsd-arm64": "0.25.4", "@esbuild/netbsd-x64": "0.25.4", "@esbuild/openbsd-arm64": "0.25.4", "@esbuild/openbsd-x64": "0.25.4", "@esbuild/sunos-x64": "0.25.4", "@esbuild/win32-arm64": "0.25.4", "@esbuild/win32-ia32": "0.25.4", "@esbuild/win32-x64": "0.25.4" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q=="],
"wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
@@ -5824,12 +5688,6 @@
"@electron/notarize/fs-extra/jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg=="],
"@electron/rebuild/node-gyp/make-fetch-happen": ["make-fetch-happen@14.0.3", "", { "dependencies": { "@npmcli/agent": "^3.0.0", "cacache": "^19.0.1", "http-cache-semantics": "^4.1.1", "minipass": "^7.0.2", "minipass-fetch": "^4.0.0", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "negotiator": "^1.0.0", "proc-log": "^5.0.0", "promise-retry": "^2.0.1", "ssri": "^12.0.0" } }, "sha512-QMjGbFTP0blj97EeidG5hk/QhKQ3T4ICckQGLgz38QF7Vgbk6e6FTARN8KhKxyBbWn8R0HU+bnw8aSoFPD4qtQ=="],
"@electron/rebuild/node-gyp/nopt": ["nopt@8.1.0", "", { "dependencies": { "abbrev": "^3.0.0" }, "bin": { "nopt": "bin/nopt.js" } }, "sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A=="],
"@electron/rebuild/node-gyp/which": ["which@5.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ=="],
"@electron/rebuild/yargs/cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
"@electron/rebuild/yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
@@ -5924,8 +5782,6 @@
"@modelcontextprotocol/sdk/express/type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="],
"@npmcli/move-file/rimraf/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=="],
"@octokit/auth-app/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.2", "", { "dependencies": { "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-4zCpzP1fWc7QlqunZ5bSEjxc6yLAlRTnDwKtgXfcI/FxxGoqedDG8V2+xJ60bV2kODqcGB+nATdtap/XYq2NZQ=="],
"@octokit/auth-app/@octokit/request/@octokit/types": ["@octokit/types@16.0.0", "", { "dependencies": { "@octokit/openapi-types": "^27.0.0" } }, "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg=="],
@@ -6100,11 +5956,11 @@
"c12/chokidar/readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="],
"cacache/glob/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="],
"cacache/glob/jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="],
"cacache/tar/minipass": ["minipass@5.0.0", "", {}, "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ=="],
"cacache/glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
"cacache/tar/minizlib": ["minizlib@2.1.2", "", { "dependencies": { "minipass": "^3.0.0", "yallist": "^4.0.0" } }, "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg=="],
"cacache/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="],
"cli-truncate/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
@@ -6138,10 +5994,6 @@
"form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
"gauge/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
"gauge/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"gray-matter/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="],
"js-beautify/glob/jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="],
@@ -6156,25 +6008,11 @@
"lazystream/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
"make-fetch-happen/http-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="],
"make-fetch-happen/https-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="],
"motion/framer-motion/motion-dom": ["motion-dom@12.34.5", "", { "dependencies": { "motion-utils": "^12.29.2" } }, "sha512-k33CsnxO2K3gBRMUZT+vPmc4Utlb5menKdG0RyVNLtlqRaaJPRWlE9fXl8NTtfZ5z3G8TDvqSu0MENLqSTaHZA=="],
"node-gyp/glob/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="],
"node-gyp/nopt/abbrev": ["abbrev@3.0.1", "", {}, "sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg=="],
"node-gyp/nopt/abbrev": ["abbrev@1.1.1", "", {}, "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q=="],
"node-gyp/tar/chownr": ["chownr@2.0.0", "", {}, "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ=="],
"node-gyp/tar/minipass": ["minipass@5.0.0", "", {}, "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ=="],
"node-gyp/tar/minizlib": ["minizlib@2.1.2", "", { "dependencies": { "minipass": "^3.0.0", "yallist": "^4.0.0" } }, "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg=="],
"node-gyp/tar/mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="],
"node-gyp/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
"node-gyp/which/isexe": ["isexe@3.1.5", "", {}, "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w=="],
"opencode/@ai-sdk/openai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.20", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-iXHVe0apM2zUEzauqJwqmpC37A5rihrStAih5Ks+JE32iTe4LZ58y17UGBjpQQTCRw9YxMeo2UFLxLpBluyvLQ=="],
@@ -6212,14 +6050,6 @@
"send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
"sqlite3/tar/chownr": ["chownr@2.0.0", "", {}, "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ=="],
"sqlite3/tar/minipass": ["minipass@5.0.0", "", {}, "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ=="],
"sqlite3/tar/minizlib": ["minizlib@2.1.2", "", { "dependencies": { "minipass": "^3.0.0", "yallist": "^4.0.0" } }, "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg=="],
"sqlite3/tar/mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="],
"storybook/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="],
"storybook/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.27.3", "", { "os": "android", "cpu": "arm" }, "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA=="],
@@ -6276,10 +6106,6 @@
"string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"tar-fs/tar-stream/bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="],
"tar-fs/tar-stream/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
"temp/rimraf/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=="],
"tsx/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="],
@@ -6350,10 +6176,6 @@
"vitest/@vitest/expect/chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="],
"wide-align/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
"wide-align/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"wrangler/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.4", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q=="],
"wrangler/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.4", "", { "os": "android", "cpu": "arm" }, "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ=="],
@@ -6468,18 +6290,6 @@
"@electron/asar/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
"@electron/rebuild/node-gyp/make-fetch-happen/cacache": ["cacache@19.0.1", "", { "dependencies": { "@npmcli/fs": "^4.0.0", "fs-minipass": "^3.0.0", "glob": "^10.2.2", "lru-cache": "^10.0.1", "minipass": "^7.0.3", "minipass-collect": "^2.0.1", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "p-map": "^7.0.2", "ssri": "^12.0.0", "tar": "^7.4.3", "unique-filename": "^4.0.0" } }, "sha512-hdsUxulXCi5STId78vRVYEtDAjq99ICAUktLTeTYsLoTE6Z8dS0c8pWNCxwdrk9YfJeobDZc2Y186hD/5ZQgFQ=="],
"@electron/rebuild/node-gyp/make-fetch-happen/minipass-fetch": ["minipass-fetch@4.0.1", "", { "dependencies": { "minipass": "^7.0.3", "minipass-sized": "^1.0.3", "minizlib": "^3.0.1" }, "optionalDependencies": { "encoding": "^0.1.13" } }, "sha512-j7U11C5HXigVuutxebFadoYBbd7VSdZWggSe64NVdvWNBqGAiXPL2QVCehjmw7lY1oF9gOllYbORh+hiNgfPgQ=="],
"@electron/rebuild/node-gyp/make-fetch-happen/negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
"@electron/rebuild/node-gyp/make-fetch-happen/ssri": ["ssri@12.0.0", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-S7iGNosepx9RadX82oimUkvr0Ct7IjJbEbs4mJcTxst8um95J3sDYU1RBEOvdu6oL1Wek2ODI5i4MAw+dZ6cAQ=="],
"@electron/rebuild/node-gyp/nopt/abbrev": ["abbrev@3.0.1", "", {}, "sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg=="],
"@electron/rebuild/node-gyp/which/isexe": ["isexe@3.1.5", "", {}, "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w=="],
"@electron/rebuild/yargs/cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"@electron/rebuild/yargs/cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
@@ -6542,8 +6352,6 @@
"@modelcontextprotocol/sdk/express/type-is/media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="],
"@npmcli/move-file/rimraf/glob/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="],
"@octokit/auth-app/@octokit/request-error/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="],
"@octokit/auth-app/@octokit/request/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="],
@@ -6592,9 +6400,9 @@
"babel-plugin-module-resolver/glob/path-scurry/minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="],
"cacache/glob/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
"cacache/glob/jackspeak/@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="],
"cacache/tar/minizlib/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
"cacache/glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
"cli-truncate/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
@@ -6622,10 +6430,6 @@
"js-beautify/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
"node-gyp/glob/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
"node-gyp/tar/minizlib/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
"opencontrol/@modelcontextprotocol/sdk/express/accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
"opencontrol/@modelcontextprotocol/sdk/express/body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="],
@@ -6662,18 +6466,12 @@
"rimraf/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
"sqlite3/tar/minizlib/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
"tar-fs/tar-stream/bl/buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="],
"temp/rimraf/glob/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="],
"tw-to-css/tailwindcss/chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
"tw-to-css/tailwindcss/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
"wide-align/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"@astrojs/check/yargs/cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"@astrojs/check/yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
@@ -6706,28 +6504,12 @@
"@aws-sdk/token-providers/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.1.2", "", {}, "sha512-l63NF9y/cLROq/yqKXSLtcMeeyOfnSQlfMSlzFt/K73oIaD8DGaQWd7Z34X9GPiKqP5rbSh84Hl4bOlLcjiSrQ=="],
"@electron/rebuild/node-gyp/make-fetch-happen/cacache/@npmcli/fs": ["@npmcli/fs@4.0.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-/xGlezI6xfGO9NwuJlnwz/K14qD1kCSAGtacBHnGzeAIuJGazcp45KP5NuyARXoKb7cwulAGWVsbeSxdG/cb0Q=="],
"@electron/rebuild/node-gyp/make-fetch-happen/cacache/fs-minipass": ["fs-minipass@3.0.3", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw=="],
"@electron/rebuild/node-gyp/make-fetch-happen/cacache/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="],
"@electron/rebuild/node-gyp/make-fetch-happen/cacache/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
"@electron/rebuild/node-gyp/make-fetch-happen/cacache/minipass-collect": ["minipass-collect@2.0.1", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw=="],
"@electron/rebuild/node-gyp/make-fetch-happen/cacache/p-map": ["p-map@7.0.4", "", {}, "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ=="],
"@electron/rebuild/node-gyp/make-fetch-happen/cacache/unique-filename": ["unique-filename@4.0.0", "", { "dependencies": { "unique-slug": "^5.0.0" } }, "sha512-XSnEewXmQ+veP7xX2dS5Q4yZAvO40cBN2MWkJ7D/6sW4Dg6wYBNwM1Vrnz1FhH5AdeLIlUXRI9e28z1YZi71NQ=="],
"@electron/rebuild/yargs/cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"@electron/rebuild/yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"@jsx-email/cli/tailwindcss/chokidar/readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
"@npmcli/move-file/rimraf/glob/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
"@solidjs/start/shiki/@shikijs/engine-javascript/oniguruma-to-es/regex": ["regex@5.1.1", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-dN5I359AVGPnwzJm2jN1k0W9LPZ+ePvoOeVMMfqIMFz53sSwXkxaJoxr50ptnsC771lK95BnTrVSZxq0b9yCGw=="],
"@solidjs/start/shiki/@shikijs/engine-javascript/oniguruma-to-es/regex-recursion": ["regex-recursion@5.1.1", "", { "dependencies": { "regex": "^5.1.1", "regex-utilities": "^2.3.0" } }, "sha512-ae7SBCbzVNrIjgSbh7wMznPcQel1DNlDtzensnFxpiNpXt1U2ju/bHugH422r+4LAVS1FpW1YCwilmnNsjum9w=="],
@@ -6740,6 +6522,10 @@
"babel-plugin-module-resolver/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
"cacache/glob/jackspeak/@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
"cacache/glob/jackspeak/@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="],
"cacache/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
"electron-builder/yargs/cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
@@ -6752,8 +6538,6 @@
"js-beautify/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
"node-gyp/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
"opencontrol/@modelcontextprotocol/sdk/express/accepts/negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
"opencontrol/@modelcontextprotocol/sdk/express/type-is/media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="],
@@ -6766,28 +6550,20 @@
"rimraf/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
"tar-fs/tar-stream/bl/buffer/ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
"temp/rimraf/glob/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
"tw-to-css/tailwindcss/chokidar/readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
"@aws-sdk/credential-provider-cognito-identity/@aws-sdk/client-cognito-identity/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.1.2", "", {}, "sha512-l63NF9y/cLROq/yqKXSLtcMeeyOfnSQlfMSlzFt/K73oIaD8DGaQWd7Z34X9GPiKqP5rbSh84Hl4bOlLcjiSrQ=="],
"@electron/rebuild/node-gyp/make-fetch-happen/cacache/glob/jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="],
"@electron/rebuild/node-gyp/make-fetch-happen/cacache/glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
"@electron/rebuild/node-gyp/make-fetch-happen/cacache/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="],
"@electron/rebuild/node-gyp/make-fetch-happen/cacache/unique-filename/unique-slug": ["unique-slug@5.0.0", "", { "dependencies": { "imurmurhash": "^0.1.4" } }, "sha512-9OdaqO5kwqR+1kVgHAhsp5vPNU0hnxRa26rBFNfNgM7M6pNtgzeBn3s/xbyCQL3dcjzOatcef6UUHpB/6MaETg=="],
"@npmcli/move-file/rimraf/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
"archiver-utils/glob/jackspeak/@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
"archiver-utils/glob/jackspeak/@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
"cacache/glob/jackspeak/@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
"cacache/glob/jackspeak/@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
"js-beautify/glob/jackspeak/@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
"js-beautify/glob/jackspeak/@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
@@ -6797,19 +6573,5 @@
"rimraf/glob/jackspeak/@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
"temp/rimraf/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
"@electron/rebuild/node-gyp/make-fetch-happen/cacache/glob/jackspeak/@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="],
"@electron/rebuild/node-gyp/make-fetch-happen/cacache/glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
"@electron/rebuild/node-gyp/make-fetch-happen/cacache/glob/jackspeak/@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
"@electron/rebuild/node-gyp/make-fetch-happen/cacache/glob/jackspeak/@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="],
"@electron/rebuild/node-gyp/make-fetch-happen/cacache/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
"@electron/rebuild/node-gyp/make-fetch-happen/cacache/glob/jackspeak/@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
"@electron/rebuild/node-gyp/make-fetch-happen/cacache/glob/jackspeak/@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
}
}

View File

@@ -1,8 +1,8 @@
{
"nodeModules": {
"x86_64-linux": "sha256-duBedS4ZTc1as03OM0KB9mKKU21Cywv4o9GHwQZv6Ts=",
"aarch64-linux": "sha256-juvQfuNBqqzeB/TIY9PuUDqgpsdyI54ImowjQLrNhns=",
"aarch64-darwin": "sha256-kKgcuEN1oJqHJc+sGjcZ4INWvbZczSTDJ8VHIWAquD4=",
"x86_64-darwin": "sha256-hXkFWOL4wi9s8HSrChpqtH4PKSNzbzVgU+0GbAxEUT4="
"x86_64-linux": "sha256-4kjoJ06VNvHltPHfzQRBG0bC6R39jao10ffGzrNZ230=",
"aarch64-linux": "sha256-6Uio+S2rcyBWbBEeOZb9N1CCKgkbKi68lOIKi3Ws/pQ=",
"aarch64-darwin": "sha256-8ngN5KVN4vhdsk0QJ11BGgSVBrcaEbwSj23c77HBpgs=",
"x86_64-darwin": "sha256-v/ueYGb9a0Nymzy+mkO4uQr78DAuJnES1qOT0onFgnQ="
}
}

View File

@@ -7,6 +7,7 @@ import { createSdk, modKey, resolveDirectory, serverUrl } from "./utils"
import {
dropdownMenuTriggerSelector,
dropdownMenuContentSelector,
sessionTimelineHeaderSelector,
projectMenuTriggerSelector,
projectCloseMenuSelector,
projectWorkspacesToggleSelector,
@@ -243,7 +244,9 @@ export async function openSessionMoreMenu(page: Page, sessionID: string) {
const scroller = page.locator(".scroll-view__viewport").first()
await expect(scroller).toBeVisible()
await expect(scroller.getByRole("heading", { level: 1 }).first()).toBeVisible({ timeout: 30_000 })
const header = page.locator(sessionTimelineHeaderSelector).first()
await expect(header).toBeVisible({ timeout: 30_000 })
await expect(header.getByRole("heading", { level: 1 }).first()).toBeVisible({ timeout: 30_000 })
const menu = page
.locator(dropdownMenuContentSelector)
@@ -259,7 +262,7 @@ export async function openSessionMoreMenu(page: Page, sessionID: string) {
if (opened) return menu
const menuTrigger = scroller.getByRole("button", { name: /more options/i }).first()
const menuTrigger = header.getByRole("button", { name: /more options/i }).first()
await expect(menuTrigger).toBeVisible()
await menuTrigger.click()

View File

@@ -25,26 +25,16 @@ test("closing active project navigates to another open project", async ({ page,
await clickMenuItem(menu, /^Close$/i, { force: true })
await expect
.poll(
() => {
const pathname = new URL(page.url()).pathname
if (new RegExp(`^/${slug}/session(?:/[^/]+)?/?$`).test(pathname)) return "project"
if (pathname === "/") return "home"
return ""
},
{ timeout: 15_000 },
)
.poll(() => {
const pathname = new URL(page.url()).pathname
if (new RegExp(`^/${slug}/session(?:/[^/]+)?/?$`).test(pathname)) return "project"
if (pathname === "/") return "home"
return ""
})
.toMatch(/^(project|home)$/)
await expect(page).not.toHaveURL(new RegExp(`/${otherSlug}/session(?:[/?#]|$)`))
await expect
.poll(
async () => {
return await page.locator(projectSwitchSelector(otherSlug)).count()
},
{ timeout: 15_000 },
)
.toBe(0)
await expect(otherButton).toHaveCount(0)
},
{ extra: [other] },
)

View File

@@ -51,6 +51,8 @@ export const dropdownMenuContentSelector = '[data-component="dropdown-menu-conte
export const inlineInputSelector = '[data-component="inline-input"]'
export const sessionTimelineHeaderSelector = "[data-session-title]"
export const sessionItemSelector = (sessionID: string) => `${sidebarNavSelector} [data-session-id="${sessionID}"]`
export const workspaceItemSelector = (slug: string) =>

View File

@@ -7,7 +7,7 @@ import {
openSharePopover,
withSession,
} from "../actions"
import { sessionItemSelector, inlineInputSelector } from "../selectors"
import { sessionItemSelector, inlineInputSelector, sessionTimelineHeaderSelector } from "../selectors"
const shareDisabled = process.env.OPENCODE_DISABLE_SHARE === "true" || process.env.OPENCODE_DISABLE_SHARE === "1"
@@ -39,12 +39,14 @@ test("session can be renamed via header menu", async ({ page, sdk, gotoSession }
await withSession(sdk, originalTitle, async (session) => {
await seedMessage(sdk, session.id)
await gotoSession(session.id)
await expect(page.getByRole("heading", { level: 1 }).first()).toHaveText(originalTitle)
await expect(page.locator(sessionTimelineHeaderSelector).getByRole("heading", { level: 1 }).first()).toHaveText(
originalTitle,
)
const menu = await openSessionMoreMenu(page, session.id)
await clickMenuItem(menu, /rename/i)
const input = page.locator(".scroll-view__viewport").locator(inlineInputSelector).first()
const input = page.locator(sessionTimelineHeaderSelector).locator(inlineInputSelector).first()
await expect(input).toBeVisible()
await expect(input).toBeFocused()
await input.fill(renamedTitle)
@@ -61,7 +63,9 @@ test("session can be renamed via header menu", async ({ page, sdk, gotoSession }
)
.toBe(renamedTitle)
await expect(page.getByRole("heading", { level: 1 }).first()).toHaveText(renamedTitle)
await expect(page.locator(sessionTimelineHeaderSelector).getByRole("heading", { level: 1 }).first()).toHaveText(
renamedTitle,
)
})
})

View File

@@ -83,23 +83,16 @@ test("changing theme persists in localStorage", async ({ page, gotoSession }) =>
const select = dialog.locator(settingsThemeSelector)
await expect(select).toBeVisible()
const currentThemeId = await page.evaluate(() => {
return document.documentElement.getAttribute("data-theme")
})
const currentTheme = (await select.locator('[data-slot="select-select-trigger-value"]').textContent())?.trim() ?? ""
await select.locator('[data-slot="select-select-trigger"]').click()
const items = page.locator('[data-slot="select-select-item"]')
const count = await items.count()
expect(count).toBeGreaterThan(1)
const nextTheme = (await items.locator('[data-slot="select-select-item-label"]').allTextContents())
.map((x) => x.trim())
.find((x) => x && x !== currentTheme)
expect(nextTheme).toBeTruthy()
const firstTheme = await items.nth(1).locator('[data-slot="select-select-item-label"]').textContent()
expect(firstTheme).toBeTruthy()
await items.filter({ hasText: nextTheme! }).first().click()
await items.nth(1).click()
await page.keyboard.press("Escape")
@@ -108,7 +101,7 @@ test("changing theme persists in localStorage", async ({ page, gotoSession }) =>
})
expect(storedThemeId).not.toBeNull()
expect(storedThemeId).not.toBe(currentThemeId)
expect(storedThemeId).not.toBe("oc-1")
const dataTheme = await page.evaluate(() => {
return document.documentElement.getAttribute("data-theme")
@@ -116,42 +109,6 @@ test("changing theme persists in localStorage", async ({ page, gotoSession }) =>
expect(dataTheme).toBe(storedThemeId)
})
test("legacy oc-1 theme migrates to oc-2", async ({ page, gotoSession }) => {
await page.addInitScript(() => {
localStorage.setItem("opencode-theme-id", "oc-1")
localStorage.setItem("opencode-theme-css-light", "--background-base:#fff;")
localStorage.setItem("opencode-theme-css-dark", "--background-base:#000;")
})
await gotoSession()
await expect(page.locator("html")).toHaveAttribute("data-theme", "oc-2")
await expect
.poll(async () => {
return await page.evaluate(() => {
return localStorage.getItem("opencode-theme-id")
})
})
.toBe("oc-2")
await expect
.poll(async () => {
return await page.evaluate(() => {
return localStorage.getItem("opencode-theme-css-light")
})
})
.toBeNull()
await expect
.poll(async () => {
return await page.evaluate(() => {
return localStorage.getItem("opencode-theme-css-dark")
})
})
.toBeNull()
})
test("changing font persists in localStorage and updates CSS variable", async ({ page, gotoSession }) => {
await gotoSession()

View File

@@ -44,14 +44,12 @@ async function store(page: Page, key: string) {
}, key)
}
test("inactive terminal tab buffers persist across tab switches", async ({ page, withProject }) => {
test("terminal tab buffers persist across tab switches", async ({ page, withProject }) => {
await withProject(async ({ directory, gotoSession }) => {
const key = workspacePersistKey(directory, "terminal")
const one = `E2E_TERM_ONE_${Date.now()}`
const two = `E2E_TERM_TWO_${Date.now()}`
const tabs = page.locator('#terminal-panel [data-slot="tabs-trigger"]')
const first = tabs.filter({ hasText: /Terminal 1/ }).first()
const second = tabs.filter({ hasText: /Terminal 2/ }).first()
await gotoSession()
await open(page)
@@ -63,39 +61,22 @@ test("inactive terminal tab buffers persist across tab switches", async ({ page,
await run(page, `echo ${two}`)
await first.click()
await expect(first).toHaveAttribute("aria-selected", "true")
await expect
.poll(
async () => {
const state = await store(page, key)
const first = state?.all.find((item) => item.titleNumber === 1)?.buffer ?? ""
const second = state?.all.find((item) => item.titleNumber === 2)?.buffer ?? ""
return {
first: first.includes(one),
second: second.includes(two),
}
},
{ timeout: 30_000 },
)
.toEqual({ first: false, second: true })
await tabs
.filter({ hasText: /Terminal 1/ })
.first()
.click()
await second.click()
await expect(second).toHaveAttribute("aria-selected", "true")
await expect
.poll(
async () => {
const state = await store(page, key)
const first = state?.all.find((item) => item.titleNumber === 1)?.buffer ?? ""
const second = state?.all.find((item) => item.titleNumber === 2)?.buffer ?? ""
return {
first: first.includes(one),
second: second.includes(two),
}
return first.includes(one) && second.includes(two)
},
{ timeout: 30_000 },
)
.toEqual({ first: true, second: false })
.toBe(true)
})
})

View File

@@ -57,7 +57,7 @@ export function sessionPath(directory: string, sessionID?: string) {
}
export function workspacePersistKey(directory: string, key: string) {
const head = (directory.slice(0, 12) || "workspace").replace(/[^a-zA-Z0-9._-]/g, "-")
const head = directory.slice(0, 12) || "workspace"
const sum = checksum(directory) ?? "0"
return `opencode.workspace.${head}.${sum}.dat:workspace:${key}`
}

View File

@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/app",
"version": "1.2.24",
"version": "1.2.21",
"description": "",
"type": "module",
"exports": {

View File

@@ -1,13 +1,6 @@
;(function () {
var key = "opencode-theme-id"
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 themeId = localStorage.getItem("opencode-theme-id")
if (!themeId) return
var scheme = localStorage.getItem("opencode-color-scheme") || "system"
var isDark = scheme === "dark" || (scheme === "system" && matchMedia("(prefers-color-scheme: dark)").matches)
@@ -16,9 +9,9 @@
document.documentElement.dataset.theme = themeId
document.documentElement.dataset.colorScheme = mode
if (themeId === "oc-2") return
if (themeId === "oc-1") return
var css = localStorage.getItem("opencode-theme-css-" + mode)
var css = localStorage.getItem("opencode-theme-css-" + themeId + "-" + mode)
if (css) {
var style = document.createElement("style")
style.id = "oc-theme-preload"

View File

@@ -1,441 +0,0 @@
import { useIsRouting, useLocation } from "@solidjs/router"
import { batch, createEffect, onCleanup, onMount } from "solid-js"
import { createStore } from "solid-js/store"
import { Tooltip } from "@opencode-ai/ui/tooltip"
type Mem = Performance & {
memory?: {
usedJSHeapSize: number
jsHeapSizeLimit: number
}
}
type Evt = PerformanceEntry & {
interactionId?: number
processingStart?: number
}
type Shift = PerformanceEntry & {
hadRecentInput: boolean
value: number
}
type Obs = PerformanceObserverInit & {
durationThreshold?: number
}
const span = 5000
const ms = (n?: number, d = 0) => {
if (n === undefined || Number.isNaN(n)) return "n/a"
return `${n.toFixed(d)}ms`
}
const time = (n?: number) => {
if (n === undefined || Number.isNaN(n)) return "n/a"
return `${Math.round(n)}`
}
const mb = (n?: number) => {
if (n === undefined || Number.isNaN(n)) return "n/a"
const v = n / 1024 / 1024
return `${v >= 1024 ? v.toFixed(0) : v.toFixed(1)}MB`
}
const bad = (n: number | undefined, limit: number, low = false) => {
if (n === undefined || Number.isNaN(n)) return false
return low ? n < limit : n > limit
}
const session = (path: string) => path.includes("/session")
function Cell(props: { bad?: boolean; dim?: boolean; label: string; tip: string; value: string; wide?: boolean }) {
return (
<Tooltip value={props.tip} placement="top">
<div
classList={{
"flex min-h-[42px] w-full min-w-0 flex-col items-center justify-center rounded-[8px] bg-white/5 px-0.5 py-1 text-center": true,
"col-span-2": !!props.wide,
}}
>
<div class="text-[10px] leading-none font-black uppercase tracking-[0.04em] opacity-70">{props.label}</div>
<div
classList={{
"text-[13px] leading-none font-bold tabular-nums sm:text-[14px]": true,
"text-text-on-critical-base": !!props.bad,
"opacity-70": !!props.dim,
}}
>
{props.value}
</div>
</div>
</Tooltip>
)
}
export function DebugBar() {
const location = useLocation()
const routing = useIsRouting()
const [state, setState] = createStore({
cls: undefined as number | undefined,
delay: undefined as number | undefined,
fps: undefined as number | undefined,
gap: undefined as number | undefined,
heap: {
limit: undefined as number | undefined,
used: undefined as number | undefined,
},
inp: undefined as number | undefined,
jank: undefined as number | undefined,
long: {
block: undefined as number | undefined,
count: undefined as number | undefined,
max: undefined as number | undefined,
},
nav: {
dur: undefined as number | undefined,
pending: false,
},
})
const heap = () => (state.heap.limit ? (state.heap.used ?? 0) / state.heap.limit : undefined)
const heapv = () => {
const value = heap()
if (value === undefined) return "n/a"
return `${Math.round(value * 100)}%`
}
const longv = () => (state.long.count === undefined ? "n/a" : `${time(state.long.block)}/${state.long.count}`)
const navv = () => (state.nav.pending ? "..." : time(state.nav.dur))
let prev = ""
let start = 0
let init = false
let one = 0
let two = 0
createEffect(() => {
const busy = routing()
const next = `${location.pathname}${location.search}`
if (!init) {
init = true
prev = next
return
}
if (busy) {
if (one !== 0) cancelAnimationFrame(one)
if (two !== 0) cancelAnimationFrame(two)
one = 0
two = 0
if (start !== 0) return
start = performance.now()
if (session(prev)) setState("nav", { dur: undefined, pending: true })
return
}
if (start === 0) {
prev = next
return
}
const at = start
const from = prev
start = 0
prev = next
if (!(session(from) || session(next))) return
if (one !== 0) cancelAnimationFrame(one)
if (two !== 0) cancelAnimationFrame(two)
one = requestAnimationFrame(() => {
one = 0
two = requestAnimationFrame(() => {
two = 0
setState("nav", { dur: performance.now() - at, pending: false })
})
})
})
onMount(() => {
const obs: PerformanceObserver[] = []
const fps: Array<{ at: number; dur: number }> = []
const long: Array<{ at: number; dur: number }> = []
const seen = new Map<number | string, { at: number; delay: number; dur: number }>()
let hasLong = false
let poll: number | undefined
let raf = 0
let last = 0
let snap = 0
const trim = (list: Array<{ at: number; dur: number }>, span: number, at: number) => {
while (list[0] && at - list[0].at > span) list.shift()
}
const syncFrame = (at: number) => {
trim(fps, span, at)
const total = fps.reduce((sum, entry) => sum + entry.dur, 0)
const gap = fps.reduce((max, entry) => Math.max(max, entry.dur), 0)
const jank = fps.filter((entry) => entry.dur > 32).length
batch(() => {
setState("fps", total > 0 ? (fps.length * 1000) / total : undefined)
setState("gap", gap > 0 ? gap : undefined)
setState("jank", jank)
})
}
const syncLong = (at = performance.now()) => {
if (!hasLong) return
trim(long, span, at)
const block = long.reduce((sum, entry) => sum + Math.max(0, entry.dur - 50), 0)
const max = long.reduce((hi, entry) => Math.max(hi, entry.dur), 0)
setState("long", { block, count: long.length, max })
}
const syncInp = (at = performance.now()) => {
for (const [key, entry] of seen) {
if (at - entry.at > span) seen.delete(key)
}
let delay = 0
let inp = 0
for (const entry of seen.values()) {
delay = Math.max(delay, entry.delay)
inp = Math.max(inp, entry.dur)
}
batch(() => {
setState("delay", delay > 0 ? delay : undefined)
setState("inp", inp > 0 ? inp : undefined)
})
}
const syncHeap = () => {
const mem = (performance as Mem).memory
if (!mem) return
setState("heap", { limit: mem.jsHeapSizeLimit, used: mem.usedJSHeapSize })
}
const reset = () => {
fps.length = 0
long.length = 0
seen.clear()
last = 0
snap = 0
batch(() => {
setState("fps", undefined)
setState("gap", undefined)
setState("jank", undefined)
setState("delay", undefined)
setState("inp", undefined)
if (hasLong) setState("long", { block: 0, count: 0, max: 0 })
})
}
const watch = (type: string, init: Obs, fn: (entries: PerformanceEntry[]) => void) => {
if (typeof PerformanceObserver === "undefined") return false
if (!(PerformanceObserver.supportedEntryTypes ?? []).includes(type)) return false
const ob = new PerformanceObserver((list) => fn(list.getEntries()))
try {
ob.observe(init)
obs.push(ob)
return true
} catch {
ob.disconnect()
return false
}
}
if (
watch("layout-shift", { buffered: true, type: "layout-shift" }, (entries) => {
const add = entries.reduce((sum, entry) => {
const item = entry as Shift
if (item.hadRecentInput) return sum
return sum + item.value
}, 0)
if (add === 0) return
setState("cls", (value) => (value ?? 0) + add)
})
) {
setState("cls", 0)
}
if (
watch("longtask", { buffered: true, type: "longtask" }, (entries) => {
const at = performance.now()
long.push(...entries.map((entry) => ({ at: entry.startTime, dur: entry.duration })))
syncLong(at)
})
) {
hasLong = true
setState("long", { block: 0, count: 0, max: 0 })
}
watch("event", { buffered: true, durationThreshold: 16, type: "event" }, (entries) => {
for (const raw of entries) {
const entry = raw as Evt
if (entry.duration < 16) continue
const key =
entry.interactionId && entry.interactionId > 0
? entry.interactionId
: `${entry.name}:${Math.round(entry.startTime)}`
const prev = seen.get(key)
const delay = Math.max(0, (entry.processingStart ?? entry.startTime) - entry.startTime)
seen.set(key, {
at: entry.startTime,
delay: Math.max(prev?.delay ?? 0, delay),
dur: Math.max(prev?.dur ?? 0, entry.duration),
})
if (seen.size <= 200) continue
const first = seen.keys().next().value
if (first !== undefined) seen.delete(first)
}
syncInp()
})
const loop = (at: number) => {
if (document.visibilityState !== "visible") {
raf = 0
return
}
if (last === 0) {
last = at
raf = requestAnimationFrame(loop)
return
}
fps.push({ at, dur: at - last })
last = at
if (at - snap >= 250) {
snap = at
syncFrame(at)
}
raf = requestAnimationFrame(loop)
}
const stop = () => {
if (raf !== 0) cancelAnimationFrame(raf)
raf = 0
if (poll === undefined) return
clearInterval(poll)
poll = undefined
}
const start = () => {
if (document.visibilityState !== "visible") return
if (poll === undefined) {
poll = window.setInterval(() => {
syncLong()
syncInp()
syncHeap()
}, 1000)
}
if (raf !== 0) return
raf = requestAnimationFrame(loop)
}
const vis = () => {
if (document.visibilityState !== "visible") {
stop()
return
}
reset()
start()
}
syncHeap()
start()
document.addEventListener("visibilitychange", vis)
onCleanup(() => {
if (one !== 0) cancelAnimationFrame(one)
if (two !== 0) cancelAnimationFrame(two)
stop()
document.removeEventListener("visibilitychange", vis)
for (const ob of obs) ob.disconnect()
})
})
return (
<aside
aria-label="Development performance diagnostics"
class="pointer-events-auto fixed bottom-3 right-3 z-50 w-[308px] max-w-[calc(100vw-1.5rem)] overflow-hidden rounded-xl border p-0.5 text-text-on-interactive-base shadow-[var(--shadow-lg-border-base)] sm:bottom-4 sm:right-4 sm:w-[324px]"
style={{
"background-color": "color-mix(in srgb, var(--icon-interactive-base) 42%, black)",
"border-color": "color-mix(in srgb, white 14%, transparent)",
}}
>
<div class="grid grid-cols-5 gap-px font-mono">
<Cell
label="NAV"
tip="Last completed route transition touching a session page, measured from router start until the first paint after it settles."
value={navv()}
bad={bad(state.nav.dur, 400)}
dim={state.nav.dur === undefined && !state.nav.pending}
/>
<Cell
label="FPS"
tip="Rolling frames per second over the last 5 seconds."
value={state.fps === undefined ? "n/a" : `${Math.round(state.fps)}`}
bad={bad(state.fps, 50, true)}
dim={state.fps === undefined}
/>
<Cell
label="FRM"
tip="Worst frame time over the last 5 seconds."
value={time(state.gap)}
bad={bad(state.gap, 50)}
dim={state.gap === undefined}
/>
<Cell
label="JNK"
tip="Frames over 32ms in the last 5 seconds."
value={state.jank === undefined ? "n/a" : `${state.jank}`}
bad={bad(state.jank, 8)}
dim={state.jank === undefined}
/>
<Cell
label="LNG"
tip={`Blocked time and long-task count in the last 5 seconds. Max task: ${ms(state.long.max)}.`}
value={longv()}
bad={bad(state.long.block, 200)}
dim={state.long.count === undefined}
/>
<Cell
label="DLY"
tip="Worst observed input delay in the last 5 seconds."
value={time(state.delay)}
bad={bad(state.delay, 100)}
dim={state.delay === undefined}
/>
<Cell
label="INP"
tip="Approximate interaction duration over the last 5 seconds. This is INP-like, not the official Web Vitals INP."
value={time(state.inp)}
bad={bad(state.inp, 200)}
dim={state.inp === undefined}
/>
<Cell
label="CLS"
tip="Cumulative layout shift for the current app lifetime."
value={state.cls === undefined ? "n/a" : state.cls.toFixed(2)}
bad={bad(state.cls, 0.1)}
dim={state.cls === undefined}
/>
<Cell
label="MEM"
tip={
state.heap.used === undefined
? "Used JS heap vs heap limit. Chromium only."
: `Used JS heap vs heap limit. ${mb(state.heap.used)} of ${mb(state.heap.limit)}.`
}
value={heapv()}
bad={bad(heap(), 0.8)}
dim={state.heap.used === undefined}
wide
/>
</div>
</aside>
)
}

View File

@@ -957,6 +957,14 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
return permission.isAutoAccepting(id, sdk.directory)
})
const flip = () => {
if (!params.id) {
permission.toggleAutoAcceptDirectory(sdk.directory)
return
}
permission.toggleAutoAccept(params.id, sdk.directory)
}
const { abort, handleSubmit } = createPromptSubmit({
info,
imageAttachments,
@@ -1214,7 +1222,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
onKeyDown={handleKeyDown}
classList={{
"select-text": true,
"w-full pl-3 pr-2 pt-2 pb-11 text-14-regular text-text-strong focus:outline-none whitespace-pre-wrap": true,
"w-full pl-3 pr-2 pt-2 pb-[76px] text-14-regular text-text-strong focus:outline-none whitespace-pre-wrap": true,
"[&_[data-type=file]]:text-syntax-property": true,
"[&_[data-type=agent]]:text-syntax-type": true,
"font-mono!": store.mode === "shell",
@@ -1222,7 +1230,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
/>
<Show when={!prompt.dirty()}>
<div
class="absolute top-0 inset-x-0 pl-3 pr-2 pt-2 pb-11 text-14-regular text-text-weak pointer-events-none whitespace-nowrap truncate"
class="absolute top-0 inset-x-0 pl-3 pr-2 pt-2 pb-[76px] text-14-regular text-text-weak pointer-events-none whitespace-nowrap truncate"
classList={{ "font-mono!": store.mode === "shell" }}
>
{placeholder()}
@@ -1250,30 +1258,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
"pointer-events": buttonsSpring() > 0.5 ? "auto" : "none",
}}
>
<TooltipKeybind
placement="top"
title={language.t("prompt.action.attachFile")}
keybind={command.keybind("file.attach")}
>
<Button
data-action="prompt-attach"
type="button"
variant="ghost"
class="size-8 p-0"
style={{
opacity: buttonsSpring(),
transform: `scale(${0.95 + buttonsSpring() * 0.05})`,
filter: `blur(${(1 - buttonsSpring()) * 2}px)`,
}}
onClick={pick}
disabled={store.mode !== "normal"}
tabIndex={store.mode === "normal" ? undefined : -1}
aria-label={language.t("prompt.action.attachFile")}
>
<Icon name="plus" class="size-4.5" />
</Button>
</TooltipKeybind>
<Tooltip
placement="top"
inactive={!prompt.dirty() && !working()}
@@ -1314,44 +1298,129 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
</div>
<div class="pointer-events-none absolute bottom-2 left-2">
<div class="pointer-events-auto">
<div
class="pointer-events-auto flex items-center gap-1.5"
style={{
"pointer-events": buttonsSpring() > 0.5 ? "auto" : "none",
}}
>
<TooltipKeybind
placement="top"
gutter={8}
title={language.t(
accepting() ? "command.permissions.autoaccept.disable" : "command.permissions.autoaccept.enable",
)}
keybind={command.keybind("permissions.autoaccept")}
title={language.t("prompt.action.attachFile")}
keybind={command.keybind("file.attach")}
>
<Button
data-action="prompt-permissions"
data-action="prompt-attach"
type="button"
variant="ghost"
onClick={() => {
if (!params.id) {
permission.toggleAutoAcceptDirectory(sdk.directory)
return
}
permission.toggleAutoAccept(params.id, sdk.directory)
class="size-8 p-0"
style={{
opacity: buttonsSpring(),
transform: `scale(${0.95 + buttonsSpring() * 0.05})`,
filter: `blur(${(1 - buttonsSpring()) * 2}px)`,
}}
classList={{
"size-6 flex items-center justify-center": true,
"text-text-base": !accepting(),
"hover:bg-surface-success-base": accepting(),
}}
aria-label={
accepting()
? language.t("command.permissions.autoaccept.disable")
: language.t("command.permissions.autoaccept.enable")
}
aria-pressed={accepting()}
onClick={pick}
disabled={store.mode !== "normal"}
tabIndex={store.mode === "normal" ? undefined : -1}
aria-label={language.t("prompt.action.attachFile")}
>
<Icon
name="chevron-double-right"
size="small"
classList={{ "text-icon-success-base": accepting() }}
/>
<Icon name="plus" class="size-4.5" />
</Button>
</TooltipKeybind>
<Show
when={providers.paid().length > 0}
fallback={
<TooltipKeybind
placement="top"
gutter={8}
title={language.t("command.model.choose")}
keybind={command.keybind("model.choose")}
>
<Button
as="div"
variant="ghost"
size="small"
class="min-w-0 max-w-[240px] text-13-regular text-text-base group"
style={{
opacity: buttonsSpring(),
transform: `scale(${0.95 + buttonsSpring() * 0.05})`,
filter: `blur(${(1 - buttonsSpring()) * 2}px)`,
}}
onClick={() => dialog.show(() => <DialogSelectModelUnpaid />)}
>
<Show when={local.model.current()?.provider?.id}>
<ProviderIcon
id={local.model.current()!.provider.id}
class="size-4 shrink-0 opacity-40 group-hover:opacity-100 transition-opacity duration-150"
style={{ "will-change": "opacity", transform: "translateZ(0)" }}
/>
</Show>
<span class="truncate">
{local.model.current()?.name ?? language.t("dialog.model.select.title")}
</span>
<Icon name="chevron-down" size="small" class="shrink-0" />
</Button>
</TooltipKeybind>
}
>
<TooltipKeybind
placement="top"
gutter={8}
title={language.t("command.model.choose")}
keybind={command.keybind("model.choose")}
>
<ModelSelectorPopover
triggerAs={Button}
triggerProps={{
variant: "ghost",
size: "small",
style: {
opacity: buttonsSpring(),
transform: `scale(${0.95 + buttonsSpring() * 0.05})`,
filter: `blur(${(1 - buttonsSpring()) * 2}px)`,
},
class: "min-w-0 max-w-[240px] text-13-regular text-text-base group",
}}
>
<Show when={local.model.current()?.provider?.id}>
<ProviderIcon
id={local.model.current()!.provider.id}
class="size-4 shrink-0 opacity-40 group-hover:opacity-100 transition-opacity duration-150"
style={{ "will-change": "opacity", transform: "translateZ(0)" }}
/>
</Show>
<span class="truncate">
{local.model.current()?.name ?? language.t("dialog.model.select.title")}
</span>
<Icon name="chevron-down" size="small" class="shrink-0" />
</ModelSelectorPopover>
</TooltipKeybind>
</Show>
<TooltipKeybind
placement="top"
gutter={8}
title={language.t("command.model.variant.cycle")}
keybind={command.keybind("model.variant.cycle")}
>
<Select
size="small"
options={variants()}
current={local.model.variant.current() ?? "default"}
label={(x) => (x === "default" ? language.t("common.default") : x)}
onSelect={(x) => local.model.variant.set(x === "default" ? undefined : x)}
class="capitalize max-w-[160px]"
valueClass="truncate text-13-regular text-text-base"
triggerStyle={{
opacity: buttonsSpring(),
transform: `scale(${0.95 + buttonsSpring() * 0.05})`,
filter: `blur(${(1 - buttonsSpring()) * 2}px)`,
}}
variant="ghost"
/>
</TooltipKeybind>
</div>
</div>
</div>
@@ -1386,7 +1455,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
current={local.agent.current()?.name ?? ""}
onSelect={local.agent.set}
class="capitalize max-w-[160px]"
valueClass="truncate text-13-regular"
valueClass="truncate text-13-regular text-text-base"
triggerStyle={{
height: "28px",
opacity: buttonsSpring(),
@@ -1397,93 +1466,32 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
variant="ghost"
/>
</TooltipKeybind>
<Show
when={providers.paid().length > 0}
fallback={
<TooltipKeybind
placement="top"
gutter={4}
title={language.t("command.model.choose")}
keybind={command.keybind("model.choose")}
>
<Button
as="div"
variant="ghost"
size="normal"
class="min-w-0 max-w-[320px] text-13-regular group"
style={{
height: "28px",
opacity: buttonsSpring(),
transform: `scale(${0.95 + buttonsSpring() * 0.05})`,
filter: `blur(${(1 - buttonsSpring()) * 2}px)`,
"pointer-events": buttonsSpring() > 0.5 ? "auto" : "none",
}}
onClick={() => dialog.show(() => <DialogSelectModelUnpaid />)}
>
<Show when={local.model.current()?.provider?.id}>
<ProviderIcon
id={local.model.current()!.provider.id}
class="size-4 shrink-0 opacity-40 group-hover:opacity-100 transition-opacity duration-150"
style={{ "will-change": "opacity", transform: "translateZ(0)" }}
/>
</Show>
<span class="truncate">
{local.model.current()?.name ?? language.t("dialog.model.select.title")}
</span>
<Icon name="chevron-down" size="small" class="shrink-0" />
</Button>
</TooltipKeybind>
}
>
<TooltipKeybind
placement="top"
gutter={4}
title={language.t("command.model.choose")}
keybind={command.keybind("model.choose")}
>
<ModelSelectorPopover
triggerAs={Button}
triggerProps={{
variant: "ghost",
size: "normal",
style: {
height: "28px",
opacity: buttonsSpring(),
transform: `scale(${0.95 + buttonsSpring() * 0.05})`,
filter: `blur(${(1 - buttonsSpring()) * 2}px)`,
"pointer-events": buttonsSpring() > 0.5 ? "auto" : "none",
},
class: "min-w-0 max-w-[320px] text-13-regular group",
}}
>
<Show when={local.model.current()?.provider?.id}>
<ProviderIcon
id={local.model.current()!.provider.id}
class="size-4 shrink-0 opacity-40 group-hover:opacity-100 transition-opacity duration-150"
style={{ "will-change": "opacity", transform: "translateZ(0)" }}
/>
</Show>
<span class="truncate">
{local.model.current()?.name ?? language.t("dialog.model.select.title")}
</span>
<Icon name="chevron-down" size="small" class="shrink-0" />
</ModelSelectorPopover>
</TooltipKeybind>
</Show>
<TooltipKeybind
placement="top"
gutter={4}
title={language.t("command.model.variant.cycle")}
keybind={command.keybind("model.variant.cycle")}
title={language.t(
accepting() ? "command.permissions.autoaccept.disable" : "command.permissions.autoaccept.enable",
)}
keybind={command.keybind("permissions.autoaccept")}
>
<Select
size="normal"
options={variants()}
current={local.model.variant.current() ?? "default"}
label={(x) => (x === "default" ? language.t("common.default") : x)}
onSelect={(x) => local.model.variant.set(x === "default" ? undefined : x)}
class="capitalize max-w-[160px]"
valueClass="truncate text-13-regular"
options={["default", "autoaccept"] as const}
current={accepting() ? "autoaccept" : "default"}
label={(x) =>
x === "autoaccept"
? language.t("command.permissions.autoaccept.enable")
: `${language.t("common.default")} ${language.t("command.category.permissions")}`
}
onSelect={(x) => {
if (!x) return
if (x === "autoaccept" && accepting()) return
if (x === "default" && !accepting()) return
flip()
}}
class="max-w-[220px]"
valueClass="truncate text-13-regular text-text-base"
triggerStyle={{
height: "28px",
opacity: buttonsSpring(),

View File

@@ -4,7 +4,6 @@ import { useSync } from "@/context/sync"
import { useSDK } from "@/context/sdk"
import { useLanguage } from "@/context/language"
import { Icon } from "@opencode-ai/ui/icon"
import { Mark } from "@opencode-ai/ui/logo"
import { getDirectory, getFilename } from "@opencode-ai/util/path"
const MAIN_WORKTREE = "main"
@@ -53,10 +52,7 @@ export function NewSessionView(props: NewSessionViewProps) {
<div class="h-12 shrink-0" aria-hidden />
<div class="flex-1 px-6 pb-30 flex items-center justify-center text-center">
<div class="w-full max-w-200 flex flex-col items-center text-center gap-4">
<div class="flex flex-col items-center gap-6">
<Mark class="w-10" />
<div class="text-20-medium text-text-strong">{language.t("session.new.title")}</div>
</div>
<div class="text-20-medium text-text-strong">{language.t("session.new.title")}</div>
<div class="w-full flex flex-col gap-4 items-center">
<div class="flex items-start justify-center gap-3 min-h-5">
<div class="text-12-medium text-text-weak select-text leading-5 min-w-0 max-w-160 break-words text-center">

View File

@@ -217,7 +217,7 @@ export const Terminal = (props: TerminalProps) => {
const currentTheme = theme.themes()[theme.themeId()]
if (!currentTheme) return fallback
const variant = mode === "dark" ? currentTheme.dark : currentTheme.light
if (!variant?.seeds && !variant?.palette) return fallback
if (!variant?.seeds) return fallback
const resolved = resolveThemeVariant(variant, mode === "dark")
const text = resolved["text-stronger"] ?? fallback.foreground
const background = resolved["background-stronger"] ?? fallback.background

View File

@@ -27,7 +27,7 @@ import type { InitError } from "../pages/error"
import { useGlobalSDK } from "./global-sdk"
import { bootstrapDirectory, bootstrapGlobal } from "./global-sync/bootstrap"
import { createChildStoreManager } from "./global-sync/child-store"
import { applyDirectoryEvent, applyGlobalEvent, cleanupDroppedSessionCaches } from "./global-sync/event-reducer"
import { applyDirectoryEvent, applyGlobalEvent } from "./global-sync/event-reducer"
import { createRefreshQueue } from "./global-sync/queue"
import { estimateRootSessionTotal, loadRootSessionsWithFallback } from "./global-sync/session-load"
import { trimSessions } from "./global-sync/session-trim"
@@ -189,7 +189,6 @@ function createGlobalSync() {
})
if (next.length !== store.session.length) {
setStore("session", reconcile(next, { key: "id" }))
cleanupDroppedSessionCaches(store, setStore, next, setSessionTodo)
}
children.unpin(directory)
return
@@ -221,7 +220,6 @@ function createGlobalSync() {
}),
)
setStore("session", reconcile(sessions, { key: "id" }))
cleanupDroppedSessionCaches(store, setStore, sessions, setSessionTodo)
sessionMeta.set(directory, { limit })
})
.catch((err) => {

View File

@@ -2,7 +2,7 @@ import { describe, expect, test } from "bun:test"
import type { Message, Part, PermissionRequest, Project, QuestionRequest, Session } from "@opencode-ai/sdk/v2/client"
import { createStore } from "solid-js/store"
import type { State } from "./types"
import { applyDirectoryEvent, applyGlobalEvent, cleanupDroppedSessionCaches } from "./event-reducer"
import { applyDirectoryEvent, applyGlobalEvent } from "./event-reducer"
const rootSession = (input: { id: string; parentID?: string; archived?: number }) =>
({
@@ -248,62 +248,6 @@ describe("applyDirectoryEvent", () => {
}
})
test("cleans caches for trimmed sessions on session.created", () => {
const dropped = rootSession({ id: "ses_b" })
const kept = rootSession({ id: "ses_a" })
const message = userMessage("msg_1", dropped.id)
const todos: string[] = []
const [store, setStore] = createStore(
baseState({
limit: 1,
session: [dropped],
message: { [dropped.id]: [message] },
part: { [message.id]: [textPart("prt_1", dropped.id, message.id)] },
session_diff: { [dropped.id]: [] },
todo: { [dropped.id]: [] },
permission: { [dropped.id]: [] },
question: { [dropped.id]: [] },
session_status: { [dropped.id]: { type: "busy" } },
}),
)
applyDirectoryEvent({
event: { type: "session.created", properties: { info: kept } },
store,
setStore,
push() {},
directory: "/tmp",
loadLsp() {},
setSessionTodo(sessionID, value) {
if (value !== undefined) return
todos.push(sessionID)
},
})
expect(store.session.map((x) => x.id)).toEqual([kept.id])
expect(store.message[dropped.id]).toBeUndefined()
expect(store.part[message.id]).toBeUndefined()
expect(store.session_diff[dropped.id]).toBeUndefined()
expect(store.todo[dropped.id]).toBeUndefined()
expect(store.permission[dropped.id]).toBeUndefined()
expect(store.question[dropped.id]).toBeUndefined()
expect(store.session_status[dropped.id]).toBeUndefined()
expect(todos).toEqual([dropped.id])
})
test("cleanupDroppedSessionCaches clears part-only orphan state", () => {
const [store, setStore] = createStore(
baseState({
session: [rootSession({ id: "ses_keep" })],
part: { msg_1: [textPart("prt_1", "ses_drop", "msg_1")] },
}),
)
cleanupDroppedSessionCaches(store, setStore, store.session)
expect(store.part.msg_1).toBeUndefined()
})
test("upserts and removes messages while clearing orphaned parts", () => {
const sessionID = "ses_1"
const [store, setStore] = createStore(

View File

@@ -13,7 +13,6 @@ import type {
} from "@opencode-ai/sdk/v2/client"
import type { State, VcsCache } from "./types"
import { trimSessions } from "./session-trim"
import { dropSessionCaches } from "./session-cache"
export function applyGlobalEvent(input: {
event: { type: string; properties?: unknown }
@@ -41,44 +40,37 @@ export function applyGlobalEvent(input: {
}
function cleanupSessionCaches(
store: Store<State>,
setStore: SetStoreFunction<State>,
sessionID: string,
setSessionTodo?: (sessionID: string, todos: Todo[] | undefined) => void,
) {
if (!sessionID) return
const hasAny =
store.message[sessionID] !== undefined ||
store.session_diff[sessionID] !== undefined ||
store.todo[sessionID] !== undefined ||
store.permission[sessionID] !== undefined ||
store.question[sessionID] !== undefined ||
store.session_status[sessionID] !== undefined
setSessionTodo?.(sessionID, undefined)
if (!hasAny) return
setStore(
produce((draft) => {
dropSessionCaches(draft, [sessionID])
}),
)
}
export function cleanupDroppedSessionCaches(
store: Store<State>,
setStore: SetStoreFunction<State>,
next: Session[],
setSessionTodo?: (sessionID: string, todos: Todo[] | undefined) => void,
) {
const keep = new Set(next.map((item) => item.id))
const stale = [
...Object.keys(store.message),
...Object.keys(store.session_diff),
...Object.keys(store.todo),
...Object.keys(store.permission),
...Object.keys(store.question),
...Object.keys(store.session_status),
...Object.values(store.part)
.map((parts) => parts?.find((part) => !!part?.sessionID)?.sessionID)
.filter((sessionID): sessionID is string => !!sessionID),
].filter((sessionID, index, list) => !keep.has(sessionID) && list.indexOf(sessionID) === index)
if (stale.length === 0) return
for (const sessionID of stale) {
setSessionTodo?.(sessionID, undefined)
}
setStore(
produce((draft) => {
dropSessionCaches(draft, stale)
const messages = draft.message[sessionID]
if (messages) {
for (const message of messages) {
const id = message?.id
if (!id) continue
delete draft.part[id]
}
}
delete draft.message[sessionID]
delete draft.session_diff[sessionID]
delete draft.todo[sessionID]
delete draft.permission[sessionID]
delete draft.question[sessionID]
delete draft.session_status[sessionID]
}),
)
}
@@ -110,7 +102,6 @@ export function applyDirectoryEvent(input: {
next.splice(result.index, 0, info)
const trimmed = trimSessions(next, { limit: input.store.limit, permission: input.store.permission })
input.setStore("session", reconcile(trimmed, { key: "id" }))
cleanupDroppedSessionCaches(input.store, input.setStore, trimmed, input.setSessionTodo)
if (!info.parentID) input.setStore("sessionTotal", (value) => value + 1)
break
}
@@ -126,7 +117,7 @@ export function applyDirectoryEvent(input: {
}),
)
}
cleanupSessionCaches(input.setStore, info.id, input.setSessionTodo)
cleanupSessionCaches(input.store, input.setStore, info.id, input.setSessionTodo)
if (info.parentID) break
input.setStore("sessionTotal", (value) => Math.max(0, value - 1))
break
@@ -139,7 +130,6 @@ export function applyDirectoryEvent(input: {
next.splice(result.index, 0, info)
const trimmed = trimSessions(next, { limit: input.store.limit, permission: input.store.permission })
input.setStore("session", reconcile(trimmed, { key: "id" }))
cleanupDroppedSessionCaches(input.store, input.setStore, trimmed, input.setSessionTodo)
break
}
case "session.deleted": {
@@ -153,7 +143,7 @@ export function applyDirectoryEvent(input: {
}),
)
}
cleanupSessionCaches(input.setStore, info.id, input.setSessionTodo)
cleanupSessionCaches(input.store, input.setStore, info.id, input.setSessionTodo)
if (info.parentID) break
input.setStore("sessionTotal", (value) => Math.max(0, value - 1))
break

View File

@@ -1,102 +0,0 @@
import { describe, expect, test } from "bun:test"
import type {
FileDiff,
Message,
Part,
PermissionRequest,
QuestionRequest,
SessionStatus,
Todo,
} from "@opencode-ai/sdk/v2/client"
import { dropSessionCaches, pickSessionCacheEvictions } from "./session-cache"
const msg = (id: string, sessionID: string) =>
({
id,
sessionID,
role: "user",
time: { created: 1 },
agent: "assistant",
model: { providerID: "openai", modelID: "gpt" },
}) as Message
const part = (id: string, sessionID: string, messageID: string) =>
({
id,
sessionID,
messageID,
type: "text",
text: id,
}) as Part
describe("app session cache", () => {
test("dropSessionCaches clears orphaned parts without message rows", () => {
const store: {
session_status: Record<string, SessionStatus | undefined>
session_diff: Record<string, FileDiff[] | undefined>
todo: Record<string, Todo[] | undefined>
message: Record<string, Message[] | undefined>
part: Record<string, Part[] | undefined>
permission: Record<string, PermissionRequest[] | undefined>
question: Record<string, QuestionRequest[] | undefined>
} = {
session_status: { ses_1: { type: "busy" } as SessionStatus },
session_diff: { ses_1: [] },
todo: { ses_1: [] as Todo[] },
message: {},
part: { msg_1: [part("prt_1", "ses_1", "msg_1")] },
permission: { ses_1: [] as PermissionRequest[] },
question: { ses_1: [] as QuestionRequest[] },
}
dropSessionCaches(store, ["ses_1"])
expect(store.message.ses_1).toBeUndefined()
expect(store.part.msg_1).toBeUndefined()
expect(store.todo.ses_1).toBeUndefined()
expect(store.session_diff.ses_1).toBeUndefined()
expect(store.session_status.ses_1).toBeUndefined()
expect(store.permission.ses_1).toBeUndefined()
expect(store.question.ses_1).toBeUndefined()
})
test("dropSessionCaches clears message-backed parts", () => {
const m = msg("msg_1", "ses_1")
const store: {
session_status: Record<string, SessionStatus | undefined>
session_diff: Record<string, FileDiff[] | undefined>
todo: Record<string, Todo[] | undefined>
message: Record<string, Message[] | undefined>
part: Record<string, Part[] | undefined>
permission: Record<string, PermissionRequest[] | undefined>
question: Record<string, QuestionRequest[] | undefined>
} = {
session_status: {},
session_diff: {},
todo: {},
message: { ses_1: [m] },
part: { [m.id]: [part("prt_1", "ses_1", m.id)] },
permission: {},
question: {},
}
dropSessionCaches(store, ["ses_1"])
expect(store.message.ses_1).toBeUndefined()
expect(store.part[m.id]).toBeUndefined()
})
test("pickSessionCacheEvictions preserves requested sessions", () => {
const seen = new Set(["ses_1", "ses_2", "ses_3"])
const stale = pickSessionCacheEvictions({
seen,
keep: "ses_4",
limit: 2,
preserve: ["ses_1"],
})
expect(stale).toEqual(["ses_2", "ses_3"])
expect([...seen]).toEqual(["ses_1", "ses_4"])
})
})

View File

@@ -1,62 +0,0 @@
import type {
FileDiff,
Message,
Part,
PermissionRequest,
QuestionRequest,
SessionStatus,
Todo,
} from "@opencode-ai/sdk/v2/client"
export const SESSION_CACHE_LIMIT = 40
type SessionCache = {
session_status: Record<string, SessionStatus | undefined>
session_diff: Record<string, FileDiff[] | undefined>
todo: Record<string, Todo[] | undefined>
message: Record<string, Message[] | undefined>
part: Record<string, Part[] | undefined>
permission: Record<string, PermissionRequest[] | undefined>
question: Record<string, QuestionRequest[] | undefined>
}
export function dropSessionCaches(store: SessionCache, sessionIDs: Iterable<string>) {
const stale = new Set(Array.from(sessionIDs).filter(Boolean))
if (stale.size === 0) return
for (const key of Object.keys(store.part)) {
const parts = store.part[key]
if (!parts?.some((part) => stale.has(part?.sessionID ?? ""))) continue
delete store.part[key]
}
for (const sessionID of stale) {
delete store.message[sessionID]
delete store.todo[sessionID]
delete store.session_diff[sessionID]
delete store.session_status[sessionID]
delete store.permission[sessionID]
delete store.question[sessionID]
}
}
export function pickSessionCacheEvictions(input: {
seen: Set<string>
keep: string
limit: number
preserve?: Iterable<string>
}) {
const stale: string[] = []
const keep = new Set([input.keep, ...Array.from(input.preserve ?? [])])
if (input.seen.has(input.keep)) input.seen.delete(input.keep)
input.seen.add(input.keep)
for (const id of input.seen) {
if (input.seen.size - stale.length <= input.limit) break
if (keep.has(id)) continue
stale.push(id)
}
for (const id of stale) {
input.seen.delete(id)
}
return stale
}

View File

@@ -6,7 +6,6 @@ import { createSimpleContext } from "@opencode-ai/ui/context"
import { useGlobalSync } from "./global-sync"
import { useSDK } from "./sdk"
import type { Message, Part } from "@opencode-ai/sdk/v2/client"
import { SESSION_CACHE_LIMIT, dropSessionCaches, pickSessionCacheEvictions } from "./global-sync/session-cache"
function sortParts(parts: Part[]) {
return parts.filter((part) => !!part?.id).sort((a, b) => cmp(a.id, b.id))
@@ -109,8 +108,6 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
const inflight = new Map<string, Promise<void>>()
const inflightDiff = new Map<string, Promise<void>>()
const inflightTodo = new Map<string, Promise<void>>()
const maxDirs = 30
const seen = new Map<string, Set<string>>()
const [meta, setMeta] = createStore({
limit: {} as Record<string, number>,
complete: {} as Record<string, boolean>,
@@ -124,62 +121,6 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
return undefined
}
const seenFor = (directory: string) => {
const existing = seen.get(directory)
if (existing) {
seen.delete(directory)
seen.set(directory, existing)
return existing
}
const created = new Set<string>()
seen.set(directory, created)
while (seen.size > maxDirs) {
const first = seen.keys().next().value
if (!first) break
const stale = [...(seen.get(first) ?? [])]
seen.delete(first)
const [, setStore] = globalSync.child(first, { bootstrap: false })
evict(first, setStore, stale)
}
return created
}
const clearMeta = (directory: string, sessionIDs: string[]) => {
if (sessionIDs.length === 0) return
setMeta(
produce((draft) => {
for (const sessionID of sessionIDs) {
const key = keyFor(directory, sessionID)
delete draft.limit[key]
delete draft.complete[key]
delete draft.loading[key]
}
}),
)
}
const evict = (directory: string, setStore: Setter, sessionIDs: string[]) => {
if (sessionIDs.length === 0) return
for (const sessionID of sessionIDs) {
globalSync.todo.set(sessionID, undefined)
}
setStore(
produce((draft) => {
dropSessionCaches(draft, sessionIDs)
}),
)
clearMeta(directory, sessionIDs)
}
const touch = (directory: string, setStore: Setter, sessionID: string) => {
const stale = pickSessionCacheEvictions({
seen: seenFor(directory),
keep: sessionID,
limit: SESSION_CACHE_LIMIT,
})
evict(directory, setStore, stale)
}
const fetchMessages = async (input: { client: typeof sdk.client; sessionID: string; limit: number }) => {
const messages = await retry(() =>
input.client.session.messages({ sessionID: input.sessionID, limit: input.limit }),
@@ -194,8 +135,6 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
}
}
const tracked = (directory: string, sessionID: string) => seen.get(directory)?.has(sessionID) ?? false
const loadMessages = async (input: {
directory: string
client: typeof sdk.client
@@ -209,7 +148,6 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
setMeta("loading", key, true)
await fetchMessages(input)
.then((next) => {
if (!tracked(input.directory, input.sessionID)) return
batch(() => {
input.setStore("message", input.sessionID, reconcile(next.session, { key: "id" }))
for (const p of next.part) {
@@ -220,7 +158,6 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
})
})
.finally(() => {
if (!tracked(input.directory, input.sessionID)) return
setMeta("loading", key, false)
})
}
@@ -287,16 +224,11 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
const key = keyFor(directory, sessionID)
const hasSession = Binary.search(store.session, sessionID, (s) => s.id).found
touch(directory, setStore, sessionID)
if (store.message[sessionID] !== undefined && hasSession && meta.limit[key] !== undefined) return
const limit = meta.limit[key] ?? messagePageSize
const sessionReq = hasSession
? Promise.resolve()
: retry(() => client.session.get({ sessionID })).then((session) => {
if (!tracked(directory, sessionID)) return
const data = session.data
if (!data) return
setStore(
@@ -326,13 +258,11 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
const directory = sdk.directory
const client = sdk.client
const [store, setStore] = globalSync.child(directory)
touch(directory, setStore, sessionID)
if (store.session_diff[sessionID] !== undefined) return
const key = keyFor(directory, sessionID)
return runInflight(inflightDiff, key, () =>
retry(() => client.session.diff({ sessionID })).then((diff) => {
if (!tracked(directory, sessionID)) return
setStore("session_diff", sessionID, reconcile(diff.data ?? [], { key: "file" }))
}),
)
@@ -341,7 +271,6 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
const directory = sdk.directory
const client = sdk.client
const [store, setStore] = globalSync.child(directory)
touch(directory, setStore, sessionID)
const existing = store.todo[sessionID]
const cached = globalSync.data.session_todo[sessionID]
if (existing !== undefined) {
@@ -358,7 +287,6 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
const key = keyFor(directory, sessionID)
return runInflight(inflightTodo, key, () =>
retry(() => client.session.todo({ sessionID })).then((todo) => {
if (!tracked(directory, sessionID)) return
const list = todo.data ?? []
setStore("todo", sessionID, reconcile(list, { key: "id" }))
globalSync.todo.set(sessionID, list)
@@ -382,7 +310,6 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
const directory = sdk.directory
const client = sdk.client
const [, setStore] = globalSync.child(directory)
touch(directory, setStore, sessionID)
const key = keyFor(directory, sessionID)
const step = count ?? messagePageSize
if (meta.loading[key]) return
@@ -398,11 +325,6 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
})
},
},
evict(sessionID: string, directory = sdk.directory) {
const [, setStore] = globalSync.child(directory)
seenFor(directory).delete(sessionID)
evict(directory, setStore, [sessionID])
},
fetch: async (count = 10) => {
const directory = sdk.directory
const client = sdk.client

View File

@@ -2,7 +2,6 @@ import { beforeAll, describe, expect, mock, test } from "bun:test"
let getWorkspaceTerminalCacheKey: (dir: string) => string
let getLegacyTerminalStorageKeys: (dir: string, legacySessionID?: string) => string[]
let migrateTerminalState: (value: unknown) => unknown
beforeAll(async () => {
mock.module("@solidjs/router", () => ({
@@ -18,7 +17,6 @@ beforeAll(async () => {
const mod = await import("./terminal")
getWorkspaceTerminalCacheKey = mod.getWorkspaceTerminalCacheKey
getLegacyTerminalStorageKeys = mod.getLegacyTerminalStorageKeys
migrateTerminalState = mod.migrateTerminalState
})
describe("getWorkspaceTerminalCacheKey", () => {
@@ -39,44 +37,3 @@ describe("getLegacyTerminalStorageKeys", () => {
])
})
})
describe("migrateTerminalState", () => {
test("drops invalid terminals and restores a valid active terminal", () => {
expect(
migrateTerminalState({
active: "missing",
all: [
null,
{ id: "one", title: "Terminal 2" },
{ id: "one", title: "duplicate", titleNumber: 9 },
{ id: "two", title: "logs", titleNumber: 4, rows: 24, cols: 80 },
{ title: "no-id" },
],
}),
).toEqual({
active: "one",
all: [
{ id: "one", title: "Terminal 2", titleNumber: 2 },
{ id: "two", title: "logs", titleNumber: 4, rows: 24, cols: 80 },
],
})
})
test("keeps a valid active id", () => {
expect(
migrateTerminalState({
active: "two",
all: [
{ id: "one", title: "Terminal 1" },
{ id: "two", title: "shell", titleNumber: 7 },
],
}),
).toEqual({
active: "two",
all: [
{ id: "one", title: "Terminal 1", titleNumber: 1 },
{ id: "two", title: "shell", titleNumber: 7 },
],
})
})
})

View File

@@ -1,6 +1,6 @@
import { createStore, produce } from "solid-js/store"
import { createSimpleContext } from "@opencode-ai/ui/context"
import { batch, createEffect, createMemo, createRoot, on, onCleanup } from "solid-js"
import { batch, createEffect, createMemo, createRoot, onCleanup } from "solid-js"
import { useParams } from "@solidjs/router"
import { useSDK } from "./sdk"
import type { Platform } from "./platform"
@@ -20,71 +20,6 @@ export type LocalPTY = {
const WORKSPACE_KEY = "__workspace__"
const MAX_TERMINAL_SESSIONS = 20
function record(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value)
}
function text(value: unknown) {
return typeof value === "string" ? value : undefined
}
function num(value: unknown) {
return typeof value === "number" && Number.isFinite(value) ? value : undefined
}
function numberFromTitle(title: string) {
const match = title.match(/^Terminal (\d+)$/)
if (!match) return
const value = Number(match[1])
if (!Number.isFinite(value) || value <= 0) return
return value
}
function pty(value: unknown): LocalPTY | undefined {
if (!record(value)) return
const id = text(value.id)
if (!id) return
const title = text(value.title) ?? ""
const number = num(value.titleNumber)
const rows = num(value.rows)
const cols = num(value.cols)
const buffer = text(value.buffer)
const scrollY = num(value.scrollY)
const cursor = num(value.cursor)
return {
id,
title,
titleNumber: number && number > 0 ? number : (numberFromTitle(title) ?? 0),
...(rows !== undefined ? { rows } : {}),
...(cols !== undefined ? { cols } : {}),
...(buffer !== undefined ? { buffer } : {}),
...(scrollY !== undefined ? { scrollY } : {}),
...(cursor !== undefined ? { cursor } : {}),
}
}
export function migrateTerminalState(value: unknown) {
if (!record(value)) return value
const seen = new Set<string>()
const all = (Array.isArray(value.all) ? value.all : []).flatMap((item) => {
const next = pty(item)
if (!next || seen.has(next.id)) return []
seen.add(next.id)
return [next]
})
const active = text(value.active)
return {
active: active && seen.has(active) ? active : all[0]?.id,
all,
}
}
export function getWorkspaceTerminalCacheKey(dir: string) {
return `${dir}:${WORKSPACE_KEY}`
}
@@ -103,16 +38,6 @@ type TerminalCacheEntry = {
const caches = new Set<Map<string, TerminalCacheEntry>>()
const trimTerminal = (pty: LocalPTY) => {
if (!pty.buffer && pty.cursor === undefined && pty.scrollY === undefined) return pty
return {
...pty,
buffer: undefined,
cursor: undefined,
scrollY: undefined,
}
}
export function clearWorkspaceTerminals(dir: string, sessionIDs?: string[], platform?: Platform) {
const key = getWorkspaceTerminalCacheKey(dir)
for (const cache of caches) {
@@ -136,11 +61,16 @@ export function clearWorkspaceTerminals(dir: string, sessionIDs?: string[], plat
function createWorkspaceTerminalSession(sdk: ReturnType<typeof useSDK>, dir: string, legacySessionID?: string) {
const legacy = getLegacyTerminalStorageKeys(dir, legacySessionID)
const numberFromTitle = (title: string) => {
const match = title.match(/^Terminal (\d+)$/)
if (!match) return
const value = Number(match[1])
if (!Number.isFinite(value) || value <= 0) return
return value
}
const [store, setStore, _, ready] = persisted(
{
...Persist.workspace(dir, "terminal", legacy),
migrate: migrateTerminalState,
},
Persist.workspace(dir, "terminal", legacy),
createStore<{
active?: string
all: LocalPTY[]
@@ -188,6 +118,26 @@ function createWorkspaceTerminalSession(sdk: ReturnType<typeof useSDK>, dir: str
})
onCleanup(unsub)
const meta = { migrated: false }
createEffect(() => {
if (!ready()) return
if (meta.migrated) return
meta.migrated = true
setStore("all", (all) => {
const next = all.map((pty) => {
const direct = Number.isFinite(pty.titleNumber) && pty.titleNumber > 0 ? pty.titleNumber : undefined
if (direct !== undefined) return pty
const parsed = numberFromTitle(pty.title)
if (parsed === undefined) return pty
return { ...pty, titleNumber: parsed }
})
if (next.every((pty, index) => pty === all[index])) return all
return next
})
})
return {
ready,
all: createMemo(() => store.all),
@@ -238,18 +188,6 @@ function createWorkspaceTerminalSession(sdk: ReturnType<typeof useSDK>, dir: str
console.error("Failed to update terminal", error)
})
},
trim(id: string) {
const index = store.all.findIndex((x) => x.id === id)
if (index === -1) return
setStore("all", index, (pty) => trimTerminal(pty))
},
trimAll() {
setStore("all", (all) => {
const next = all.map(trimTerminal)
if (next.every((pty, index) => pty === all[index])) return all
return next
})
},
async clone(id: string) {
const index = store.all.findIndex((x) => x.id === id)
const pty = store.all[index]
@@ -384,27 +322,12 @@ export const { use: useTerminal, provider: TerminalProvider } = createSimpleCont
const workspace = createMemo(() => loadWorkspace(params.dir!, params.id))
createEffect(
on(
() => ({ dir: params.dir, id: params.id }),
(next, prev) => {
if (!prev?.dir) return
if (next.dir === prev.dir && next.id === prev.id) return
if (next.dir === prev.dir && next.id) return
loadWorkspace(prev.dir, prev.id).trimAll()
},
{ defer: true },
),
)
return {
ready: () => workspace().ready(),
all: () => workspace().all(),
active: () => workspace().active(),
new: () => workspace().new(),
update: (pty: Partial<LocalPTY> & { id: string }) => workspace().update(pty),
trim: (id: string) => workspace().trim(id),
trimAll: () => workspace().trimAll(),
clone: (id: string) => workspace().clone(id),
open: (id: string) => workspace().open(id),
close: (id: string) => workspace().close(id),

View File

@@ -34,7 +34,6 @@ import { useProviders } from "@/hooks/use-providers"
import { showToast, Toast, toaster } from "@opencode-ai/ui/toast"
import { useGlobalSDK } from "@/context/global-sdk"
import { clearWorkspaceTerminals } from "@/context/terminal"
import { dropSessionCaches, pickSessionCacheEvictions } from "@/context/global-sync/session-cache"
import { useNotification } from "@/context/notification"
import { usePermission } from "@/context/permission"
import { Binary } from "@opencode-ai/util/binary"
@@ -54,7 +53,6 @@ import { useCommand, type CommandOption } from "@/context/command"
import { ConstrainDragXAxis } from "@/utils/solid-dnd"
import { DialogSelectDirectory } from "@/components/dialog-select-directory"
import { DialogEditProject } from "@/components/dialog-edit-project"
import { DebugBar } from "@/components/debug-bar"
import { Titlebar } from "@/components/titlebar"
import { useServer } from "@/context/server"
import { useLanguage, type Locale } from "@/context/language"
@@ -425,17 +423,6 @@ export default function Layout(props: ParentProps) {
return
}
if (
e.details?.type === "question.replied" ||
e.details?.type === "question.rejected" ||
e.details?.type === "permission.replied"
) {
const props = e.details.properties as { sessionID: string }
const sessionKey = `${e.name}:${props.sessionID}`
dismissSessionAlert(sessionKey)
return
}
if (e.details?.type !== "permission.asked" && e.details?.type !== "question.asked") return
const title =
e.details.type === "permission.asked"
@@ -670,24 +657,25 @@ export default function Layout(props: ParentProps) {
const prefetchQueues = new Map<string, PrefetchQueue>()
const PREFETCH_MAX_SESSIONS_PER_DIR = 10
const prefetchedByDir = new Map<string, Set<string>>()
const prefetchedByDir = new Map<string, Map<string, true>>()
const lruFor = (directory: string) => {
const existing = prefetchedByDir.get(directory)
if (existing) return existing
const created = new Set<string>()
const created = new Map<string, true>()
prefetchedByDir.set(directory, created)
return created
}
const markPrefetched = (directory: string, sessionID: string) => {
const lru = lruFor(directory)
return pickSessionCacheEvictions({
seen: lru,
keep: sessionID,
limit: PREFETCH_MAX_SESSIONS_PER_DIR,
preserve: directory === params.dir && params.id ? [params.id] : undefined,
})
if (lru.has(sessionID)) lru.delete(sessionID)
lru.set(sessionID, true)
while (lru.size > PREFETCH_MAX_SESSIONS_PER_DIR) {
const oldest = lru.keys().next().value as string | undefined
if (!oldest) return
lru.delete(oldest)
}
}
createEffect(() => {
@@ -736,7 +724,6 @@ export default function Layout(props: ParentProps) {
return retry(() => globalSDK.client.session.messages({ directory, sessionID, limit: prefetchChunk }))
.then((messages) => {
if (prefetchToken.value !== token) return
if (!lruFor(directory).has(sessionID)) return
const items = (messages.data ?? []).filter((x) => !!x?.info?.id)
const next = items.map((x) => x.info).filter((m): m is Message => !!m?.id)
@@ -800,18 +787,7 @@ export default function Layout(props: ParentProps) {
const lru = lruFor(directory)
const known = lru.has(session.id)
if (!known && lru.size >= PREFETCH_MAX_SESSIONS_PER_DIR && priority !== "high") return
const stale = markPrefetched(directory, session.id)
if (stale.length > 0) {
const [, setStore] = globalSync.child(directory, { bootstrap: false })
for (const id of stale) {
globalSync.todo.set(id, undefined)
}
setStore(
produce((draft) => {
dropSessionCaches(draft, stale)
}),
)
}
markPrefetched(directory, session.id)
if (priority === "high") q.pending.unshift(session.id)
if (priority !== "high") q.pending.push(session.id)
@@ -1903,7 +1879,6 @@ export default function Layout(props: ParentProps) {
const SidebarPanel = (panelProps: { project: LocalProject | undefined; mobile?: boolean; merged?: boolean }) => {
const merged = createMemo(() => panelProps.mobile || (panelProps.merged ?? layout.sidebar.opened()))
const hover = createMemo(() => !panelProps.mobile && panelProps.merged === false && !layout.sidebar.opened())
const projectName = createMemo(() => {
const project = panelProps.project
if (!project) return ""
@@ -1929,11 +1904,11 @@ export default function Layout(props: ParentProps) {
return (
<div
classList={{
"flex flex-col min-h-0 min-w-0 box-border rounded-tl-[12px] px-2": true,
"flex flex-col min-h-0 min-w-0 rounded-tl-[12px] px-2": true,
"border border-b-0 border-border-weak-base": !merged(),
"border-l border-t border-border-weaker-base": merged(),
"bg-background-base": merged() || hover(),
"bg-background-stronger": !merged() && !hover(),
"bg-background-base": merged(),
"bg-background-stronger": !merged(),
"flex-1 min-w-0": panelProps.mobile,
"max-w-full overflow-hidden": panelProps.mobile,
}}
@@ -2136,204 +2111,193 @@ export default function Layout(props: ParentProps) {
}
return (
<div class="relative bg-background-base flex-1 min-h-0 min-w-0 flex flex-col select-none [&_input]:select-text [&_textarea]:select-text [&_[contenteditable]]:select-text">
<div class="relative bg-background-base flex-1 min-h-0 flex flex-col select-none [&_input]:select-text [&_textarea]:select-text [&_[contenteditable]]:select-text">
<Titlebar />
<div class="flex-1 min-h-0 min-w-0 flex">
<div class="flex-1 min-h-0 relative">
<div class="size-full relative overflow-x-hidden">
<nav
aria-label={language.t("sidebar.nav.projectsAndSessions")}
data-component="sidebar-nav-desktop"
classList={{
"hidden xl:block": true,
"absolute inset-y-0 left-0": true,
"z-10": true,
}}
style={{ width: `${Math.max(layout.sidebar.width(), 244)}px` }}
ref={(el) => {
setState("nav", el)
}}
onMouseEnter={() => {
disarm()
}}
onMouseLeave={() => {
aim.reset()
if (!sidebarHovering()) return
<div class="flex-1 min-h-0 relative overflow-x-hidden">
<nav
aria-label={language.t("sidebar.nav.projectsAndSessions")}
data-component="sidebar-nav-desktop"
classList={{
"hidden xl:block": true,
"absolute inset-y-0 left-0": true,
"z-10": true,
}}
style={{ width: `${Math.max(layout.sidebar.width(), 244)}px` }}
ref={(el) => {
setState("nav", el)
}}
onMouseEnter={() => {
disarm()
}}
onMouseLeave={() => {
aim.reset()
if (!sidebarHovering()) return
arm()
}}
>
<div class="@container w-full h-full contain-strict">
<SidebarContent
opened={() => layout.sidebar.opened()}
aimMove={aim.move}
projects={() => layout.projects.list()}
renderProject={(project) => (
<SortableProject ctx={projectSidebarCtx} project={project} sortNow={sortNow} />
)}
handleDragStart={handleDragStart}
handleDragEnd={handleDragEnd}
handleDragOver={handleDragOver}
openProjectLabel={language.t("command.project.open")}
openProjectKeybind={() => command.keybind("project.open")}
onOpenProject={chooseProject}
renderProjectOverlay={() => (
<ProjectDragOverlay
projects={() => layout.projects.list()}
activeProject={() => store.activeProject}
/>
)}
settingsLabel={() => language.t("sidebar.settings")}
settingsKeybind={() => command.keybind("settings.open")}
onOpenSettings={openSettings}
helpLabel={() => language.t("sidebar.help")}
onOpenHelp={() => platform.openLink("https://opencode.ai/desktop-feedback")}
renderPanel={() => (
<Show when={currentProject()} keyed>
{(project) => <SidebarPanel project={project} merged />}
</Show>
)}
/>
</div>
<Show when={layout.sidebar.opened()}>
<div onPointerDown={() => setSizing(true)}>
<ResizeHandle
direction="horizontal"
size={layout.sidebar.width()}
min={244}
max={typeof window === "undefined" ? 1000 : window.innerWidth * 0.3 + 64}
collapseThreshold={244}
onResize={(w) => {
setSizing(true)
if (sizet !== undefined) clearTimeout(sizet)
sizet = window.setTimeout(() => setSizing(false), 120)
layout.sidebar.resize(w)
}}
onCollapse={layout.sidebar.close}
/>
</div>
</Show>
</nav>
<div
class="hidden xl:block pointer-events-none absolute top-0 right-0 z-0 border-t border-border-weaker-base"
style={{ left: "calc(4rem + 12px)" }}
/>
<div class="xl:hidden">
<div
classList={{
"fixed inset-x-0 top-10 bottom-0 z-40 transition-opacity duration-200": true,
"opacity-100 pointer-events-auto": layout.mobileSidebar.opened(),
"opacity-0 pointer-events-none": !layout.mobileSidebar.opened(),
}}
onClick={(e) => {
if (e.target === e.currentTarget) layout.mobileSidebar.hide()
}}
/>
<nav
aria-label={language.t("sidebar.nav.projectsAndSessions")}
data-component="sidebar-nav-mobile"
classList={{
"@container fixed top-10 bottom-0 left-0 z-50 w-full max-w-[400px] overflow-hidden border-r border-border-weaker-base bg-background-base transition-transform duration-200 ease-out": true,
"translate-x-0": layout.mobileSidebar.opened(),
"-translate-x-full": !layout.mobileSidebar.opened(),
}}
onClick={(e) => e.stopPropagation()}
>
<SidebarContent
mobile
opened={() => layout.sidebar.opened()}
aimMove={aim.move}
projects={() => layout.projects.list()}
renderProject={(project) => (
<SortableProject ctx={projectSidebarCtx} project={project} sortNow={sortNow} mobile />
)}
handleDragStart={handleDragStart}
handleDragEnd={handleDragEnd}
handleDragOver={handleDragOver}
openProjectLabel={language.t("command.project.open")}
openProjectKeybind={() => command.keybind("project.open")}
onOpenProject={chooseProject}
renderProjectOverlay={() => (
<ProjectDragOverlay
projects={() => layout.projects.list()}
activeProject={() => store.activeProject}
/>
)}
settingsLabel={() => language.t("sidebar.settings")}
settingsKeybind={() => command.keybind("settings.open")}
onOpenSettings={openSettings}
helpLabel={() => language.t("sidebar.help")}
onOpenHelp={() => platform.openLink("https://opencode.ai/desktop-feedback")}
renderPanel={() => <SidebarPanel project={currentProject()} mobile />}
/>
</nav>
</div>
<div
classList={{
"absolute inset-0": true,
"xl:inset-y-0 xl:right-0 xl:left-[var(--main-left)]": true,
"z-20": true,
"transition-[left] duration-200 ease-[cubic-bezier(0.22,1,0.36,1)] will-change-[left] motion-reduce:transition-none":
!sizing(),
}}
style={{
"--main-left": layout.sidebar.opened() ? `${Math.max(layout.sidebar.width(), 244)}px` : "4rem",
}}
>
<main
classList={{
"size-full overflow-x-hidden flex flex-col items-start contain-strict border-t border-border-weak-base bg-background-base xl:border-l xl:rounded-tl-[12px]": true,
}}
>
<Show when={!autoselecting()} fallback={<div class="size-full" />}>
{props.children}
arm()
}}
>
<div class="@container w-full h-full contain-strict">
<SidebarContent
opened={() => layout.sidebar.opened()}
aimMove={aim.move}
projects={() => layout.projects.list()}
renderProject={(project) => (
<SortableProject ctx={projectSidebarCtx} project={project} sortNow={sortNow} />
)}
handleDragStart={handleDragStart}
handleDragEnd={handleDragEnd}
handleDragOver={handleDragOver}
openProjectLabel={language.t("command.project.open")}
openProjectKeybind={() => command.keybind("project.open")}
onOpenProject={chooseProject}
renderProjectOverlay={() => (
<ProjectDragOverlay projects={() => layout.projects.list()} activeProject={() => store.activeProject} />
)}
settingsLabel={() => language.t("sidebar.settings")}
settingsKeybind={() => command.keybind("settings.open")}
onOpenSettings={openSettings}
helpLabel={() => language.t("sidebar.help")}
onOpenHelp={() => platform.openLink("https://opencode.ai/desktop-feedback")}
renderPanel={() => (
<Show when={currentProject()} keyed>
{(project) => <SidebarPanel project={project} merged />}
</Show>
</main>
</div>
<div
classList={{
"hidden xl:flex absolute inset-y-0 left-16 z-30": true,
"opacity-100 translate-x-0 pointer-events-auto": peeked() && !layout.sidebar.opened(),
"opacity-0 -translate-x-2 pointer-events-none": !peeked() || layout.sidebar.opened(),
"transition-[opacity,transform] motion-reduce:transition-none": true,
"duration-180 ease-out": peeked() && !layout.sidebar.opened(),
"duration-120 ease-in": !peeked() || layout.sidebar.opened(),
}}
onMouseMove={disarm}
onMouseEnter={() => {
disarm()
aim.reset()
}}
onPointerDown={disarm}
onMouseLeave={() => {
arm()
}}
>
<Show when={peek()} keyed>
{(project) => <SidebarPanel project={project} merged={false} />}
</Show>
</div>
<div
classList={{
"hidden xl:block pointer-events-none absolute inset-y-0 right-0 z-25 overflow-hidden": true,
"opacity-100 translate-x-0": peeked() && !layout.sidebar.opened(),
"opacity-0 -translate-x-2": !peeked() || layout.sidebar.opened(),
"transition-[opacity,transform] motion-reduce:transition-none": true,
"duration-180 ease-out": peeked() && !layout.sidebar.opened(),
"duration-120 ease-in": !peeked() || layout.sidebar.opened(),
}}
style={{ left: `calc(4rem + ${Math.max(Math.max(layout.sidebar.width(), 244) - 64, 0)}px)` }}
>
<div class="h-full w-px" style={{ "box-shadow": "var(--shadow-sidebar-overlay)" }} />
</div>
)}
/>
</div>
<Show when={layout.sidebar.opened()}>
<div onPointerDown={() => setSizing(true)}>
<ResizeHandle
direction="horizontal"
size={layout.sidebar.width()}
min={244}
max={typeof window === "undefined" ? 1000 : window.innerWidth * 0.3 + 64}
collapseThreshold={244}
onResize={(w) => {
setSizing(true)
if (sizet !== undefined) clearTimeout(sizet)
sizet = window.setTimeout(() => setSizing(false), 120)
layout.sidebar.resize(w)
}}
onCollapse={layout.sidebar.close}
/>
</div>
</Show>
</nav>
<div
class="hidden xl:block pointer-events-none absolute top-0 right-0 z-0 border-t border-border-weaker-base"
style={{ left: "calc(4rem + 12px)" }}
/>
<div class="xl:hidden">
<div
classList={{
"fixed inset-x-0 top-10 bottom-0 z-40 transition-opacity duration-200": true,
"opacity-100 pointer-events-auto": layout.mobileSidebar.opened(),
"opacity-0 pointer-events-none": !layout.mobileSidebar.opened(),
}}
onClick={(e) => {
if (e.target === e.currentTarget) layout.mobileSidebar.hide()
}}
/>
<nav
aria-label={language.t("sidebar.nav.projectsAndSessions")}
data-component="sidebar-nav-mobile"
classList={{
"@container fixed top-10 bottom-0 left-0 z-50 w-full max-w-[400px] overflow-hidden border-r border-border-weaker-base bg-background-base transition-transform duration-200 ease-out": true,
"translate-x-0": layout.mobileSidebar.opened(),
"-translate-x-full": !layout.mobileSidebar.opened(),
}}
onClick={(e) => e.stopPropagation()}
>
<SidebarContent
mobile
opened={() => layout.sidebar.opened()}
aimMove={aim.move}
projects={() => layout.projects.list()}
renderProject={(project) => (
<SortableProject ctx={projectSidebarCtx} project={project} sortNow={sortNow} mobile />
)}
handleDragStart={handleDragStart}
handleDragEnd={handleDragEnd}
handleDragOver={handleDragOver}
openProjectLabel={language.t("command.project.open")}
openProjectKeybind={() => command.keybind("project.open")}
onOpenProject={chooseProject}
renderProjectOverlay={() => (
<ProjectDragOverlay projects={() => layout.projects.list()} activeProject={() => store.activeProject} />
)}
settingsLabel={() => language.t("sidebar.settings")}
settingsKeybind={() => command.keybind("settings.open")}
onOpenSettings={openSettings}
helpLabel={() => language.t("sidebar.help")}
onOpenHelp={() => platform.openLink("https://opencode.ai/desktop-feedback")}
renderPanel={() => <SidebarPanel project={currentProject()} mobile />}
/>
</nav>
</div>
<div
classList={{
"absolute inset-0": true,
"xl:inset-y-0 xl:right-0 xl:left-[var(--main-left)]": true,
"z-20": true,
"transition-[left] duration-200 ease-[cubic-bezier(0.22,1,0.36,1)] will-change-[left] motion-reduce:transition-none":
!sizing(),
}}
style={{
"--main-left": layout.sidebar.opened() ? `${Math.max(layout.sidebar.width(), 244)}px` : "4rem",
}}
>
<main
classList={{
"size-full overflow-x-hidden flex flex-col items-start contain-strict border-t border-border-weak-base bg-background-base xl:border-l xl:rounded-tl-[12px]": true,
}}
>
<Show when={!autoselecting()} fallback={<div class="size-full" />}>
{props.children}
</Show>
</main>
</div>
<div
classList={{
"hidden xl:flex absolute inset-y-0 left-16 z-30": true,
"opacity-100 translate-x-0 pointer-events-auto": peeked() && !layout.sidebar.opened(),
"opacity-0 -translate-x-2 pointer-events-none": !peeked() || layout.sidebar.opened(),
"transition-[opacity,transform] motion-reduce:transition-none": true,
"duration-180 ease-out": peeked() && !layout.sidebar.opened(),
"duration-120 ease-in": !peeked() || layout.sidebar.opened(),
}}
onMouseMove={disarm}
onMouseEnter={() => {
disarm()
aim.reset()
}}
onPointerDown={disarm}
onMouseLeave={() => {
arm()
}}
>
<Show when={peek()} keyed>
{(project) => <SidebarPanel project={project} merged={false} />}
</Show>
</div>
<div
classList={{
"hidden xl:block pointer-events-none absolute inset-y-0 right-0 z-25 overflow-hidden": true,
"opacity-100 translate-x-0": peeked() && !layout.sidebar.opened(),
"opacity-0 -translate-x-2": !peeked() || layout.sidebar.opened(),
"transition-[opacity,transform] motion-reduce:transition-none": true,
"duration-180 ease-out": peeked() && !layout.sidebar.opened(),
"duration-120 ease-in": !peeked() || layout.sidebar.opened(),
}}
style={{ left: `calc(4rem + ${Math.max(Math.max(layout.sidebar.width(), 244) - 64, 0)}px)` }}
>
<div class="h-full w-px" style={{ "box-shadow": "var(--shadow-sidebar-overlay)" }} />
</div>
{import.meta.env.DEV && <DebugBar />}
</div>
<Toast.Region />
</div>

View File

@@ -91,7 +91,6 @@ const ProjectTile = (props: {
modal={!props.sidebarHovering()}
onOpenChange={(value) => {
props.setMenu(value)
props.setSuppressHover(value)
if (value) props.setOpen(false)
}}
>
@@ -108,12 +107,6 @@ const ProjectTile = (props: {
!props.selected() && !props.active(),
"bg-surface-base-hover border border-border-weak-base": !props.selected() && props.active(),
}}
onPointerDown={(event) => {
if (!props.overlay()) return
if (event.button !== 2 && !(event.button === 0 && event.ctrlKey)) return
props.setSuppressHover(true)
event.preventDefault()
}}
onMouseEnter={(event: MouseEvent) => {
if (!props.overlay()) return
if (props.suppressHover()) return

View File

@@ -37,6 +37,7 @@ import { createOpenReviewFile, createSizing } from "@/pages/session/helpers"
import { MessageTimeline } from "@/pages/session/message-timeline"
import { type DiffStyle, SessionReviewTab, type SessionReviewTabProps } from "@/pages/session/review-tab"
import { resetSessionModel, syncSessionModel } from "@/pages/session/session-model-helpers"
import { createScrollSpy } from "@/pages/session/scroll-spy"
import { SessionMobileTabs } from "@/pages/session/session-mobile-tabs"
import { SessionSidePanel } from "@/pages/session/session-side-panel"
import { TerminalPanel } from "@/pages/session/terminal-panel"
@@ -121,13 +122,9 @@ function createSessionHistoryWindow(input: SessionHistoryWindowInput) {
return
}
const beforeTop = el.scrollTop
const beforeHeight = el.scrollHeight
fn()
requestAnimationFrame(() => {
const delta = el.scrollHeight - beforeHeight
if (!delta) return
el.scrollTop = beforeTop + delta
})
void el.scrollHeight
el.scrollTop = beforeTop
}
const backfillTurns = () => {
@@ -210,7 +207,7 @@ function createSessionHistoryWindow(input: SessionHistoryWindowInput) {
if (!input.userScrolled()) return
const el = input.scroller()
if (!el) return
if (el.scrollTop >= turnScrollThreshold) return
if (el.scrollHeight - el.clientHeight + el.scrollTop >= turnScrollThreshold) return
const start = turnStart()
if (start > 0) {
@@ -284,7 +281,6 @@ export default function Page() {
const [ui, setUi] = createStore({
git: false,
pendingMessage: undefined as string | undefined,
reviewSnap: false,
scrollGesture: 0,
scroll: {
overflow: false,
@@ -430,12 +426,10 @@ export default function Page() {
createEffect(
on(
() => ({ dir: params.dir, id: params.id }),
(next, prev) => {
if (!prev) return
if (next.dir === prev.dir && next.id === prev.id) return
if (prev.id) sync.session.evict(prev.id, prev.dir)
if (!next.id) resetSessionModel(local)
() => params.id,
(id, prev) => {
if (id || !prev) return
resetSessionModel(local)
},
{ defer: true },
),
@@ -460,21 +454,6 @@ export default function Page() {
return key
}, sessionKey())
let reviewFrame: number | undefined
createComputed((prev) => {
const open = desktopReviewOpen()
if (prev === undefined || prev === open) return open
if (reviewFrame !== undefined) cancelAnimationFrame(reviewFrame)
setUi("reviewSnap", true)
reviewFrame = requestAnimationFrame(() => {
reviewFrame = undefined
setUi("reviewSnap", false)
})
return open
}, desktopReviewOpen())
const turnDiffs = createMemo(() => lastUserMessage()?.summary?.diffs ?? [])
const reviewDiffs = createMemo(() => (store.changes === "session" ? diffs() : turnDiffs()))
@@ -485,49 +464,20 @@ export default function Page() {
return "main"
})
const activeMessage = createMemo(() => {
if (!store.messageId) return lastUserMessage()
const found = visibleUserMessages()?.find((m) => m.id === store.messageId)
return found ?? lastUserMessage()
})
const setActiveMessage = (message: UserMessage | undefined) => {
messageMark = scrollMark
setStore("messageId", message?.id)
}
const anchor = (id: string) => `message-${id}`
const cursor = () => {
const root = scroller
if (!root) return store.messageId
const box = root.getBoundingClientRect()
const line = box.top + 100
const list = [...root.querySelectorAll<HTMLElement>("[data-message-id]")]
.map((el) => {
const id = el.dataset.messageId
if (!id) return
const rect = el.getBoundingClientRect()
return { id, top: rect.top, bottom: rect.bottom }
})
.filter((item): item is { id: string; top: number; bottom: number } => !!item)
const shown = list.filter((item) => item.bottom > box.top && item.top < box.bottom)
const hit = shown.find((item) => item.top <= line && item.bottom >= line)
if (hit) return hit.id
const near = [...shown].sort((a, b) => {
const da = Math.abs(a.top - line)
const db = Math.abs(b.top - line)
if (da !== db) return da - db
return a.top - b.top
})[0]
if (near) return near.id
return list.filter((item) => item.top <= line).at(-1)?.id ?? list[0]?.id ?? store.messageId
}
function navigateMessageByOffset(offset: number) {
const msgs = visibleUserMessages()
if (msgs.length === 0) return
const current = store.messageId && messageMark === scrollMark ? store.messageId : cursor()
const current = store.messageId
const base = current ? msgs.findIndex((m) => m.id === current) : msgs.length
const currentIndex = base === -1 ? msgs.length : base
const targetIndex = currentIndex + offset
@@ -600,8 +550,6 @@ export default function Page() {
let dockHeight = 0
let scroller: HTMLDivElement | undefined
let content: HTMLDivElement | undefined
let scrollMark = 0
let messageMark = 0
const scrollGestureWindowMs = 250
@@ -646,7 +594,6 @@ export default function Page() {
() => {
setStore("messageId", undefined)
setStore("changes", "session")
setUi("pendingMessage", undefined)
},
{ defer: true },
),
@@ -1141,11 +1088,17 @@ export default function Page() {
let scrollStateFrame: number | undefined
let scrollStateTarget: HTMLDivElement | undefined
const scrollSpy = createScrollSpy({
onActive: (id) => {
if (id === store.messageId) return
setStore("messageId", id)
},
})
const updateScrollState = (el: HTMLDivElement) => {
const max = el.scrollHeight - el.clientHeight
const overflow = max > 1
const bottom = !overflow || el.scrollTop >= max - 2
const bottom = !overflow || Math.abs(el.scrollTop) <= 2 || !autoScroll.userScrolled()
if (ui.scroll.overflow === overflow && ui.scroll.bottom === bottom) return
setUi("scroll", { overflow, bottom })
@@ -1168,7 +1121,7 @@ export default function Page() {
const resumeScroll = () => {
setStore("messageId", undefined)
autoScroll.forceScrollToBottom()
autoScroll.smoothScrollToBottom()
clearMessageHash()
const el = scroller
@@ -1188,21 +1141,31 @@ export default function Page() {
),
)
createEffect(
on(
sessionKey,
() => {
scrollSpy.clear()
},
{ defer: true },
),
)
const anchor = (id: string) => `message-${id}`
const setScrollRef = (el: HTMLDivElement | undefined) => {
scroller = el
autoScroll.scrollRef(el)
scrollSpy.setContainer(el)
if (el) scheduleScrollState(el)
}
const markUserScroll = () => {
scrollMark += 1
}
createResizeObserver(
() => content,
() => {
const el = scroller
if (el) scheduleScrollState(el)
scrollSpy.markDirty()
},
)
@@ -1226,15 +1189,14 @@ export default function Page() {
const el = scroller
const delta = next - dockHeight
const stick = el
? !autoScroll.userScrolled() || el.scrollHeight - el.clientHeight - el.scrollTop < 10 + Math.max(0, delta)
: false
const stick = el ? Math.abs(el.scrollTop) < 10 + Math.max(0, delta) : false
dockHeight = next
if (stick) autoScroll.forceScrollToBottom()
if (stick) autoScroll.smoothScrollToBottom()
if (el) scheduleScrollState(el)
scrollSpy.markDirty()
},
)
@@ -1262,7 +1224,7 @@ export default function Page() {
onCleanup(() => {
document.removeEventListener("keydown", handleKeyDown)
if (reviewFrame !== undefined) cancelAnimationFrame(reviewFrame)
scrollSpy.destroy()
if (scrollStateFrame !== undefined) cancelAnimationFrame(scrollStateFrame)
})
@@ -1284,7 +1246,7 @@ export default function Page() {
classList={{
"@container relative shrink-0 flex flex-col min-h-0 h-full bg-background-stronger flex-1 md:flex-none": true,
"transition-[width] duration-[240ms] ease-[cubic-bezier(0.22,1,0.36,1)] will-change-[width] motion-reduce:transition-none":
!size.active() && !ui.reviewSnap,
!size.active(),
}}
style={{
width: sessionPanelWidth(),
@@ -1293,7 +1255,7 @@ export default function Page() {
<div class="flex-1 min-h-0 overflow-hidden">
<Switch>
<Match when={params.id}>
<Show when={lastUserMessage()}>
<Show when={activeMessage()}>
<MessageTimeline
mobileChanges={mobileChanges()}
mobileFallback={reviewContent({
@@ -1313,9 +1275,11 @@ export default function Page() {
onAutoScrollHandleScroll={autoScroll.handleScroll}
onMarkScrollGesture={markScrollGesture}
hasScrollGesture={hasScrollGesture}
onUserScroll={markUserScroll}
isDesktop={isDesktop()}
onScrollSpyScroll={scrollSpy.onScroll}
onTurnBackfillScroll={historyWindow.onScrollerScroll}
onAutoScrollInteraction={autoScroll.handleInteraction}
onPreserveScrollAnchor={autoScroll.preserve}
centered={centered()}
setContentRef={(el) => {
content = el
@@ -1332,6 +1296,8 @@ export default function Page() {
}}
renderedUserMessages={historyWindow.renderedUserMessages()}
anchor={anchor}
onRegisterMessage={scrollSpy.register}
onUnregisterMessage={scrollSpy.unregister}
/>
</Show>
</Match>
@@ -1396,7 +1362,6 @@ export default function Page() {
reviewPanel={reviewPanel}
activeDiff={tree.activeDiff}
focusReviewDiff={focusReviewDiff}
reviewSnap={ui.reviewSnap}
size={size}
/>
</div>

View File

@@ -140,7 +140,7 @@ export function SessionComposerRegion(props: {
<div
classList={{
"w-full px-3 pointer-events-auto": true,
"md:max-w-200 md:mx-auto 2xl:max-w-[1000px]": props.centered,
"md:max-w-[500px] md:mx-auto 2xl:max-w-[700px]": props.centered,
}}
>
<Show when={props.state.questionRequest()} keyed>

View File

@@ -446,9 +446,9 @@ export function FileTabContent(props: { tab: string }) {
)
return (
<Tabs.Content value={props.tab} class="mt-3 relative h-full">
<Tabs.Content value={props.tab} class="mt-3 relative flex h-full min-h-0 flex-col overflow-hidden contain-strict">
<ScrollView
class="h-full"
class="h-full min-h-0 flex-1"
viewportRef={(el: HTMLDivElement) => {
scroll = el
restoreScroll()

View File

@@ -1,28 +1,31 @@
import { For, createEffect, createMemo, on, onCleanup, Show, Index, type JSX } from "solid-js"
import { createStore, produce } from "solid-js/store"
import { useNavigate, useParams } from "@solidjs/router"
import {
For,
Index,
createEffect,
createMemo,
createSignal,
on,
onCleanup,
Show,
startTransition,
type JSX,
} from "solid-js"
import { createStore } from "solid-js/store"
import { useParams } from "@solidjs/router"
import { Button } from "@opencode-ai/ui/button"
import { FileIcon } from "@opencode-ai/ui/file-icon"
import { Icon } from "@opencode-ai/ui/icon"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
import { Dialog } from "@opencode-ai/ui/dialog"
import { InlineInput } from "@opencode-ai/ui/inline-input"
import { Spinner } from "@opencode-ai/ui/spinner"
import { SessionTurn } from "@opencode-ai/ui/session-turn"
import { ScrollView } from "@opencode-ai/ui/scroll-view"
import type { AssistantMessage, Message as MessageType, Part, TextPart, UserMessage } from "@opencode-ai/sdk/v2"
import { showToast } from "@opencode-ai/ui/toast"
import { Binary } from "@opencode-ai/util/binary"
import { getFilename } from "@opencode-ai/util/path"
import { shouldMarkBoundaryGesture, normalizeWheelDelta } from "@/pages/session/message-gesture"
import { SessionContextUsage } from "@/components/session-context-usage"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { useLanguage } from "@/context/language"
import { useSettings } from "@/context/settings"
import { useSDK } from "@/context/sdk"
import { useSync } from "@/context/sync"
import { parseCommentNote, readCommentMetadata } from "@/utils/comment-note"
import { SessionTimelineHeader } from "@/pages/session/session-timeline-header"
type MessageComment = {
path: string
@@ -34,7 +37,9 @@ type MessageComment = {
}
const emptyMessages: MessageType[] = []
const idle = { type: "idle" as const }
const isDefaultSessionTitle = (title?: string) =>
!!title && /^(New session - |Child session - )\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(title)
const messageComments = (parts: Part[]): MessageComment[] =>
parts.flatMap((part) => {
@@ -111,6 +116,8 @@ function createTimelineStaging(input: TimelineStageInput) {
completedSession: "",
count: 0,
})
const [readySession, setReadySession] = createSignal("")
let active = ""
const stagedCount = createMemo(() => {
const total = input.messages().length
@@ -135,23 +142,46 @@ function createTimelineStaging(input: TimelineStageInput) {
cancelAnimationFrame(frame)
frame = undefined
}
const scheduleReady = (sessionKey: string) => {
if (input.sessionKey() !== sessionKey) return
if (readySession() === sessionKey) return
setReadySession(sessionKey)
}
createEffect(
on(
() => [input.sessionKey(), input.turnStart() > 0, input.messages().length] as const,
([sessionKey, isWindowed, total]) => {
const switched = active !== sessionKey
if (switched) {
active = sessionKey
setReadySession("")
}
const staging = state.activeSession === sessionKey && state.completedSession !== sessionKey
const shouldStage = isWindowed && total > input.config.init && state.completedSession !== sessionKey
if (staging && !switched && shouldStage && frame !== undefined) return
cancel()
const shouldStage =
isWindowed &&
total > input.config.init &&
state.completedSession !== sessionKey &&
state.activeSession !== sessionKey
if (shouldStage) setReadySession("")
if (!shouldStage) {
setState({ activeSession: "", count: total })
setState({
activeSession: "",
completedSession: isWindowed ? sessionKey : state.completedSession,
count: total,
})
if (total <= 0) {
setReadySession("")
return
}
if (readySession() !== sessionKey) scheduleReady(sessionKey)
return
}
let count = Math.min(total, input.config.init)
if (staging) count = Math.min(total, Math.max(count, state.count))
setState({ activeSession: sessionKey, count })
const step = () => {
@@ -161,10 +191,11 @@ function createTimelineStaging(input: TimelineStageInput) {
}
const currentTotal = input.messages().length
count = Math.min(currentTotal, count + input.config.batch)
setState("count", count)
startTransition(() => setState("count", count))
if (count >= currentTotal) {
setState({ completedSession: sessionKey, activeSession: "" })
frame = undefined
scheduleReady(sessionKey)
return
}
frame = requestAnimationFrame(step)
@@ -178,9 +209,12 @@ function createTimelineStaging(input: TimelineStageInput) {
const key = input.sessionKey()
return state.activeSession === key && state.completedSession !== key
})
const ready = createMemo(() => readySession() === input.sessionKey())
onCleanup(cancel)
return { messages: stagedUserMessages, isStaging }
onCleanup(() => {
cancel()
})
return { messages: stagedUserMessages, isStaging, ready }
}
export function MessageTimeline(props: {
@@ -193,9 +227,11 @@ export function MessageTimeline(props: {
onAutoScrollHandleScroll: () => void
onMarkScrollGesture: (target?: EventTarget | null) => void
hasScrollGesture: () => boolean
onUserScroll: () => void
isDesktop: boolean
onScrollSpyScroll: () => void
onTurnBackfillScroll: () => void
onAutoScrollInteraction: (event: MouseEvent) => void
onPreserveScrollAnchor: (target: HTMLElement) => void
centered: boolean
setContentRef: (el: HTMLDivElement) => void
turnStart: number
@@ -204,18 +240,25 @@ export function MessageTimeline(props: {
onLoadEarlier: () => void
renderedUserMessages: UserMessage[]
anchor: (id: string) => string
onRegisterMessage: (el: HTMLDivElement, id: string) => void
onUnregisterMessage: (id: string) => void
}) {
let touchGesture: number | undefined
const params = useParams()
const navigate = useNavigate()
const sdk = useSDK()
const sync = useSync()
const settings = useSettings()
const dialog = useDialog()
const language = useLanguage()
const rendered = createMemo(() => props.renderedUserMessages.map((message) => message.id))
const trigger = (target: EventTarget | null) => {
const next =
target instanceof Element
? target.closest('[data-slot="collapsible-trigger"], [data-slot="accordion-trigger"], [data-scroll-preserve]')
: undefined
if (!(next instanceof HTMLElement)) return
return next
}
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
const sessionID = createMemo(() => params.id)
const sessionMessages = createMemo(() => {
@@ -228,62 +271,20 @@ export function MessageTimeline(props: {
(item): item is AssistantMessage => item.role === "assistant" && typeof item.time.completed !== "number",
),
)
const sessionStatus = createMemo(() => {
const id = sessionID()
if (!id) return idle
return sync.data.session_status[id] ?? idle
})
const working = createMemo(() => !!pending() || sessionStatus().type !== "idle")
const [slot, setSlot] = createStore({
open: false,
show: false,
fade: false,
})
let f: number | undefined
const clear = () => {
if (f !== undefined) window.clearTimeout(f)
f = undefined
}
onCleanup(clear)
createEffect(
on(
working,
(on, prev) => {
clear()
if (on) {
setSlot({ open: true, show: true, fade: false })
return
}
if (prev) {
setSlot({ open: false, show: true, fade: true })
f = window.setTimeout(() => setSlot({ show: false, fade: false }), 260)
return
}
setSlot({ open: false, show: false, fade: false })
},
{ defer: true },
),
)
const sessionStatus = createMemo(() => sync.data.session_status[sessionID() ?? ""]?.type ?? "idle")
const activeMessageID = createMemo(() => {
const parentID = pending()?.parentID
if (parentID) {
const messages = sessionMessages()
const result = Binary.search(messages, parentID, (message) => message.id)
const message = result.found ? messages[result.index] : messages.find((item) => item.id === parentID)
if (message && message.role === "user") return message.id
const messages = sessionMessages()
const message = pending()
if (message?.parentID) {
const result = Binary.search(messages, message.parentID, (item) => item.id)
const parent = result.found ? messages[result.index] : messages.find((item) => item.id === message.parentID)
if (parent?.role === "user") return parent.id
}
const status = sessionStatus()
if (status.type !== "idle") {
const messages = sessionMessages()
for (let i = messages.length - 1; i >= 0; i--) {
if (messages[i].role === "user") return messages[i].id
}
if (sessionStatus() === "idle") return undefined
for (let i = messages.length - 1; i >= 0; i--) {
if (messages[i].role === "user") return messages[i].id
}
return undefined
})
const info = createMemo(() => {
@@ -291,9 +292,19 @@ export function MessageTimeline(props: {
if (!id) return
return sync.session.get(id)
})
const titleValue = createMemo(() => info()?.title)
const titleValue = createMemo(() => {
const title = info()?.title
if (!title) return
if (isDefaultSessionTitle(title)) return language.t("command.session.new")
return title
})
const defaultTitle = createMemo(() => isDefaultSessionTitle(info()?.title))
const headerTitle = createMemo(
() => titleValue() ?? (props.renderedUserMessages.length ? language.t("command.session.new") : undefined),
)
const placeholderTitle = createMemo(() => defaultTitle() || (!info()?.title && props.renderedUserMessages.length > 0))
const parentID = createMemo(() => info()?.parentID)
const showHeader = createMemo(() => !!(titleValue() || parentID()))
const showHeader = createMemo(() => !!(headerTitle() || parentID()))
const stageCfg = { init: 1, batch: 3 }
const staging = createTimelineStaging({
sessionKey,
@@ -301,212 +312,7 @@ export function MessageTimeline(props: {
messages: () => props.renderedUserMessages,
config: stageCfg,
})
const [title, setTitle] = createStore({
draft: "",
editing: false,
saving: false,
menuOpen: false,
pendingRename: false,
})
let titleRef: HTMLInputElement | undefined
const errorMessage = (err: unknown) => {
if (err && typeof err === "object" && "data" in err) {
const data = (err as { data?: { message?: string } }).data
if (data?.message) return data.message
}
if (err instanceof Error) return err.message
return language.t("common.requestFailed")
}
createEffect(
on(
sessionKey,
() => setTitle({ draft: "", editing: false, saving: false, menuOpen: false, pendingRename: false }),
{ defer: true },
),
)
const openTitleEditor = () => {
if (!sessionID()) return
setTitle({ editing: true, draft: titleValue() ?? "" })
requestAnimationFrame(() => {
titleRef?.focus()
titleRef?.select()
})
}
const closeTitleEditor = () => {
if (title.saving) return
setTitle({ editing: false, saving: false })
}
const saveTitleEditor = async () => {
const id = sessionID()
if (!id) return
if (title.saving) return
const next = title.draft.trim()
if (!next || next === (titleValue() ?? "")) {
setTitle({ editing: false, saving: false })
return
}
setTitle("saving", true)
await sdk.client.session
.update({ sessionID: id, title: next })
.then(() => {
sync.set(
produce((draft) => {
const index = draft.session.findIndex((s) => s.id === id)
if (index !== -1) draft.session[index].title = next
}),
)
setTitle({ editing: false, saving: false })
})
.catch((err) => {
setTitle("saving", false)
showToast({
title: language.t("common.requestFailed"),
description: errorMessage(err),
})
})
}
const navigateAfterSessionRemoval = (sessionID: string, parentID?: string, nextSessionID?: string) => {
if (params.id !== sessionID) return
if (parentID) {
navigate(`/${params.dir}/session/${parentID}`)
return
}
if (nextSessionID) {
navigate(`/${params.dir}/session/${nextSessionID}`)
return
}
navigate(`/${params.dir}/session`)
}
const archiveSession = async (sessionID: string) => {
const session = sync.session.get(sessionID)
if (!session) return
const sessions = sync.data.session ?? []
const index = sessions.findIndex((s) => s.id === sessionID)
const nextSession = index === -1 ? undefined : (sessions[index + 1] ?? sessions[index - 1])
await sdk.client.session
.update({ sessionID, time: { archived: Date.now() } })
.then(() => {
sync.set(
produce((draft) => {
const index = draft.session.findIndex((s) => s.id === sessionID)
if (index !== -1) draft.session.splice(index, 1)
}),
)
navigateAfterSessionRemoval(sessionID, session.parentID, nextSession?.id)
})
.catch((err) => {
showToast({
title: language.t("common.requestFailed"),
description: errorMessage(err),
})
})
}
const deleteSession = async (sessionID: string) => {
const session = sync.session.get(sessionID)
if (!session) return false
const sessions = (sync.data.session ?? []).filter((s) => !s.parentID && !s.time?.archived)
const index = sessions.findIndex((s) => s.id === sessionID)
const nextSession = index === -1 ? undefined : (sessions[index + 1] ?? sessions[index - 1])
const result = await sdk.client.session
.delete({ sessionID })
.then((x) => x.data)
.catch((err) => {
showToast({
title: language.t("session.delete.failed.title"),
description: errorMessage(err),
})
return false
})
if (!result) return false
sync.set(
produce((draft) => {
const removed = new Set<string>([sessionID])
const byParent = new Map<string, string[]>()
for (const item of draft.session) {
const parentID = item.parentID
if (!parentID) continue
const existing = byParent.get(parentID)
if (existing) {
existing.push(item.id)
continue
}
byParent.set(parentID, [item.id])
}
const stack = [sessionID]
while (stack.length) {
const parentID = stack.pop()
if (!parentID) continue
const children = byParent.get(parentID)
if (!children) continue
for (const child of children) {
if (removed.has(child)) continue
removed.add(child)
stack.push(child)
}
}
draft.session = draft.session.filter((s) => !removed.has(s.id))
}),
)
navigateAfterSessionRemoval(sessionID, session.parentID, nextSession?.id)
return true
}
const navigateParent = () => {
const id = parentID()
if (!id) return
navigate(`/${params.dir}/session/${id}`)
}
function DialogDeleteSession(props: { sessionID: string }) {
const name = createMemo(() => sync.session.get(props.sessionID)?.title ?? language.t("command.session.new"))
const handleDelete = async () => {
await deleteSession(props.sessionID)
dialog.close()
}
return (
<Dialog title={language.t("session.delete.title")} fit>
<div class="flex flex-col gap-4 pl-6 pr-2.5 pb-3">
<div class="flex flex-col gap-1">
<span class="text-14-regular text-text-strong">
{language.t("session.delete.confirm", { name: name() })}
</span>
</div>
<div class="flex justify-end gap-2">
<Button variant="ghost" size="large" onClick={() => dialog.close()}>
{language.t("common.cancel")}
</Button>
<Button variant="primary" size="large" onClick={handleDelete}>
{language.t("session.delete.button")}
</Button>
</div>
</div>
</Dialog>
)
}
const rendered = createMemo(() => staging.messages().map((message) => message.id))
return (
<Show
@@ -530,7 +336,18 @@ export function MessageTimeline(props: {
<Icon name="arrow-down-to-line" />
</button>
</div>
<SessionTimelineHeader
centered={props.centered}
showHeader={showHeader}
sessionKey={sessionKey}
sessionID={sessionID}
parentID={parentID}
titleValue={titleValue}
headerTitle={headerTitle}
placeholderTitle={placeholderTitle}
/>
<ScrollView
reverse
viewportRef={props.setScrollRef}
onWheel={(e) => {
const root = e.currentTarget
@@ -564,166 +381,44 @@ export function MessageTimeline(props: {
touchGesture = undefined
}}
onPointerDown={(e) => {
const next = trigger(e.target)
if (next) props.onPreserveScrollAnchor(next)
if (e.target !== e.currentTarget) return
props.onMarkScrollGesture(e.currentTarget)
}}
onKeyDown={(e) => {
if (e.key !== "Enter" && e.key !== " ") return
const next = trigger(e.target)
if (!next) return
props.onPreserveScrollAnchor(next)
}}
onScroll={(e) => {
props.onScheduleScrollState(e.currentTarget)
props.onTurnBackfillScroll()
if (!props.hasScrollGesture()) return
props.onUserScroll()
props.onAutoScrollHandleScroll()
props.onMarkScrollGesture(e.currentTarget)
if (props.isDesktop) props.onScrollSpyScroll()
}}
onClick={(e) => {
props.onAutoScrollInteraction(e)
}}
onClick={props.onAutoScrollInteraction}
class="relative min-w-0 w-full h-full"
style={{
"--session-title-height": showHeader() ? "40px" : "0px",
"--session-title-height": showHeader() ? "72px" : "0px",
"--sticky-accordion-top": showHeader() ? "48px" : "0px",
}}
>
<div ref={props.setContentRef} class="min-w-0 w-full">
<Show when={showHeader()}>
<div
data-session-title
classList={{
"sticky top-0 z-30 bg-[linear-gradient(to_bottom,var(--background-stronger)_48px,transparent)]": true,
"w-full": true,
"pb-4": true,
"pl-2 pr-3 md:pl-4 md:pr-3": true,
"md:max-w-200 md:mx-auto 2xl:max-w-[1000px]": props.centered,
}}
>
<div class="h-12 w-full flex items-center justify-between gap-2">
<div class="flex items-center gap-1 min-w-0 flex-1 pr-3">
<Show when={parentID()}>
<IconButton
tabIndex={-1}
icon="arrow-left"
variant="ghost"
onClick={navigateParent}
aria-label={language.t("common.goBack")}
/>
</Show>
<div class="flex items-center min-w-0 grow-1">
<div
class="shrink-0 flex items-center justify-center overflow-hidden transition-[width,margin] duration-300 ease-[cubic-bezier(0.22,1,0.36,1)]"
style={{
width: slot.open ? "16px" : "0px",
"margin-right": slot.open ? "8px" : "0px",
}}
aria-hidden="true"
>
<Show when={slot.show}>
<div
class="transition-opacity duration-200 ease-out"
classList={{
"opacity-0": slot.fade,
}}
>
<Spinner class="size-4" style={{ color: "var(--icon-interactive-base)" }} />
</div>
</Show>
</div>
<Show when={titleValue() || title.editing}>
<Show
when={title.editing}
fallback={
<h1
class="text-14-medium text-text-strong truncate grow-1 min-w-0"
onDblClick={openTitleEditor}
>
{titleValue()}
</h1>
}
>
<InlineInput
ref={(el) => {
titleRef = el
}}
value={title.draft}
disabled={title.saving}
class="text-14-medium text-text-strong grow-1 min-w-0 rounded-[6px]"
style={{ "--inline-input-shadow": "var(--shadow-xs-border-select)" }}
onInput={(event) => setTitle("draft", event.currentTarget.value)}
onKeyDown={(event) => {
event.stopPropagation()
if (event.key === "Enter") {
event.preventDefault()
void saveTitleEditor()
return
}
if (event.key === "Escape") {
event.preventDefault()
closeTitleEditor()
}
}}
onBlur={closeTitleEditor}
/>
</Show>
</Show>
</div>
</div>
<Show when={sessionID()}>
{(id) => (
<div class="shrink-0 flex items-center gap-3">
<SessionContextUsage placement="bottom" />
<DropdownMenu
gutter={4}
placement="bottom-end"
open={title.menuOpen}
onOpenChange={(open) => setTitle("menuOpen", open)}
>
<DropdownMenu.Trigger
as={IconButton}
icon="dot-grid"
variant="ghost"
class="size-6 rounded-md data-[expanded]:bg-surface-base-active"
aria-label={language.t("common.moreOptions")}
/>
<DropdownMenu.Portal>
<DropdownMenu.Content
style={{ "min-width": "104px" }}
onCloseAutoFocus={(event) => {
if (!title.pendingRename) return
event.preventDefault()
setTitle("pendingRename", false)
openTitleEditor()
}}
>
<DropdownMenu.Item
onSelect={() => {
setTitle("pendingRename", true)
setTitle("menuOpen", false)
}}
>
<DropdownMenu.ItemLabel>{language.t("common.rename")}</DropdownMenu.ItemLabel>
</DropdownMenu.Item>
<DropdownMenu.Item onSelect={() => void archiveSession(id())}>
<DropdownMenu.ItemLabel>{language.t("common.archive")}</DropdownMenu.ItemLabel>
</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Item
onSelect={() => dialog.show(() => <DialogDeleteSession sessionID={id()} />)}
>
<DropdownMenu.ItemLabel>{language.t("common.delete")}</DropdownMenu.ItemLabel>
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu>
</div>
)}
</Show>
</div>
</div>
</Show>
<div>
<div
ref={props.setContentRef}
role="log"
class="flex flex-col gap-12 items-start justify-start pb-16 transition-[margin]"
class="flex flex-col gap-0 items-start justify-start pb-16 transition-[margin]"
style={{ "padding-top": "var(--session-title-height)" }}
classList={{
"w-full": true,
"md:max-w-200 md:mx-auto 2xl:max-w-[1000px]": props.centered,
"md:max-w-[500px] md:mx-auto 2xl:max-w-[700px]": props.centered,
"mt-0.5": props.centered,
"mt-0": !props.centered,
}}
@@ -745,6 +440,15 @@ export function MessageTimeline(props: {
</Show>
<For each={rendered()}>
{(messageID) => {
// Capture at creation time: animate only messages added after the
// timeline finishes its initial backfill staging, plus the first
// turn while a brand new session is still using its default title.
const isNew =
staging.ready() ||
(defaultTitle() &&
sessionStatus() !== "idle" &&
props.renderedUserMessages.length === 1 &&
messageID === props.renderedUserMessages[0]?.id)
const active = createMemo(() => activeMessageID() === messageID)
const queued = createMemo(() => {
if (active()) return false
@@ -753,16 +457,23 @@ export function MessageTimeline(props: {
return false
})
const comments = createMemo(() => messageComments(sync.data.part[messageID] ?? []), [], {
equals: (a, b) => JSON.stringify(a) === JSON.stringify(b),
equals: (a, b) => {
if (a.length !== b.length) return false
return a.every((x, i) => x.path === b[i].path && x.comment === b[i].comment)
},
})
const commentCount = createMemo(() => comments().length)
return (
<div
id={props.anchor(messageID)}
data-message-id={messageID}
ref={(el) => {
props.onRegisterMessage(el, messageID)
onCleanup(() => props.onUnregisterMessage(messageID))
}}
classList={{
"min-w-0 w-full max-w-full": true,
"md:max-w-200 2xl:max-w-[1000px]": props.centered,
"md:max-w-[500px] 2xl:max-w-[700px]": props.centered,
}}
>
<Show when={commentCount() > 0}>
@@ -806,7 +517,7 @@ export function MessageTimeline(props: {
messageID={messageID}
active={active()}
queued={queued()}
status={active() ? sessionStatus() : undefined}
animate={isNew || active()}
showReasoningSummaries={settings.general.showReasoningSummaries()}
shellToolDefaultOpen={settings.general.shellToolPartsExpanded()}
editToolDefaultOpen={settings.general.editToolPartsExpanded()}

View File

@@ -0,0 +1,127 @@
import { describe, expect, test } from "bun:test"
import { createScrollSpy, pickOffsetId, pickVisibleId } from "./scroll-spy"
const rect = (top: number, height = 80): DOMRect =>
({
x: 0,
y: top,
top,
left: 0,
right: 800,
bottom: top + height,
width: 800,
height,
toJSON: () => ({}),
}) as DOMRect
const setRect = (el: Element, top: number, height = 80) => {
Object.defineProperty(el, "getBoundingClientRect", {
configurable: true,
value: () => rect(top, height),
})
}
describe("pickVisibleId", () => {
test("prefers higher intersection ratio", () => {
const id = pickVisibleId(
[
{ id: "a", ratio: 0.2, top: 100 },
{ id: "b", ratio: 0.8, top: 300 },
],
120,
)
expect(id).toBe("b")
})
test("breaks ratio ties by nearest line", () => {
const id = pickVisibleId(
[
{ id: "a", ratio: 0.5, top: 90 },
{ id: "b", ratio: 0.5, top: 140 },
],
130,
)
expect(id).toBe("b")
})
})
describe("pickOffsetId", () => {
test("uses binary search cutoff", () => {
const id = pickOffsetId(
[
{ id: "a", top: 0 },
{ id: "b", top: 200 },
{ id: "c", top: 400 },
],
350,
)
expect(id).toBe("b")
})
})
describe("createScrollSpy fallback", () => {
test("tracks active id from offsets and dirty refresh", () => {
const active: string[] = []
const root = document.createElement("div") as HTMLDivElement
const one = document.createElement("div")
const two = document.createElement("div")
const three = document.createElement("div")
root.append(one, two, three)
document.body.append(root)
Object.defineProperty(root, "scrollTop", { configurable: true, writable: true, value: 250 })
setRect(root, 0, 800)
setRect(one, -250)
setRect(two, -50)
setRect(three, 150)
const queue: FrameRequestCallback[] = []
const flush = () => {
const run = [...queue]
queue.length = 0
for (const cb of run) cb(0)
}
const spy = createScrollSpy({
onActive: (id) => active.push(id),
raf: (cb) => (queue.push(cb), queue.length),
caf: () => {},
IntersectionObserver: undefined,
ResizeObserver: undefined,
MutationObserver: undefined,
})
spy.setContainer(root)
spy.register(one, "a")
spy.register(two, "b")
spy.register(three, "c")
spy.onScroll()
flush()
expect(spy.getActiveId()).toBe("b")
expect(active.at(-1)).toBe("b")
root.scrollTop = 450
setRect(one, -450)
setRect(two, -250)
setRect(three, -50)
spy.onScroll()
flush()
expect(spy.getActiveId()).toBe("c")
root.scrollTop = 250
setRect(one, -250)
setRect(two, 250)
setRect(three, 150)
spy.markDirty()
spy.onScroll()
flush()
expect(spy.getActiveId()).toBe("a")
spy.destroy()
})
})

View File

@@ -0,0 +1,275 @@
type Visible = {
id: string
ratio: number
top: number
}
type Offset = {
id: string
top: number
}
type Input = {
onActive: (id: string) => void
raf?: (cb: FrameRequestCallback) => number
caf?: (id: number) => void
IntersectionObserver?: typeof globalThis.IntersectionObserver
ResizeObserver?: typeof globalThis.ResizeObserver
MutationObserver?: typeof globalThis.MutationObserver
}
export const pickVisibleId = (list: Visible[], line: number) => {
if (list.length === 0) return
const sorted = [...list].sort((a, b) => {
if (b.ratio !== a.ratio) return b.ratio - a.ratio
const da = Math.abs(a.top - line)
const db = Math.abs(b.top - line)
if (da !== db) return da - db
return a.top - b.top
})
return sorted[0]?.id
}
export const pickOffsetId = (list: Offset[], cutoff: number) => {
if (list.length === 0) return
let lo = 0
let hi = list.length - 1
let out = 0
while (lo <= hi) {
const mid = (lo + hi) >> 1
const top = list[mid]?.top
if (top === undefined) break
if (top <= cutoff) {
out = mid
lo = mid + 1
continue
}
hi = mid - 1
}
return list[out]?.id
}
export const createScrollSpy = (input: Input) => {
const raf = input.raf ?? requestAnimationFrame
const caf = input.caf ?? cancelAnimationFrame
const CtorIO = input.IntersectionObserver ?? globalThis.IntersectionObserver
const CtorRO = input.ResizeObserver ?? globalThis.ResizeObserver
const CtorMO = input.MutationObserver ?? globalThis.MutationObserver
let root: HTMLDivElement | undefined
let io: IntersectionObserver | undefined
let ro: ResizeObserver | undefined
let mo: MutationObserver | undefined
let frame: number | undefined
let active: string | undefined
let dirty = true
const node = new Map<string, HTMLElement>()
const id = new WeakMap<HTMLElement, string>()
const visible = new Map<string, { ratio: number; top: number }>()
let offset: Offset[] = []
const schedule = () => {
if (frame !== undefined) return
frame = raf(() => {
frame = undefined
update()
})
}
const refreshOffset = () => {
const el = root
if (!el) {
offset = []
dirty = false
return
}
const base = el.getBoundingClientRect().top
offset = [...node].map(([next, item]) => ({
id: next,
top: item.getBoundingClientRect().top - base + el.scrollTop,
}))
offset.sort((a, b) => a.top - b.top)
dirty = false
}
const update = () => {
const el = root
if (!el) return
const line = el.getBoundingClientRect().top + 100
const next =
pickVisibleId(
[...visible].map(([k, v]) => ({
id: k,
ratio: v.ratio,
top: v.top,
})),
line,
) ??
(() => {
if (dirty) refreshOffset()
return pickOffsetId(offset, el.scrollTop + 100)
})()
if (!next || next === active) return
active = next
input.onActive(next)
}
const observe = () => {
const el = root
if (!el) return
io?.disconnect()
io = undefined
if (CtorIO) {
try {
io = new CtorIO(
(entries) => {
for (const entry of entries) {
const item = entry.target
if (!(item instanceof HTMLElement)) continue
const key = id.get(item)
if (!key) continue
if (!entry.isIntersecting || entry.intersectionRatio <= 0) {
visible.delete(key)
continue
}
visible.set(key, {
ratio: entry.intersectionRatio,
top: entry.boundingClientRect.top,
})
}
schedule()
},
{
root: el,
threshold: [0, 0.25, 0.5, 0.75, 1],
},
)
} catch {
io = undefined
}
}
if (io) {
for (const item of node.values()) io.observe(item)
}
ro?.disconnect()
ro = undefined
if (CtorRO) {
ro = new CtorRO(() => {
dirty = true
schedule()
})
ro.observe(el)
for (const item of node.values()) ro.observe(item)
}
mo?.disconnect()
mo = undefined
if (CtorMO) {
mo = new CtorMO(() => {
dirty = true
schedule()
})
mo.observe(el, { subtree: true, childList: true, characterData: true })
}
dirty = true
schedule()
}
const setContainer = (el?: HTMLDivElement) => {
if (root === el) return
root = el
visible.clear()
active = undefined
observe()
}
const register = (el: HTMLElement, key: string) => {
const prev = node.get(key)
if (prev && prev !== el) {
io?.unobserve(prev)
ro?.unobserve(prev)
}
node.set(key, el)
id.set(el, key)
if (io) io.observe(el)
if (ro) ro.observe(el)
dirty = true
schedule()
}
const unregister = (key: string) => {
const item = node.get(key)
if (!item) return
io?.unobserve(item)
ro?.unobserve(item)
node.delete(key)
visible.delete(key)
dirty = true
schedule()
}
const markDirty = () => {
dirty = true
schedule()
}
const clear = () => {
for (const item of node.values()) {
io?.unobserve(item)
ro?.unobserve(item)
}
node.clear()
visible.clear()
offset = []
active = undefined
dirty = true
}
const destroy = () => {
if (frame !== undefined) caf(frame)
frame = undefined
clear()
io?.disconnect()
ro?.disconnect()
mo?.disconnect()
io = undefined
ro = undefined
mo = undefined
root = undefined
}
return {
setContainer,
register,
unregister,
onScroll: schedule,
markDirty,
clear,
destroy,
getActiveId: () => active,
}
}

View File

@@ -31,7 +31,6 @@ export function SessionSidePanel(props: {
reviewPanel: () => JSX.Element
activeDiff?: string
focusReviewDiff: (path: string) => void
reviewSnap: boolean
size: Sizing
}) {
const params = useParams()
@@ -229,7 +228,7 @@ export function SessionSidePanel(props: {
classList={{
"pointer-events-none": !open(),
"transition-[width] duration-[240ms] ease-[cubic-bezier(0.22,1,0.36,1)] will-change-[width] motion-reduce:transition-none":
!props.size.active() && !props.reviewSnap,
!props.size.active(),
}}
style={{ width: panelWidth() }}
>

View File

@@ -0,0 +1,522 @@
import { createEffect, createMemo, on, onCleanup, Show } from "solid-js"
import { createStore, produce } from "solid-js/store"
import { useNavigate, useParams } from "@solidjs/router"
import { Button } from "@opencode-ai/ui/button"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
import { Dialog } from "@opencode-ai/ui/dialog"
import { prefersReducedMotion } from "@opencode-ai/ui/hooks"
import { InlineInput } from "@opencode-ai/ui/inline-input"
import { animate, type AnimationPlaybackControls, clearFadeStyles, FAST_SPRING } from "@opencode-ai/ui/motion"
import { showToast } from "@opencode-ai/ui/toast"
import { errorMessage } from "@/pages/layout/helpers"
import { SessionContextUsage } from "@/components/session-context-usage"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { useLanguage } from "@/context/language"
import { useSDK } from "@/context/sdk"
import { useSync } from "@/context/sync"
export function SessionTimelineHeader(props: {
centered: boolean
showHeader: () => boolean
sessionKey: () => string
sessionID: () => string | undefined
parentID: () => string | undefined
titleValue: () => string | undefined
headerTitle: () => string | undefined
placeholderTitle: () => boolean
}) {
const navigate = useNavigate()
const params = useParams()
const sdk = useSDK()
const sync = useSync()
const dialog = useDialog()
const language = useLanguage()
const reduce = prefersReducedMotion
const [title, setTitle] = createStore({
draft: "",
editing: false,
saving: false,
menuOpen: false,
pendingRename: false,
})
const [headerText, setHeaderText] = createStore({
session: props.sessionKey(),
value: props.headerTitle(),
prev: undefined as string | undefined,
muted: props.placeholderTitle(),
prevMuted: false,
})
let headerAnim: AnimationPlaybackControls | undefined
let enterAnim: AnimationPlaybackControls | undefined
let leaveAnim: AnimationPlaybackControls | undefined
let titleRef: HTMLInputElement | undefined
let headerRef: HTMLDivElement | undefined
let enterRef: HTMLSpanElement | undefined
let leaveRef: HTMLSpanElement | undefined
const clearHeaderAnim = () => {
headerAnim?.stop()
headerAnim = undefined
}
const animateHeader = () => {
const el = headerRef
if (!el) return
clearHeaderAnim()
if (!headerText.muted || reduce()) {
el.style.opacity = "1"
return
}
headerAnim = animate(el, { opacity: [0, 1] }, { type: "spring", visualDuration: 1.0, bounce: 0 })
headerAnim.finished.then(() => {
if (headerRef !== el) return
clearFadeStyles(el)
})
}
const clearTitleAnims = () => {
enterAnim?.stop()
enterAnim = undefined
leaveAnim?.stop()
leaveAnim = undefined
}
const settleTitleEnter = () => {
if (enterRef) clearFadeStyles(enterRef)
}
const hideLeave = () => {
if (!leaveRef) return
leaveRef.style.opacity = "0"
leaveRef.style.filter = ""
leaveRef.style.transform = ""
}
const animateEnterSpan = () => {
if (!enterRef) return
if (reduce()) {
settleTitleEnter()
return
}
enterAnim = animate(
enterRef,
{ opacity: [0, 1], filter: ["blur(2px)", "blur(0px)"], transform: ["translateY(-2px)", "translateY(0)"] },
FAST_SPRING,
)
enterAnim.finished.then(() => settleTitleEnter())
}
const crossfadeTitle = (nextTitle: string, nextMuted: boolean) => {
clearTitleAnims()
setHeaderText({ prev: headerText.value, prevMuted: headerText.muted })
setHeaderText({ value: nextTitle, muted: nextMuted })
if (reduce()) {
setHeaderText({ prev: undefined, prevMuted: false })
hideLeave()
settleTitleEnter()
return
}
if (leaveRef) {
leaveAnim = animate(
leaveRef,
{ opacity: [1, 0], filter: ["blur(0px)", "blur(2px)"], transform: ["translateY(0)", "translateY(2px)"] },
FAST_SPRING,
)
leaveAnim.finished.then(() => {
setHeaderText({ prev: undefined, prevMuted: false })
hideLeave()
})
}
animateEnterSpan()
}
const fadeInTitle = (nextTitle: string, nextMuted: boolean) => {
clearTitleAnims()
setHeaderText({ value: nextTitle, muted: nextMuted, prev: undefined, prevMuted: false })
animateEnterSpan()
}
const snapTitle = (nextTitle: string | undefined, nextMuted: boolean) => {
clearTitleAnims()
setHeaderText({ value: nextTitle, muted: nextMuted, prev: undefined, prevMuted: false })
settleTitleEnter()
}
createEffect(
on(props.showHeader, (show, prev) => {
if (!show) {
clearHeaderAnim()
return
}
if (show === prev) return
animateHeader()
}),
)
createEffect(
on(
() => [props.sessionKey(), props.headerTitle(), props.placeholderTitle()] as const,
([nextSession, nextTitle, nextMuted]) => {
if (nextSession !== headerText.session) {
setHeaderText("session", nextSession)
if (nextTitle && nextMuted) {
fadeInTitle(nextTitle, nextMuted)
return
}
snapTitle(nextTitle, nextMuted)
return
}
if (nextTitle === headerText.value && nextMuted === headerText.muted) return
if (!nextTitle) {
snapTitle(undefined, false)
return
}
if (!headerText.value) {
fadeInTitle(nextTitle, nextMuted)
return
}
if (title.saving || title.editing) {
snapTitle(nextTitle, nextMuted)
return
}
crossfadeTitle(nextTitle, nextMuted)
},
),
)
onCleanup(() => {
clearHeaderAnim()
clearTitleAnims()
})
const toastError = (err: unknown) => errorMessage(err, language.t("common.requestFailed"))
createEffect(
on(
props.sessionKey,
() => setTitle({ draft: "", editing: false, saving: false, menuOpen: false, pendingRename: false }),
{ defer: true },
),
)
const openTitleEditor = () => {
if (!props.sessionID()) return
setTitle({ editing: true, draft: props.titleValue() ?? "" })
requestAnimationFrame(() => {
titleRef?.focus()
titleRef?.select()
})
}
const closeTitleEditor = () => {
if (title.saving) return
setTitle({ editing: false, saving: false })
}
const saveTitleEditor = async () => {
const id = props.sessionID()
if (!id) return
if (title.saving) return
const next = title.draft.trim()
if (!next || next === (props.titleValue() ?? "")) {
setTitle({ editing: false, saving: false })
return
}
setTitle("saving", true)
await sdk.client.session
.update({ sessionID: id, title: next })
.then(() => {
sync.set(
produce((draft) => {
const index = draft.session.findIndex((session) => session.id === id)
if (index !== -1) draft.session[index].title = next
}),
)
setTitle({ editing: false, saving: false })
})
.catch((err) => {
setTitle("saving", false)
showToast({
title: language.t("common.requestFailed"),
description: toastError(err),
})
})
}
const navigateAfterSessionRemoval = (sessionID: string, parentID?: string, nextSessionID?: string) => {
if (params.id !== sessionID) return
if (parentID) {
navigate(`/${params.dir}/session/${parentID}`)
return
}
if (nextSessionID) {
navigate(`/${params.dir}/session/${nextSessionID}`)
return
}
navigate(`/${params.dir}/session`)
}
const archiveSession = async (sessionID: string) => {
const session = sync.session.get(sessionID)
if (!session) return
const sessions = sync.data.session ?? []
const index = sessions.findIndex((item) => item.id === sessionID)
const nextSession = index === -1 ? undefined : (sessions[index + 1] ?? sessions[index - 1])
await sdk.client.session
.update({ sessionID, time: { archived: Date.now() } })
.then(() => {
sync.set(
produce((draft) => {
const index = draft.session.findIndex((item) => item.id === sessionID)
if (index !== -1) draft.session.splice(index, 1)
}),
)
navigateAfterSessionRemoval(sessionID, session.parentID, nextSession?.id)
})
.catch((err) => {
showToast({
title: language.t("common.requestFailed"),
description: toastError(err),
})
})
}
const deleteSession = async (sessionID: string) => {
const session = sync.session.get(sessionID)
if (!session) return false
const sessions = (sync.data.session ?? []).filter((item) => !item.parentID && !item.time?.archived)
const index = sessions.findIndex((item) => item.id === sessionID)
const nextSession = index === -1 ? undefined : (sessions[index + 1] ?? sessions[index - 1])
const result = await sdk.client.session
.delete({ sessionID })
.then((x) => x.data)
.catch((err) => {
showToast({
title: language.t("session.delete.failed.title"),
description: toastError(err),
})
return false
})
if (!result) return false
sync.set(
produce((draft) => {
const removed = new Set<string>([sessionID])
const byParent = new Map<string, string[]>()
for (const item of draft.session) {
const parentID = item.parentID
if (!parentID) continue
const existing = byParent.get(parentID)
if (existing) {
existing.push(item.id)
continue
}
byParent.set(parentID, [item.id])
}
const stack = [sessionID]
while (stack.length) {
const parentID = stack.pop()
if (!parentID) continue
const children = byParent.get(parentID)
if (!children) continue
for (const child of children) {
if (removed.has(child)) continue
removed.add(child)
stack.push(child)
}
}
draft.session = draft.session.filter((item) => !removed.has(item.id))
}),
)
navigateAfterSessionRemoval(sessionID, session.parentID, nextSession?.id)
return true
}
const navigateParent = () => {
const id = props.parentID()
if (!id) return
navigate(`/${params.dir}/session/${id}`)
}
function DialogDeleteSession(input: { sessionID: string }) {
const name = createMemo(() => sync.session.get(input.sessionID)?.title ?? language.t("command.session.new"))
const handleDelete = async () => {
await deleteSession(input.sessionID)
dialog.close()
}
return (
<Dialog title={language.t("session.delete.title")} fit>
<div class="flex flex-col gap-4 pl-6 pr-2.5 pb-3">
<div class="flex flex-col gap-1">
<span class="text-14-regular text-text-strong">
{language.t("session.delete.confirm", { name: name() })}
</span>
</div>
<div class="flex justify-end gap-2">
<Button variant="ghost" size="large" onClick={() => dialog.close()}>
{language.t("common.cancel")}
</Button>
<Button variant="primary" size="large" onClick={handleDelete}>
{language.t("session.delete.button")}
</Button>
</div>
</div>
</Dialog>
)
}
return (
<Show when={props.showHeader()}>
<div
data-session-title
ref={(el) => {
headerRef = el
el.style.opacity = "0"
}}
class="pointer-events-none absolute inset-x-0 top-0 z-30"
>
<div
classList={{
"bg-[linear-gradient(to_bottom,var(--background-stronger)_38px,transparent)]": true,
"w-full": true,
"pb-10": true,
"px-4 md:px-5": true,
"md:max-w-[500px] md:mx-auto 2xl:max-w-[700px]": props.centered,
}}
>
<div class="pointer-events-auto h-12 w-full flex items-center justify-between gap-2">
<div class="flex items-center gap-1 min-w-0 flex-1">
<Show when={props.parentID()}>
<div>
<IconButton
tabIndex={-1}
icon="arrow-left"
variant="ghost"
onClick={navigateParent}
aria-label={language.t("common.goBack")}
/>
</div>
</Show>
<Show when={!!headerText.value || title.editing}>
<Show
when={title.editing}
fallback={
<h1 class="text-14-medium text-text-strong grow-1 min-w-0" onDblClick={openTitleEditor}>
<span class="grid min-w-0" style={{ overflow: "clip" }}>
<span ref={enterRef} class="col-start-1 row-start-1 min-w-0 truncate">
<span classList={{ "opacity-60": headerText.muted }}>{headerText.value}</span>
</span>
<span
ref={leaveRef}
class="col-start-1 row-start-1 min-w-0 truncate pointer-events-none"
style={{ opacity: "0" }}
>
<span classList={{ "opacity-60": headerText.prevMuted }}>{headerText.prev}</span>
</span>
</span>
</h1>
}
>
<InlineInput
ref={(el) => {
titleRef = el
}}
value={title.draft}
disabled={title.saving}
class="text-14-medium text-text-strong grow-1 min-w-0 rounded-[6px]"
style={{ "--inline-input-shadow": "var(--shadow-xs-border-select)" }}
onInput={(event) => setTitle("draft", event.currentTarget.value)}
onKeyDown={(event) => {
event.stopPropagation()
if (event.key === "Enter") {
event.preventDefault()
void saveTitleEditor()
return
}
if (event.key === "Escape") {
event.preventDefault()
closeTitleEditor()
}
}}
onBlur={closeTitleEditor}
/>
</Show>
</Show>
</div>
<Show when={props.sessionID()}>
{(id) => (
<div class="shrink-0 flex items-center gap-3">
<SessionContextUsage placement="bottom" />
<DropdownMenu
gutter={4}
placement="bottom-end"
open={title.menuOpen}
onOpenChange={(open) => setTitle("menuOpen", open)}
>
<DropdownMenu.Trigger
as={IconButton}
icon="dot-grid"
variant="ghost"
class="size-6 rounded-md data-[expanded]:bg-surface-base-active"
aria-label={language.t("common.moreOptions")}
/>
<DropdownMenu.Portal>
<DropdownMenu.Content
style={{ "min-width": "104px" }}
onCloseAutoFocus={(event) => {
if (!title.pendingRename) return
event.preventDefault()
setTitle("pendingRename", false)
openTitleEditor()
}}
>
<DropdownMenu.Item
onSelect={() => {
setTitle("pendingRename", true)
setTitle("menuOpen", false)
}}
>
<DropdownMenu.ItemLabel>{language.t("common.rename")}</DropdownMenu.ItemLabel>
</DropdownMenu.Item>
<DropdownMenu.Item onSelect={() => void archiveSession(id())}>
<DropdownMenu.ItemLabel>{language.t("common.archive")}</DropdownMenu.ItemLabel>
</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Item onSelect={() => dialog.show(() => <DialogDeleteSession sessionID={id()} />)}>
<DropdownMenu.ItemLabel>{language.t("common.delete")}</DropdownMenu.ItemLabel>
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu>
</div>
)}
</Show>
</div>
</div>
</div>
</Show>
)
}

View File

@@ -250,7 +250,6 @@ export function TerminalPanel() {
<div id={`terminal-wrapper-${id}`} class="absolute inset-0">
<Terminal
pty={pty()}
onConnect={() => terminal.trim(id)}
onCleanup={terminal.update}
onConnectError={() => terminal.clone(id)}
/>

View File

@@ -1,5 +1,4 @@
import type { UserMessage } from "@opencode-ai/sdk/v2"
import { useLocation, useNavigate } from "@solidjs/router"
import { createEffect, createMemo, onCleanup, onMount } from "solid-js"
import { messageIdFromHash } from "./message-id-from-hash"
@@ -16,7 +15,7 @@ export const useSessionHashScroll = (input: {
setPendingMessage: (value: string | undefined) => void
setActiveMessage: (message: UserMessage | undefined) => void
setTurnStart: (value: number) => void
autoScroll: { pause: () => void; forceScrollToBottom: () => void }
autoScroll: { pause: () => void; snapToBottom: () => void }
scroller: () => HTMLDivElement | undefined
anchor: (id: string) => string
scheduleScrollState: (el: HTMLDivElement) => void
@@ -26,40 +25,14 @@ export const useSessionHashScroll = (input: {
const messageById = createMemo(() => new Map(visibleUserMessages().map((m) => [m.id, m])))
const messageIndex = createMemo(() => new Map(visibleUserMessages().map((m, i) => [m.id, i])))
let pendingKey = ""
let clearing = false
const location = useLocation()
const navigate = useNavigate()
const frames = new Set<number>()
const queue = (fn: () => void) => {
const id = requestAnimationFrame(() => {
frames.delete(id)
fn()
})
frames.add(id)
}
const cancel = () => {
for (const id of frames) cancelAnimationFrame(id)
frames.clear()
}
const clearMessageHash = () => {
cancel()
input.consumePendingMessage(input.sessionKey())
if (input.pendingMessage()) input.setPendingMessage(undefined)
if (!location.hash) return
clearing = true
navigate(location.pathname + location.search, { replace: true })
if (!window.location.hash) return
window.history.replaceState(null, "", window.location.pathname + window.location.search)
}
const updateHash = (id: string) => {
const hash = `#${input.anchor(id)}`
if (location.hash === hash) return
clearing = false
navigate(location.pathname + location.search + hash, {
replace: true,
})
window.history.replaceState(null, "", `${window.location.pathname}${window.location.search}#${input.anchor(id)}`)
}
const scrollToElement = (el: HTMLElement, behavior: ScrollBehavior) => {
@@ -68,51 +41,65 @@ export const useSessionHashScroll = (input: {
const a = el.getBoundingClientRect()
const b = root.getBoundingClientRect()
const sticky = root.querySelector("[data-session-title]")
const inset = sticky instanceof HTMLElement ? sticky.offsetHeight : 0
const top = Math.max(0, a.top - b.top + root.scrollTop - inset)
const title = parseFloat(getComputedStyle(root).getPropertyValue("--session-title-height"))
const inset = Number.isNaN(title) ? 0 : title
// With column-reverse, scrollTop is negative — don't clamp to 0
const top = a.top - b.top + root.scrollTop - inset
root.scrollTo({ top, behavior })
return true
}
const seek = (id: string, behavior: ScrollBehavior, left = 4): boolean => {
const el = document.getElementById(input.anchor(id))
if (el) return scrollToElement(el, behavior)
if (left <= 0) return false
queue(() => {
seek(id, behavior, left - 1)
})
return false
}
const scrollToMessage = (message: UserMessage, behavior: ScrollBehavior = "smooth") => {
cancel()
if (input.currentMessageId() !== message.id) input.setActiveMessage(message)
const index = messageIndex().get(message.id) ?? -1
if (index !== -1 && index < input.turnStart()) {
input.setTurnStart(index)
queue(() => {
seek(message.id, behavior)
requestAnimationFrame(() => {
const el = document.getElementById(input.anchor(message.id))
if (!el) {
requestAnimationFrame(() => {
const next = document.getElementById(input.anchor(message.id))
if (!next) return
scrollToElement(next, behavior)
})
return
}
scrollToElement(el, behavior)
})
updateHash(message.id)
return
}
if (seek(message.id, behavior)) {
const el = document.getElementById(input.anchor(message.id))
if (!el) {
updateHash(message.id)
requestAnimationFrame(() => {
const next = document.getElementById(input.anchor(message.id))
if (!next) return
if (!scrollToElement(next, behavior)) return
})
return
}
if (scrollToElement(el, behavior)) {
updateHash(message.id)
return
}
requestAnimationFrame(() => {
const next = document.getElementById(input.anchor(message.id))
if (!next) return
if (!scrollToElement(next, behavior)) return
})
updateHash(message.id)
}
const applyHash = (behavior: ScrollBehavior) => {
const hash = location.hash.slice(1)
const hash = window.location.hash.slice(1)
if (!hash) {
input.autoScroll.forceScrollToBottom()
input.autoScroll.snapToBottom()
const el = input.scroller()
if (el) input.scheduleScrollState(el)
return
@@ -136,17 +123,28 @@ export const useSessionHashScroll = (input: {
return
}
input.autoScroll.forceScrollToBottom()
input.autoScroll.snapToBottom()
const el = input.scroller()
if (el) input.scheduleScrollState(el)
}
onMount(() => {
if (typeof window !== "undefined" && "scrollRestoration" in window.history) {
window.history.scrollRestoration = "manual"
}
const handler = () => {
if (!input.sessionID() || !input.messagesReady()) return
requestAnimationFrame(() => applyHash("auto"))
}
window.addEventListener("hashchange", handler)
onCleanup(() => window.removeEventListener("hashchange", handler))
})
createEffect(() => {
const hash = location.hash
if (!hash) clearing = false
if (!input.sessionID() || !input.messagesReady()) return
cancel()
queue(() => applyHash("auto"))
requestAnimationFrame(() => applyHash("auto"))
})
createEffect(() => {
@@ -168,29 +166,17 @@ export const useSessionHashScroll = (input: {
}
}
if (!targetId && !clearing) targetId = messageIdFromHash(location.hash)
if (!targetId) return
if (input.currentMessageId() === targetId) return
const pending = input.pendingMessage() === targetId
const msg = messageById().get(targetId)
if (!msg) return
if (pending) input.setPendingMessage(undefined)
if (input.currentMessageId() === targetId && !pending) return
if (input.pendingMessage() === targetId) input.setPendingMessage(undefined)
input.autoScroll.pause()
cancel()
queue(() => scrollToMessage(msg, "auto"))
requestAnimationFrame(() => scrollToMessage(msg, "auto"))
})
onMount(() => {
if (typeof window !== "undefined" && "scrollRestoration" in window.history) {
window.history.scrollRestoration = "manual"
}
})
onCleanup(cancel)
return {
clearMessageHash,
scrollToMessage,

View File

@@ -1,46 +0,0 @@
import { beforeEach, describe, expect, test } from "bun:test"
const src = await Bun.file(new URL("../public/oc-theme-preload.js", import.meta.url)).text()
const run = () => Function(src)()
beforeEach(() => {
document.head.innerHTML = ""
document.documentElement.removeAttribute("data-theme")
document.documentElement.removeAttribute("data-color-scheme")
localStorage.clear()
Object.defineProperty(window, "matchMedia", {
value: () =>
({
matches: false,
}) as MediaQueryList,
configurable: true,
})
})
describe("theme preload", () => {
test("migrates legacy oc-1 to oc-2 before mount", () => {
localStorage.setItem("opencode-theme-id", "oc-1")
localStorage.setItem("opencode-theme-css-light", "--background-base:#fff;")
localStorage.setItem("opencode-theme-css-dark", "--background-base:#000;")
run()
expect(document.documentElement.dataset.theme).toBe("oc-2")
expect(document.documentElement.dataset.colorScheme).toBe("light")
expect(localStorage.getItem("opencode-theme-id")).toBe("oc-2")
expect(localStorage.getItem("opencode-theme-css-light")).toBeNull()
expect(localStorage.getItem("opencode-theme-css-dark")).toBeNull()
expect(document.getElementById("oc-theme-preload")).toBeNull()
})
test("keeps cached css for non-default themes", () => {
localStorage.setItem("opencode-theme-id", "nightowl")
localStorage.setItem("opencode-theme-css-light", "--background-base:#fff;")
run()
expect(document.documentElement.dataset.theme).toBe("nightowl")
expect(document.getElementById("oc-theme-preload")?.textContent).toContain("--background-base:#fff;")
})
})

View File

@@ -104,12 +104,4 @@ describe("persist localStorage resilience", () => {
const result = persistTesting.normalize({ value: "ok" }, '{"value":"\\x"}')
expect(result).toBeUndefined()
})
test("workspace storage sanitizes Windows filename characters", () => {
const result = persistTesting.workspaceStorage("C:\\Users\\foo")
expect(result).toStartWith("opencode.workspace.")
expect(result.endsWith(".dat")).toBeTrue()
expect(/[:\\/]/.test(result)).toBeFalse()
})
})

View File

@@ -204,7 +204,7 @@ function normalize(defaults: unknown, raw: string, migrate?: (value: unknown) =>
}
function workspaceStorage(dir: string) {
const head = (dir.slice(0, 12) || "workspace").replace(/[^a-zA-Z0-9._-]/g, "-")
const head = dir.slice(0, 12) || "workspace"
const sum = checksum(dir) ?? "0"
return `opencode.workspace.${head}.${sum}.dat`
}
@@ -300,7 +300,6 @@ export const PersistTesting = {
localStorageDirect,
localStorageWithPrefix,
normalize,
workspaceStorage,
}
export const Persist = {

View File

@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-app",
"version": "1.2.24",
"version": "1.2.21",
"type": "module",
"license": "MIT",
"scripts": {

View File

@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/console-core",
"version": "1.2.24",
"version": "1.2.21",
"private": true,
"type": "module",
"license": "MIT",

View File

@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-function",
"version": "1.2.24",
"version": "1.2.21",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-mail",
"version": "1.2.24",
"version": "1.2.21",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",

View File

@@ -1,7 +1,7 @@
{
"name": "@opencode-ai/desktop-electron",
"private": true,
"version": "1.2.24",
"version": "1.2.21",
"type": "module",
"license": "MIT",
"homepage": "https://opencode.ai",

View File

@@ -1,7 +1,7 @@
{
"name": "@opencode-ai/desktop",
"private": true,
"version": "1.2.24",
"version": "1.2.21",
"type": "module",
"license": "MIT",
"scripts": {

View File

@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/enterprise",
"version": "1.2.24",
"version": "1.2.21",
"private": true,
"type": "module",
"license": "MIT",

View File

@@ -1,7 +1,7 @@
id = "opencode"
name = "OpenCode"
description = "The open source coding agent."
version = "1.2.24"
version = "1.2.21"
schema_version = 1
authors = ["Anomaly"]
repository = "https://github.com/anomalyco/opencode"
@@ -11,26 +11,26 @@ name = "OpenCode"
icon = "./icons/opencode.svg"
[agent_servers.opencode.targets.darwin-aarch64]
archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.24/opencode-darwin-arm64.zip"
archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.21/opencode-darwin-arm64.zip"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.darwin-x86_64]
archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.24/opencode-darwin-x64.zip"
archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.21/opencode-darwin-x64.zip"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.linux-aarch64]
archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.24/opencode-linux-arm64.tar.gz"
archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.21/opencode-linux-arm64.tar.gz"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.linux-x86_64]
archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.24/opencode-linux-x64.tar.gz"
archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.21/opencode-linux-x64.tar.gz"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.windows-x86_64]
archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.24/opencode-windows-x64.zip"
archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.21/opencode-windows-x64.zip"
cmd = "./opencode.exe"
args = ["acp"]

View File

@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/function",
"version": "1.2.24",
"version": "1.2.21",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "1.2.24",
"version": "1.2.21",
"name": "opencode",
"type": "module",
"license": "MIT",
@@ -41,7 +41,6 @@
"@types/babel__core": "7.20.5",
"@types/bun": "catalog:",
"@types/mime-types": "3.0.1",
"@types/semver": "^7.5.8",
"@types/turndown": "5.0.5",
"@types/yargs": "17.0.33",
"@types/which": "3.0.4",
@@ -91,8 +90,8 @@
"@opencode-ai/sdk": "workspace:*",
"@opencode-ai/util": "workspace:*",
"@openrouter/ai-sdk-provider": "1.5.4",
"@opentui/core": "0.1.87",
"@opentui/solid": "0.1.87",
"@opentui/core": "0.1.86",
"@opentui/solid": "0.1.86",
"@parcel/watcher": "2.5.1",
"@pierre/diffs": "catalog:",
"@solid-primitives/event-bus": "1.1.2",
@@ -122,7 +121,6 @@
"opentui-spinner": "0.0.6",
"partial-json": "0.1.7",
"remeda": "catalog:",
"semver": "^7.6.3",
"solid-js": "catalog:",
"strip-ansi": "7.1.2",
"tree-sitter-bash": "0.25.0",

View File

@@ -29,7 +29,7 @@ import {
} from "@agentclientprotocol/sdk"
import { Log } from "../util/log"
import { pathToFileURL } from "url"
import { pathToFileURL } from "bun"
import { Filesystem } from "../util/filesystem"
import { Hash } from "../util/hash"
import { ACPSessionManager } from "./session"

View File

@@ -1,4 +1,4 @@
import semver from "semver"
import { semver } from "bun"
import { text } from "node:stream/consumers"
import { Log } from "../util/log"
import { Process } from "../util/process"
@@ -45,6 +45,6 @@ export namespace PackageRegistry {
const isRange = /[\s^~*xX<>|=]/.test(cachedVersion)
if (isRange) return !semver.satisfies(latestVersion, cachedVersion)
return semver.lt(cachedVersion, latestVersion)
return semver.order(cachedVersion, latestVersion) === -1
}
}

View File

@@ -27,9 +27,8 @@ import { Provider } from "../../provider/provider"
import { Bus } from "../../bus"
import { MessageV2 } from "../../session/message-v2"
import { SessionPrompt } from "@/session/prompt"
import { $ } from "bun"
import { setTimeout as sleep } from "node:timers/promises"
import { Process } from "@/util/process"
import { git } from "@/util/git"
type GitHubAuthor = {
login: string
@@ -256,7 +255,7 @@ export const GithubInstallCommand = cmd({
}
// Get repo info
const info = (await git(["remote", "get-url", "origin"], { cwd: Instance.worktree })).text().trim()
const info = (await $`git remote get-url origin`.quiet().nothrow().text()).trim()
const parsed = parseGitHubRemote(info)
if (!parsed) {
prompts.log.error(`Could not find git repository. Please run this command from a git repository.`)
@@ -494,26 +493,6 @@ export const GithubRunCommand = cmd({
? "pr_review"
: "issue"
: undefined
const gitText = async (args: string[]) => {
const result = await git(args, { cwd: Instance.worktree })
if (result.exitCode !== 0) {
throw new Process.RunFailedError(["git", ...args], result.exitCode, result.stdout, result.stderr)
}
return result.text().trim()
}
const gitRun = async (args: string[]) => {
const result = await git(args, { cwd: Instance.worktree })
if (result.exitCode !== 0) {
throw new Process.RunFailedError(["git", ...args], result.exitCode, result.stdout, result.stderr)
}
return result
}
const gitStatus = (args: string[]) => git(args, { cwd: Instance.worktree })
const commitChanges = async (summary: string, actor?: string) => {
const args = ["commit", "-m", summary]
if (actor) args.push("-m", `Co-authored-by: ${actor} <${actor}@users.noreply.github.com>`)
await gitRun(args)
}
try {
if (useGithubToken) {
@@ -574,7 +553,7 @@ export const GithubRunCommand = cmd({
}
const branchPrefix = isWorkflowDispatchEvent ? "dispatch" : "schedule"
const branch = await checkoutNewBranch(branchPrefix)
const head = await gitText(["rev-parse", "HEAD"])
const head = (await $`git rev-parse HEAD`).stdout.toString().trim()
const response = await chat(userPrompt, promptFiles)
const { dirty, uncommittedChanges, switched } = await branchIsDirty(head, branch)
if (switched) {
@@ -608,7 +587,7 @@ export const GithubRunCommand = cmd({
// Local PR
if (prData.headRepository.nameWithOwner === prData.baseRepository.nameWithOwner) {
await checkoutLocalBranch(prData)
const head = await gitText(["rev-parse", "HEAD"])
const head = (await $`git rev-parse HEAD`).stdout.toString().trim()
const dataPrompt = buildPromptDataForPR(prData)
const response = await chat(`${userPrompt}\n\n${dataPrompt}`, promptFiles)
const { dirty, uncommittedChanges, switched } = await branchIsDirty(head, prData.headRefName)
@@ -626,7 +605,7 @@ export const GithubRunCommand = cmd({
// Fork PR
else {
const forkBranch = await checkoutForkBranch(prData)
const head = await gitText(["rev-parse", "HEAD"])
const head = (await $`git rev-parse HEAD`).stdout.toString().trim()
const dataPrompt = buildPromptDataForPR(prData)
const response = await chat(`${userPrompt}\n\n${dataPrompt}`, promptFiles)
const { dirty, uncommittedChanges, switched } = await branchIsDirty(head, forkBranch)
@@ -645,7 +624,7 @@ export const GithubRunCommand = cmd({
// Issue
else {
const branch = await checkoutNewBranch("issue")
const head = await gitText(["rev-parse", "HEAD"])
const head = (await $`git rev-parse HEAD`).stdout.toString().trim()
const issueData = await fetchIssue()
const dataPrompt = buildPromptDataForIssue(issueData)
const response = await chat(`${userPrompt}\n\n${dataPrompt}`, promptFiles)
@@ -679,7 +658,7 @@ export const GithubRunCommand = cmd({
exitCode = 1
console.error(e instanceof Error ? e.message : String(e))
let msg = e
if (e instanceof Process.RunFailedError) {
if (e instanceof $.ShellError) {
msg = e.stderr.toString()
} else if (e instanceof Error) {
msg = e.message
@@ -1070,29 +1049,29 @@ export const GithubRunCommand = cmd({
const config = "http.https://github.com/.extraheader"
// actions/checkout@v6 no longer stores credentials in .git/config,
// so this may not exist - use nothrow() to handle gracefully
const ret = await gitStatus(["config", "--local", "--get", config])
const ret = await $`git config --local --get ${config}`.nothrow()
if (ret.exitCode === 0) {
gitConfig = ret.stdout.toString().trim()
await gitRun(["config", "--local", "--unset-all", config])
await $`git config --local --unset-all ${config}`
}
const newCredentials = Buffer.from(`x-access-token:${appToken}`, "utf8").toString("base64")
await gitRun(["config", "--local", config, `AUTHORIZATION: basic ${newCredentials}`])
await gitRun(["config", "--global", "user.name", AGENT_USERNAME])
await gitRun(["config", "--global", "user.email", `${AGENT_USERNAME}@users.noreply.github.com`])
await $`git config --local ${config} "AUTHORIZATION: basic ${newCredentials}"`
await $`git config --global user.name "${AGENT_USERNAME}"`
await $`git config --global user.email "${AGENT_USERNAME}@users.noreply.github.com"`
}
async function restoreGitConfig() {
if (gitConfig === undefined) return
const config = "http.https://github.com/.extraheader"
await gitRun(["config", "--local", config, gitConfig])
await $`git config --local ${config} "${gitConfig}"`
}
async function checkoutNewBranch(type: "issue" | "schedule" | "dispatch") {
console.log("Checking out new branch...")
const branch = generateBranchName(type)
await gitRun(["checkout", "-b", branch])
await $`git checkout -b ${branch}`
return branch
}
@@ -1102,8 +1081,8 @@ export const GithubRunCommand = cmd({
const branch = pr.headRefName
const depth = Math.max(pr.commits.totalCount, 20)
await gitRun(["fetch", "origin", `--depth=${depth}`, branch])
await gitRun(["checkout", branch])
await $`git fetch origin --depth=${depth} ${branch}`
await $`git checkout ${branch}`
}
async function checkoutForkBranch(pr: GitHubPullRequest) {
@@ -1113,9 +1092,9 @@ export const GithubRunCommand = cmd({
const localBranch = generateBranchName("pr")
const depth = Math.max(pr.commits.totalCount, 20)
await gitRun(["remote", "add", "fork", `https://github.com/${pr.headRepository.nameWithOwner}.git`])
await gitRun(["fetch", "fork", `--depth=${depth}`, remoteBranch])
await gitRun(["checkout", "-b", localBranch, `fork/${remoteBranch}`])
await $`git remote add fork https://github.com/${pr.headRepository.nameWithOwner}.git`
await $`git fetch fork --depth=${depth} ${remoteBranch}`
await $`git checkout -b ${localBranch} fork/${remoteBranch}`
return localBranch
}
@@ -1136,23 +1115,28 @@ export const GithubRunCommand = cmd({
async function pushToNewBranch(summary: string, branch: string, commit: boolean, isSchedule: boolean) {
console.log("Pushing to new branch...")
if (commit) {
await gitRun(["add", "."])
await $`git add .`
if (isSchedule) {
await commitChanges(summary)
// No co-author for scheduled events - the schedule is operating as the repo
await $`git commit -m "${summary}"`
} else {
await commitChanges(summary, actor)
await $`git commit -m "${summary}
Co-authored-by: ${actor} <${actor}@users.noreply.github.com>"`
}
}
await gitRun(["push", "-u", "origin", branch])
await $`git push -u origin ${branch}`
}
async function pushToLocalBranch(summary: string, commit: boolean) {
console.log("Pushing to local branch...")
if (commit) {
await gitRun(["add", "."])
await commitChanges(summary, actor)
await $`git add .`
await $`git commit -m "${summary}
Co-authored-by: ${actor} <${actor}@users.noreply.github.com>"`
}
await gitRun(["push"])
await $`git push`
}
async function pushToForkBranch(summary: string, pr: GitHubPullRequest, commit: boolean) {
@@ -1161,28 +1145,30 @@ export const GithubRunCommand = cmd({
const remoteBranch = pr.headRefName
if (commit) {
await gitRun(["add", "."])
await commitChanges(summary, actor)
await $`git add .`
await $`git commit -m "${summary}
Co-authored-by: ${actor} <${actor}@users.noreply.github.com>"`
}
await gitRun(["push", "fork", `HEAD:${remoteBranch}`])
await $`git push fork HEAD:${remoteBranch}`
}
async function branchIsDirty(originalHead: string, expectedBranch: string) {
console.log("Checking if branch is dirty...")
// Detect if the agent switched branches during chat (e.g. created
// its own branch, committed, and possibly pushed/created a PR).
const current = await gitText(["rev-parse", "--abbrev-ref", "HEAD"])
const current = (await $`git rev-parse --abbrev-ref HEAD`).stdout.toString().trim()
if (current !== expectedBranch) {
console.log(`Branch changed during chat: expected ${expectedBranch}, now on ${current}`)
return { dirty: true, uncommittedChanges: false, switched: true }
}
const ret = await gitStatus(["status", "--porcelain"])
const ret = await $`git status --porcelain`
const status = ret.stdout.toString().trim()
if (status.length > 0) {
return { dirty: true, uncommittedChanges: true, switched: false }
}
const head = await gitText(["rev-parse", "HEAD"])
const head = (await $`git rev-parse HEAD`).stdout.toString().trim()
return {
dirty: head !== originalHead,
uncommittedChanges: false,
@@ -1194,11 +1180,11 @@ export const GithubRunCommand = cmd({
// Falls back to fetching from origin when local refs are missing
// (common in shallow clones from actions/checkout).
async function hasNewCommits(base: string, head: string) {
const result = await gitStatus(["rev-list", "--count", `${base}..${head}`])
const result = await $`git rev-list --count ${base}..${head}`.nothrow()
if (result.exitCode !== 0) {
console.log(`rev-list failed, fetching origin/${base}...`)
await gitStatus(["fetch", "origin", base, "--depth=1"])
const retry = await gitStatus(["rev-list", "--count", `origin/${base}..${head}`])
await $`git fetch origin ${base} --depth=1`.nothrow()
const retry = await $`git rev-list --count origin/${base}..${head}`.nothrow()
if (retry.exitCode !== 0) return true // assume dirty if we can't tell
return parseInt(retry.stdout.toString().trim()) > 0
}

View File

@@ -1,8 +1,7 @@
import { UI } from "../ui"
import { cmd } from "./cmd"
import { Instance } from "@/project/instance"
import { Process } from "@/util/process"
import { git } from "@/util/git"
import { $ } from "bun"
export const PrCommand = cmd({
command: "pr <number>",
@@ -28,35 +27,21 @@ export const PrCommand = cmd({
UI.println(`Fetching and checking out PR #${prNumber}...`)
// Use gh pr checkout with custom branch name
const result = await Process.run(
["gh", "pr", "checkout", `${prNumber}`, "--branch", localBranchName, "--force"],
{
nothrow: true,
},
)
const result = await $`gh pr checkout ${prNumber} --branch ${localBranchName} --force`.nothrow()
if (result.code !== 0) {
if (result.exitCode !== 0) {
UI.error(`Failed to checkout PR #${prNumber}. Make sure you have gh CLI installed and authenticated.`)
process.exit(1)
}
// Fetch PR info for fork handling and session link detection
const prInfoResult = await Process.text(
[
"gh",
"pr",
"view",
`${prNumber}`,
"--json",
"headRepository,headRepositoryOwner,isCrossRepository,headRefName,body",
],
{ nothrow: true },
)
const prInfoResult =
await $`gh pr view ${prNumber} --json headRepository,headRepositoryOwner,isCrossRepository,headRefName,body`.nothrow()
let sessionId: string | undefined
if (prInfoResult.code === 0) {
const prInfoText = prInfoResult.text
if (prInfoResult.exitCode === 0) {
const prInfoText = prInfoResult.text()
if (prInfoText.trim()) {
const prInfo = JSON.parse(prInfoText)
@@ -67,19 +52,15 @@ export const PrCommand = cmd({
const remoteName = forkOwner
// Check if remote already exists
const remotes = (await git(["remote"], { cwd: Instance.worktree })).text().trim()
const remotes = (await $`git remote`.nothrow().text()).trim()
if (!remotes.split("\n").includes(remoteName)) {
await git(["remote", "add", remoteName, `https://github.com/${forkOwner}/${forkName}.git`], {
cwd: Instance.worktree,
})
await $`git remote add ${remoteName} https://github.com/${forkOwner}/${forkName}.git`.nothrow()
UI.println(`Added fork remote: ${remoteName}`)
}
// Set upstream to the fork so pushes go there
const headRefName = prInfo.headRefName
await git(["branch", `--set-upstream-to=${remoteName}/${headRefName}`, localBranchName], {
cwd: Instance.worktree,
})
await $`git branch --set-upstream-to=${remoteName}/${headRefName} ${localBranchName}`.nothrow()
}
// Check for opencode session link in PR body
@@ -90,11 +71,9 @@ export const PrCommand = cmd({
UI.println(`Found opencode session: ${sessionUrl}`)
UI.println(`Importing session...`)
const importResult = await Process.text(["opencode", "import", sessionUrl], {
nothrow: true,
})
if (importResult.code === 0) {
const importOutput = importResult.text.trim()
const importResult = await $`opencode import ${sessionUrl}`.nothrow()
if (importResult.exitCode === 0) {
const importOutput = importResult.text().trim()
// Extract session ID from the output (format: "Imported session: <session-id>")
const sessionIdMatch = importOutput.match(/Imported session: ([a-zA-Z0-9_-]+)/)
if (sessionIdMatch) {

View File

@@ -1,6 +1,6 @@
import type { Argv } from "yargs"
import path from "path"
import { pathToFileURL } from "url"
import { pathToFileURL } from "bun"
import { UI } from "../ui"
import { cmd } from "./cmd"
import { Flag } from "../../flag/flag"
@@ -280,11 +280,6 @@ export const RunCommand = cmd({
type: "string",
describe: "attach to a running opencode server (e.g., http://localhost:4096)",
})
.option("password", {
alias: ["p"],
type: "string",
describe: "basic auth password (defaults to OPENCODE_SERVER_PASSWORD)",
})
.option("dir", {
type: "string",
describe: "directory to run in, path on remote server if attaching",
@@ -653,21 +648,14 @@ export const RunCommand = cmd({
}
if (args.attach) {
const headers = (() => {
const password = args.password ?? process.env.OPENCODE_SERVER_PASSWORD
if (!password) return undefined
const username = process.env.OPENCODE_SERVER_USERNAME ?? "opencode"
const auth = `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}`
return { Authorization: auth }
})()
const sdk = createOpencodeClient({ baseUrl: args.attach, directory, headers })
const sdk = createOpencodeClient({ baseUrl: args.attach, directory })
return await execute(sdk)
}
await bootstrap(process.cwd(), async () => {
const fetchFn = (async (input: RequestInfo | URL, init?: RequestInit) => {
const request = new Request(input, init)
return Server.Default().fetch(request)
return Server.App().fetch(request)
}) as typeof globalThis.fetch
const sdk = createOpencodeClient({ baseUrl: "http://opencode.internal", fetch: fetchFn })
await execute(sdk)

View File

@@ -20,7 +20,6 @@ import { DialogHelp } from "./ui/dialog-help"
import { CommandProvider, useCommandDialog } from "@tui/component/dialog-command"
import { DialogAgent } from "@tui/component/dialog-agent"
import { DialogSessionList } from "@tui/component/dialog-session-list"
import { DialogWorkspaceList } from "@tui/component/dialog-workspace-list"
import { KeybindProvider } from "@tui/context/keybind"
import { ThemeProvider, useTheme } from "@tui/context/theme"
import { Home } from "@tui/routes/home"
@@ -112,6 +111,7 @@ export function tui(input: {
fetch?: typeof fetch
headers?: RequestInit["headers"]
events?: EventSource
onExit?: () => Promise<void>
}) {
// promise to prevent immediate exit
return new Promise<void>(async (resolve) => {
@@ -126,6 +126,7 @@ export function tui(input: {
const onExit = async () => {
unguard?.()
await input.onExit?.()
resolve()
}
@@ -372,22 +373,6 @@ function App() {
dialog.replace(() => <DialogSessionList />)
},
},
...(Flag.OPENCODE_EXPERIMENTAL_WORKSPACES
? [
{
title: "Manage workspaces",
value: "workspace.list",
category: "Workspace",
suggested: true,
slash: {
name: "workspaces",
},
onSelect: () => {
dialog.replace(() => <DialogWorkspaceList />)
},
},
]
: []),
{
title: "New session",
suggested: route.data.type === "session",
@@ -402,12 +387,9 @@ function App() {
const current = promptRef.current
// Don't require focus - if there's any text, preserve it
const currentPrompt = current?.current?.input ? current.current : undefined
const workspaceID =
route.data.type === "session" ? sync.session.get(route.data.sessionID)?.workspaceID : undefined
route.navigate({
type: "home",
initialPrompt: currentPrompt,
workspaceID,
})
dialog.clear()
},

View File

@@ -1,326 +0,0 @@
import { useDialog } from "@tui/ui/dialog"
import { DialogSelect } from "@tui/ui/dialog-select"
import { useRoute } from "@tui/context/route"
import { useSync } from "@tui/context/sync"
import { createEffect, createMemo, createSignal, onMount } from "solid-js"
import type { Session } from "@opencode-ai/sdk/v2"
import { useSDK } from "../context/sdk"
import { useToast } from "../ui/toast"
import { useKeybind } from "../context/keybind"
import { DialogSessionList } from "./workspace/dialog-session-list"
import { createOpencodeClient } from "@opencode-ai/sdk/v2"
async function openWorkspace(input: {
dialog: ReturnType<typeof useDialog>
route: ReturnType<typeof useRoute>
sdk: ReturnType<typeof useSDK>
sync: ReturnType<typeof useSync>
toast: ReturnType<typeof useToast>
workspaceID: string
forceCreate?: boolean
}) {
const cacheSession = (session: Session) => {
input.sync.set(
"session",
[...input.sync.data.session.filter((item) => item.id !== session.id), session].toSorted((a, b) =>
a.id.localeCompare(b.id),
),
)
}
const client = createOpencodeClient({
baseUrl: input.sdk.url,
fetch: input.sdk.fetch,
directory: input.sync.data.path.directory || input.sdk.directory,
experimental_workspaceID: input.workspaceID,
})
const listed = input.forceCreate ? undefined : await client.session.list({ roots: true, limit: 1 })
const session = listed?.data?.[0]
if (session?.id) {
cacheSession(session)
input.route.navigate({
type: "session",
sessionID: session.id,
})
input.dialog.clear()
return
}
let created: Session | undefined
while (!created) {
const result = await client.session.create({ workspaceID: input.workspaceID }).catch(() => undefined)
if (!result) {
input.toast.show({
message: "Failed to open workspace",
variant: "error",
})
return
}
if (result.response.status >= 500 && result.response.status < 600) {
await Bun.sleep(1000)
continue
}
if (!result.data) {
input.toast.show({
message: "Failed to open workspace",
variant: "error",
})
return
}
created = result.data
}
cacheSession(created)
input.route.navigate({
type: "session",
sessionID: created.id,
})
input.dialog.clear()
}
function DialogWorkspaceCreate(props: { onSelect: (workspaceID: string) => Promise<void> }) {
const dialog = useDialog()
const sync = useSync()
const sdk = useSDK()
const toast = useToast()
const [creating, setCreating] = createSignal<string>()
onMount(() => {
dialog.setSize("medium")
})
const options = createMemo(() => {
const type = creating()
if (type) {
return [
{
title: `Creating ${type} workspace...`,
value: "creating" as const,
description: "This can take a while for remote environments",
},
]
}
return [
{
title: "Worktree",
value: "worktree" as const,
description: "Create a local git worktree",
},
]
})
const createWorkspace = async (type: string) => {
if (creating()) return
setCreating(type)
const result = await sdk.client.experimental.workspace.create({ type, branch: null }).catch((err) => {
console.log(err)
return undefined
})
console.log(JSON.stringify(result, null, 2))
const workspace = result?.data
if (!workspace) {
setCreating(undefined)
toast.show({
message: "Failed to create workspace",
variant: "error",
})
return
}
await sync.workspace.sync()
await props.onSelect(workspace.id)
setCreating(undefined)
}
return (
<DialogSelect
title={creating() ? "Creating Workspace" : "New Workspace"}
skipFilter={true}
options={options()}
onSelect={(option) => {
if (option.value === "creating") return
void createWorkspace(option.value)
}}
/>
)
}
export function DialogWorkspaceList() {
const dialog = useDialog()
const route = useRoute()
const sync = useSync()
const sdk = useSDK()
const toast = useToast()
const keybind = useKeybind()
const [toDelete, setToDelete] = createSignal<string>()
const [counts, setCounts] = createSignal<Record<string, number | null | undefined>>({})
const open = (workspaceID: string, forceCreate?: boolean) =>
openWorkspace({
dialog,
route,
sdk,
sync,
toast,
workspaceID,
forceCreate,
})
async function selectWorkspace(workspaceID: string) {
if (workspaceID === "__local__") {
if (localCount() > 0) {
dialog.replace(() => <DialogSessionList localOnly={true} />)
return
}
route.navigate({
type: "home",
})
dialog.clear()
return
}
const count = counts()[workspaceID]
if (count && count > 0) {
dialog.replace(() => <DialogSessionList workspaceID={workspaceID} />)
return
}
if (count === 0) {
await open(workspaceID)
return
}
const client = createOpencodeClient({
baseUrl: sdk.url,
fetch: sdk.fetch,
directory: sync.data.path.directory || sdk.directory,
experimental_workspaceID: workspaceID,
})
const listed = await client.session.list({ roots: true, limit: 1 }).catch(() => undefined)
if (listed?.data?.length) {
dialog.replace(() => <DialogSessionList workspaceID={workspaceID} />)
return
}
await open(workspaceID)
}
const currentWorkspaceID = createMemo(() => {
if (route.data.type === "session") {
return sync.session.get(route.data.sessionID)?.workspaceID ?? "__local__"
}
return "__local__"
})
const localCount = createMemo(
() => sync.data.session.filter((session) => !session.workspaceID && !session.parentID).length,
)
let run = 0
createEffect(() => {
const workspaces = sync.data.workspaceList
const next = ++run
if (!workspaces.length) {
setCounts({})
return
}
setCounts(Object.fromEntries(workspaces.map((workspace) => [workspace.id, undefined])))
void Promise.all(
workspaces.map(async (workspace) => {
const client = createOpencodeClient({
baseUrl: sdk.url,
fetch: sdk.fetch,
directory: sync.data.path.directory || sdk.directory,
experimental_workspaceID: workspace.id,
})
const result = await client.session.list({ roots: true }).catch(() => undefined)
return [workspace.id, result ? (result.data?.length ?? 0) : null] as const
}),
).then((entries) => {
if (run !== next) return
setCounts(Object.fromEntries(entries))
})
})
const options = createMemo(() => [
{
title: "Local",
value: "__local__",
category: "Workspace",
description: "Use the local machine",
footer: `${localCount()} session${localCount() === 1 ? "" : "s"}`,
},
...sync.data.workspaceList.map((workspace) => {
const count = counts()[workspace.id]
return {
title:
toDelete() === workspace.id
? `Delete ${workspace.id}? Press ${keybind.print("session_delete")} again`
: workspace.id,
value: workspace.id,
category: workspace.type,
description: workspace.branch ? `Branch ${workspace.branch}` : undefined,
footer:
count === undefined
? "Loading sessions..."
: count === null
? "Sessions unavailable"
: `${count} session${count === 1 ? "" : "s"}`,
}
}),
{
title: "+ New workspace",
value: "__create__",
category: "Actions",
description: "Create a new workspace",
},
])
onMount(() => {
dialog.setSize("large")
void sync.workspace.sync()
})
return (
<DialogSelect
title="Workspaces"
skipFilter={true}
options={options()}
current={currentWorkspaceID()}
onMove={() => {
setToDelete(undefined)
}}
onSelect={(option) => {
setToDelete(undefined)
if (option.value === "__create__") {
dialog.replace(() => <DialogWorkspaceCreate onSelect={(workspaceID) => open(workspaceID, true)} />)
return
}
void selectWorkspace(option.value)
}}
keybind={[
{
keybind: keybind.all.session_delete?.[0],
title: "delete",
onTrigger: async (option) => {
if (option.value === "__create__" || option.value === "__local__") return
if (toDelete() !== option.value) {
setToDelete(option.value)
return
}
const result = await sdk.client.experimental.workspace.remove({ id: option.value }).catch(() => undefined)
setToDelete(undefined)
if (result?.error) {
toast.show({
message: "Failed to delete workspace",
variant: "error",
})
return
}
if (currentWorkspaceID() === option.value) {
route.navigate({
type: "home",
})
}
await sync.workspace.sync()
},
},
]}
/>
)
}

View File

@@ -37,7 +37,6 @@ import { DialogSkill } from "../dialog-skill"
export type PromptProps = {
sessionID?: string
workspaceID?: string
visible?: boolean
disabled?: boolean
onSubmit?: () => void
@@ -540,27 +539,12 @@ export function Prompt(props: PromptProps) {
promptModelWarning()
return
}
let sessionID = props.sessionID
if (sessionID == null) {
const res = await sdk.client.session.create({
workspaceID: props.workspaceID,
})
if (res.error) {
console.log("Creating a session failed:", res.error)
toast.show({
message: "Creating a session failed. Open console for more details.",
variant: "error",
})
return
}
sessionID = res.data.id
}
const sessionID = props.sessionID
? props.sessionID
: await (async () => {
const sessionID = await sdk.client.session.create({}).then((x) => x.data!.id)
return sessionID
})()
const messageID = Identifier.ascending("message")
let inputText = store.prompt.input

View File

@@ -1,151 +0,0 @@
import { useDialog } from "@tui/ui/dialog"
import { DialogSelect } from "@tui/ui/dialog-select"
import { useRoute } from "@tui/context/route"
import { useSync } from "@tui/context/sync"
import { createMemo, createSignal, createResource, onMount, Show } from "solid-js"
import { Locale } from "@/util/locale"
import { useKeybind } from "../../context/keybind"
import { useTheme } from "../../context/theme"
import { useSDK } from "../../context/sdk"
import { DialogSessionRename } from "../dialog-session-rename"
import { useKV } from "../../context/kv"
import { createDebouncedSignal } from "../../util/signal"
import { Spinner } from "../spinner"
import { useToast } from "../../ui/toast"
export function DialogSessionList(props: { workspaceID?: string; localOnly?: boolean } = {}) {
const dialog = useDialog()
const route = useRoute()
const sync = useSync()
const keybind = useKeybind()
const { theme } = useTheme()
const sdk = useSDK()
const kv = useKV()
const toast = useToast()
const [toDelete, setToDelete] = createSignal<string>()
const [search, setSearch] = createDebouncedSignal("", 150)
const [listed, listedActions] = createResource(
() => props.workspaceID,
async (workspaceID) => {
if (!workspaceID) return undefined
const result = await sdk.client.session.list({ roots: true })
return result.data ?? []
},
)
const [searchResults] = createResource(search, async (query) => {
if (!query || props.localOnly) return undefined
const result = await sdk.client.session.list({
search: query,
limit: 30,
...(props.workspaceID ? { roots: true } : {}),
})
return result.data ?? []
})
const currentSessionID = createMemo(() => (route.data.type === "session" ? route.data.sessionID : undefined))
const sessions = createMemo(() => {
if (searchResults()) return searchResults()!
if (props.workspaceID) return listed() ?? []
if (props.localOnly) return sync.data.session.filter((session) => !session.workspaceID)
return sync.data.session
})
const options = createMemo(() => {
const today = new Date().toDateString()
return sessions()
.filter((x) => {
if (x.parentID !== undefined) return false
if (props.workspaceID && listed()) return true
if (props.workspaceID) return x.workspaceID === props.workspaceID
if (props.localOnly) return !x.workspaceID
return true
})
.toSorted((a, b) => b.time.updated - a.time.updated)
.map((x) => {
const date = new Date(x.time.updated)
let category = date.toDateString()
if (category === today) {
category = "Today"
}
const isDeleting = toDelete() === x.id
const status = sync.data.session_status?.[x.id]
const isWorking = status?.type === "busy"
return {
title: isDeleting ? `Press ${keybind.print("session_delete")} again to confirm` : x.title,
bg: isDeleting ? theme.error : undefined,
value: x.id,
category,
footer: Locale.time(x.time.updated),
gutter: isWorking ? <Spinner /> : undefined,
}
})
})
onMount(() => {
dialog.setSize("large")
})
return (
<DialogSelect
title={props.workspaceID ? `Workspace Sessions` : props.localOnly ? "Local Sessions" : "Sessions"}
options={options()}
skipFilter={!props.localOnly}
current={currentSessionID()}
onFilter={setSearch}
onMove={() => {
setToDelete(undefined)
}}
onSelect={(option) => {
route.navigate({
type: "session",
sessionID: option.value,
})
dialog.clear()
}}
keybind={[
{
keybind: keybind.all.session_delete?.[0],
title: "delete",
onTrigger: async (option) => {
if (toDelete() === option.value) {
const deleted = await sdk.client.session
.delete({
sessionID: option.value,
})
.then(() => true)
.catch(() => false)
setToDelete(undefined)
if (!deleted) {
toast.show({
message: "Failed to delete session",
variant: "error",
})
return
}
if (props.workspaceID) {
listedActions.mutate((sessions) => sessions?.filter((session) => session.id !== option.value))
return
}
sync.set(
"session",
sync.data.session.filter((session) => session.id !== option.value),
)
return
}
setToDelete(option.value)
},
},
{
keybind: keybind.all.session_rename?.[0],
title: "rename",
onTrigger: async (option) => {
dialog.replace(() => <DialogSessionRename session={option.value} />)
},
},
]}
/>
)
}

View File

@@ -15,7 +15,6 @@ export const { use: useExit, provider: ExitProvider } = createSimpleContext({
init: (input: { onExit?: () => Promise<void> }) => {
const renderer = useRenderer()
let message: string | undefined
let task: Promise<void> | undefined
const store = {
set: (value?: string) => {
const prev = message
@@ -30,24 +29,20 @@ export const { use: useExit, provider: ExitProvider } = createSimpleContext({
get: () => message,
}
const exit: Exit = Object.assign(
(reason?: unknown) => {
if (task) return task
task = (async () => {
// Reset window title before destroying renderer
renderer.setTerminalTitle("")
renderer.destroy()
win32FlushInputBuffer()
if (reason) {
const formatted = FormatError(reason) ?? FormatUnknownError(reason)
if (formatted) {
process.stderr.write(formatted + "\n")
}
async (reason?: unknown) => {
// Reset window title before destroying renderer
renderer.setTerminalTitle("")
renderer.destroy()
win32FlushInputBuffer()
if (reason) {
const formatted = FormatError(reason) ?? FormatUnknownError(reason)
if (formatted) {
process.stderr.write(formatted + "\n")
}
const text = store.get()
if (text) process.stdout.write(text + "\n")
await input.onExit?.()
})()
return task
}
const text = store.get()
if (text) process.stdout.write(text + "\n")
await input.onExit?.()
},
{
message: store,

View File

@@ -5,7 +5,6 @@ import type { PromptInfo } from "../component/prompt/history"
export type HomeRoute = {
type: "home"
initialPrompt?: PromptInfo
workspaceID?: string
}
export type SessionRoute = {

View File

@@ -5,7 +5,6 @@ import { batch, onCleanup, onMount } from "solid-js"
export type EventSource = {
on: (handler: (event: Event) => void) => () => void
setWorkspace?: (workspaceID?: string) => void
}
export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
@@ -18,21 +17,13 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
events?: EventSource
}) => {
const abort = new AbortController()
let workspaceID: string | undefined
let sse: AbortController | undefined
function createSDK() {
return createOpencodeClient({
baseUrl: props.url,
signal: abort.signal,
directory: props.directory,
fetch: props.fetch,
headers: props.headers,
experimental_workspaceID: workspaceID,
})
}
let sdk = createSDK()
const sdk = createOpencodeClient({
baseUrl: props.url,
signal: abort.signal,
directory: props.directory,
fetch: props.fetch,
headers: props.headers,
})
const emitter = createGlobalEmitter<{
[key in Event["type"]]: Extract<Event, { type: key }>
@@ -70,56 +61,41 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
flush()
}
function startSSE() {
sse?.abort()
const ctrl = new AbortController()
sse = ctrl
;(async () => {
while (true) {
if (abort.signal.aborted || ctrl.signal.aborted) break
const events = await sdk.event.subscribe({}, { signal: ctrl.signal })
for await (const event of events.stream) {
if (ctrl.signal.aborted) break
handleEvent(event)
}
if (timer) clearTimeout(timer)
if (queue.length > 0) flush()
}
})().catch(() => {})
}
onMount(() => {
onMount(async () => {
// If an event source is provided, use it instead of SSE
if (props.events) {
const unsub = props.events.on(handleEvent)
onCleanup(unsub)
} else {
startSSE()
return
}
// Fall back to SSE
while (true) {
if (abort.signal.aborted) break
const events = await sdk.event.subscribe(
{},
{
signal: abort.signal,
},
)
for await (const event of events.stream) {
handleEvent(event)
}
// Flush any remaining events
if (timer) clearTimeout(timer)
if (queue.length > 0) {
flush()
}
}
})
onCleanup(() => {
abort.abort()
sse?.abort()
if (timer) clearTimeout(timer)
})
return {
get client() {
return sdk
},
directory: props.directory,
event: emitter,
fetch: props.fetch ?? fetch,
setWorkspace(next?: string) {
if (workspaceID === next) return
workspaceID = next
sdk = createSDK()
props.events?.setWorkspace?.(next)
if (!props.events) startSSE()
},
url: props.url,
}
return { client: sdk, event: emitter, url: props.url }
},
})

View File

@@ -28,7 +28,6 @@ import { useArgs } from "./args"
import { batch, onMount } from "solid-js"
import { Log } from "@/util/log"
import type { Path } from "@opencode-ai/sdk"
import type { Workspace } from "@opencode-ai/sdk/v2"
export const { use: useSync, provider: SyncProvider } = createSimpleContext({
name: "Sync",
@@ -74,7 +73,6 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
formatter: FormatterStatus[]
vcs: VcsInfo | undefined
path: Path
workspaceList: Workspace[]
}>({
provider_next: {
all: [],
@@ -102,17 +100,10 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
formatter: [],
vcs: undefined,
path: { state: "", config: "", worktree: "", directory: "" },
workspaceList: [],
})
const sdk = useSDK()
async function syncWorkspaces() {
const result = await sdk.client.experimental.workspace.list().catch(() => undefined)
if (!result?.data) return
setStore("workspaceList", reconcile(result.data))
}
sdk.event.listen((e) => {
const event = e.details
switch (event.type) {
@@ -422,7 +413,6 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
sdk.client.provider.auth().then((x) => setStore("provider_auth", reconcile(x.data ?? {}))),
sdk.client.vcs.get().then((x) => setStore("vcs", reconcile(x.data))),
sdk.client.path.get().then((x) => setStore("path", reconcile(x.data!))),
syncWorkspaces(),
]).then(() => {
setStore("status", "complete")
})
@@ -491,12 +481,6 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
fullSyncedSessions.add(sessionID)
},
},
workspace: {
get(workspaceID: string) {
return store.workspaceList.find((workspace) => workspace.id === workspaceID)
},
sync: syncWorkspaces,
},
bootstrap,
}
return result

View File

@@ -1,5 +1,5 @@
import { Prompt, type PromptRef } from "@tui/component/prompt"
import { createEffect, createMemo, Match, on, onMount, Show, Switch } from "solid-js"
import { createMemo, Match, onMount, Show, Switch } from "solid-js"
import { useTheme } from "@tui/context/theme"
import { useKeybind } from "@tui/context/keybind"
import { Logo } from "../component/logo"
@@ -14,7 +14,6 @@ import { usePromptRef } from "../context/prompt"
import { Installation } from "@/installation"
import { useKV } from "../context/kv"
import { useCommandDialog } from "../component/dialog-command"
import { useLocal } from "../context/local"
// TODO: what is the best way to do this?
let once = false
@@ -77,7 +76,6 @@ export function Home() {
let prompt: PromptRef
const args = useArgs()
const local = useLocal()
onMount(() => {
if (once) return
if (route.initialPrompt) {
@@ -86,21 +84,9 @@ export function Home() {
} else if (args.prompt) {
prompt.set({ input: args.prompt, parts: [] })
once = true
prompt.submit()
}
})
// Wait for sync and model store to be ready before auto-submitting --prompt
createEffect(
on(
() => sync.ready && local.model.ready,
(ready) => {
if (!ready) return
if (!args.prompt) return
if (prompt.current?.input !== args.prompt) return
prompt.submit()
},
),
)
const directory = useDirectory()
const keybind = useKeybind()
@@ -121,7 +107,6 @@ export function Home() {
promptRef.set(r)
}}
hint={Hint}
workspaceID={route.workspaceID}
/>
</box>
<box height={4} minHeight={0} width="100%" maxWidth={75} alignItems="center" paddingTop={3} flexShrink={1}>

View File

@@ -7,7 +7,6 @@ import { SplitBorder } from "@tui/component/border"
import type { AssistantMessage, Session } from "@opencode-ai/sdk/v2"
import { useCommandDialog } from "@tui/component/dialog-command"
import { useKeybind } from "../../context/keybind"
import { Flag } from "@/flag/flag"
import { useTerminalDimensions } from "@opentui/solid"
const Title = (props: { session: Accessor<Session> }) => {
@@ -30,17 +29,6 @@ const ContextInfo = (props: { context: Accessor<string | undefined>; cost: Acces
)
}
const WorkspaceInfo = (props: { workspace: Accessor<string | undefined> }) => {
const { theme } = useTheme()
return (
<Show when={props.workspace()}>
<text fg={theme.textMuted} wrapMode="none" flexShrink={0}>
{props.workspace()}
</text>
</Show>
)
}
export function Header() {
const route = useRouteData("session")
const sync = useSync()
@@ -71,14 +59,6 @@ export function Header() {
return result
})
const workspace = createMemo(() => {
const id = session()?.workspaceID
if (!id) return "Workspace local"
const info = sync.workspace.get(id)
if (!info) return `Workspace ${id}`
return `Workspace ${id} (${info.type})`
})
const { theme } = useTheme()
const keybind = useKeybind()
const command = useCommandDialog()
@@ -103,19 +83,9 @@ export function Header() {
<Match when={session()?.parentID}>
<box flexDirection="column" gap={1}>
<box flexDirection={narrow() ? "column" : "row"} justifyContent="space-between" gap={narrow() ? 1 : 0}>
{Flag.OPENCODE_EXPERIMENTAL_WORKSPACES ? (
<box flexDirection="column">
<text fg={theme.text}>
<b>Subagent session</b>
</text>
<WorkspaceInfo workspace={workspace} />
</box>
) : (
<text fg={theme.text}>
<b>Subagent session</b>
</text>
)}
<text fg={theme.text}>
<b>Subagent session</b>
</text>
<ContextInfo context={context} cost={cost} />
</box>
<box flexDirection="row" gap={2}>
@@ -154,14 +124,7 @@ export function Header() {
</Match>
<Match when={true}>
<box flexDirection={narrow() ? "column" : "row"} justifyContent="space-between" gap={1}>
{Flag.OPENCODE_EXPERIMENTAL_WORKSPACES ? (
<box flexDirection="column">
<Title session={session} />
<WorkspaceInfo workspace={workspace} />
</box>
) : (
<Title session={session} />
)}
<Title session={session} />
<ContextInfo context={context} cost={cost} />
</box>
</Match>

View File

@@ -182,12 +182,6 @@ export function Session() {
return new CustomSpeedScroll(3)
})
createEffect(() => {
if (session()?.workspaceID) {
sdk.setWorkspace(session()?.workspaceID)
}
})
createEffect(async () => {
await sync.session
.sync(route.sessionID)

View File

@@ -42,9 +42,6 @@ function createWorkerFetch(client: RpcClient): typeof fetch {
function createEventSource(client: RpcClient): EventSource {
return {
on: (handler) => client.on<Event>("event", handler),
setWorkspace: (workspaceID) => {
void client.call("setWorkspace", { workspaceID })
},
}
}
@@ -113,20 +110,18 @@ export const TuiThreadCommand = cmd({
return
}
// Resolve relative --project paths from PWD, then use the real cwd after
// chdir so the thread and worker share the same directory key.
// Resolve relative paths against PWD to preserve behavior when using --cwd flag
const root = Filesystem.resolve(process.env.PWD ?? process.cwd())
const next = args.project
const cwd = args.project
? Filesystem.resolve(path.isAbsolute(args.project) ? args.project : path.join(root, args.project))
: Filesystem.resolve(process.cwd())
: root
const file = await target()
try {
process.chdir(next)
process.chdir(cwd)
} catch {
UI.error("Failed to change directory to " + next)
UI.error("Failed to change directory to " + cwd)
return
}
const cwd = Filesystem.resolve(process.cwd())
const worker = new Worker(file, {
env: Object.fromEntries(
@@ -213,6 +208,7 @@ export const TuiThreadCommand = cmd({
prompt,
fork: args.fork,
},
onExit: stop,
})
} finally {
await stop()

View File

@@ -1,9 +1,9 @@
import { $ } from "bun"
import { platform, release } from "os"
import clipboardy from "clipboardy"
import { lazy } from "../../../../util/lazy.js"
import { tmpdir } from "os"
import path from "path"
import fs from "fs/promises"
import { Filesystem } from "../../../../util/filesystem"
import { Process } from "../../../../util/process"
import { which } from "../../../../util/which"
@@ -34,38 +34,23 @@ export namespace Clipboard {
if (os === "darwin") {
const tmpfile = path.join(tmpdir(), "opencode-clipboard.png")
try {
await Process.run(
[
"osascript",
"-e",
'set imageData to the clipboard as "PNGf"',
"-e",
`set fileRef to open for access POSIX file "${tmpfile}" with write permission`,
"-e",
"set eof fileRef to 0",
"-e",
"write imageData to fileRef",
"-e",
"close access fileRef",
],
{ nothrow: true },
)
await $`osascript -e 'set imageData to the clipboard as "PNGf"' -e 'set fileRef to open for access POSIX file "${tmpfile}" with write permission' -e 'set eof fileRef to 0' -e 'write imageData to fileRef' -e 'close access fileRef'`
.nothrow()
.quiet()
const buffer = await Filesystem.readBytes(tmpfile)
return { data: buffer.toString("base64"), mime: "image/png" }
} catch {
} finally {
await fs.rm(tmpfile, { force: true }).catch(() => {})
await $`rm -f "${tmpfile}"`.nothrow().quiet()
}
}
if (os === "win32" || release().includes("WSL")) {
const script =
"Add-Type -AssemblyName System.Windows.Forms; $img = [System.Windows.Forms.Clipboard]::GetImage(); if ($img) { $ms = New-Object System.IO.MemoryStream; $img.Save($ms, [System.Drawing.Imaging.ImageFormat]::Png); [System.Convert]::ToBase64String($ms.ToArray()) }"
const base64 = await Process.text(["powershell.exe", "-NonInteractive", "-NoProfile", "-command", script], {
nothrow: true,
})
if (base64.text) {
const imageBuffer = Buffer.from(base64.text.trim(), "base64")
const base64 = await $`powershell.exe -NonInteractive -NoProfile -command "${script}"`.nothrow().text()
if (base64) {
const imageBuffer = Buffer.from(base64.trim(), "base64")
if (imageBuffer.length > 0) {
return { data: imageBuffer.toString("base64"), mime: "image/png" }
}
@@ -73,15 +58,13 @@ export namespace Clipboard {
}
if (os === "linux") {
const wayland = await Process.run(["wl-paste", "-t", "image/png"], { nothrow: true })
if (wayland.stdout.byteLength > 0) {
return { data: Buffer.from(wayland.stdout).toString("base64"), mime: "image/png" }
const wayland = await $`wl-paste -t image/png`.nothrow().arrayBuffer()
if (wayland && wayland.byteLength > 0) {
return { data: Buffer.from(wayland).toString("base64"), mime: "image/png" }
}
const x11 = await Process.run(["xclip", "-selection", "clipboard", "-t", "image/png", "-o"], {
nothrow: true,
})
if (x11.stdout.byteLength > 0) {
return { data: Buffer.from(x11.stdout).toString("base64"), mime: "image/png" }
const x11 = await $`xclip -selection clipboard -t image/png -o`.nothrow().arrayBuffer()
if (x11 && x11.byteLength > 0) {
return { data: Buffer.from(x11).toString("base64"), mime: "image/png" }
}
}
@@ -98,7 +81,7 @@ export namespace Clipboard {
console.log("clipboard: using osascript")
return async (text: string) => {
const escaped = text.replace(/\\/g, "\\\\").replace(/"/g, '\\"')
await Process.run(["osascript", "-e", `set the clipboard to "${escaped}"`], { nothrow: true })
await $`osascript -e 'set the clipboard to "${escaped}"'`.nothrow().quiet()
}
}

View File

@@ -44,7 +44,7 @@ const eventStream = {
abort: undefined as AbortController | undefined,
}
const startEventStream = (input: { directory: string; workspaceID?: string }) => {
const startEventStream = (directory: string) => {
if (eventStream.abort) eventStream.abort.abort()
const abort = new AbortController()
eventStream.abort = abort
@@ -54,13 +54,12 @@ const startEventStream = (input: { directory: string; workspaceID?: string }) =>
const request = new Request(input, init)
const auth = getAuthorizationHeader()
if (auth) request.headers.set("Authorization", auth)
return Server.Default().fetch(request)
return Server.App().fetch(request)
}) as typeof globalThis.fetch
const sdk = createOpencodeClient({
baseUrl: "http://opencode.internal",
directory: input.directory,
experimental_workspaceID: input.workspaceID,
directory,
fetch: fetchFn,
signal,
})
@@ -96,7 +95,7 @@ const startEventStream = (input: { directory: string; workspaceID?: string }) =>
})
}
startEventStream({ directory: process.cwd() })
startEventStream(process.cwd())
export const rpc = {
async fetch(input: { url: string; method: string; headers: Record<string, string>; body?: string }) {
@@ -110,7 +109,7 @@ export const rpc = {
headers,
body: input.body,
})
const response = await Server.Default().fetch(request)
const response = await Server.App().fetch(request)
const body = await response.text()
return {
status: response.status,
@@ -136,9 +135,6 @@ export const rpc = {
Config.global.reset()
await Instance.disposeAll()
},
async setWorkspace(input: { workspaceID?: string }) {
startEventStream({ directory: process.cwd(), workspaceID: input.workspaceID })
},
async shutdown() {
Log.Default.info("worker shutting down")
if (eventStream.abort) eventStream.abort.abort()

View File

@@ -3,11 +3,11 @@ import { UI } from "../ui"
import * as prompts from "@clack/prompts"
import { Installation } from "../../installation"
import { Global } from "../../global"
import { $ } from "bun"
import fs from "fs/promises"
import path from "path"
import os from "os"
import { Filesystem } from "../../util/filesystem"
import { Process } from "../../util/process"
interface UninstallArgs {
keepConfig: boolean
@@ -192,13 +192,16 @@ async function executeUninstall(method: Installation.Method, targets: RemovalTar
const cmd = cmds[method]
if (cmd) {
spinner.start(`Running ${cmd.join(" ")}...`)
const result = await Process.run(method === "choco" ? ["choco", "uninstall", "opencode", "-y", "-r"] : cmd, {
nothrow: true,
})
if (result.code !== 0) {
spinner.stop(`Package manager uninstall failed: exit code ${result.code}`, 1)
const text = `${result.stdout.toString("utf8")}\n${result.stderr.toString("utf8")}`
if (method === "choco" && text.includes("not running from an elevated command shell")) {
const result =
method === "choco"
? await $`echo Y | choco uninstall opencode -y -r`.quiet().nothrow()
: await $`${cmd}`.quiet().nothrow()
if (result.exitCode !== 0) {
spinner.stop(`Package manager uninstall failed: exit code ${result.exitCode}`, 1)
if (
method === "choco" &&
result.stdout.toString("utf8").includes("not running from an elevated command shell")
) {
prompts.log.warn(`You may need to run '${cmd.join(" ")}' from an elevated command shell`)
} else {
prompts.log.warn(`You may need to run manually: ${cmd.join(" ")}`)

View File

@@ -972,14 +972,6 @@ export namespace Config {
.describe(
"Timeout in milliseconds for requests to this provider. Default is 300000 (5 minutes). Set to false to disable timeout.",
),
chunkTimeout: z
.number()
.int()
.positive()
.optional()
.describe(
"Timeout in milliseconds between streamed SSE chunks for this provider. If no chunk arrives within this window, the request is aborted.",
),
})
.catchall(z.any())
.optional(),

View File

@@ -1,5 +1,6 @@
import { Instance } from "@/project/instance"
import type { MiddlewareHandler } from "hono"
import { Flag } from "../flag/flag"
import { Installation } from "../installation"
import { getAdaptor } from "./adaptors"
import { Workspace } from "./workspace"
import { WorkspaceContext } from "./workspace-context"
@@ -37,7 +38,7 @@ async function routeRequest(req: Request) {
export const WorkspaceRouterMiddleware: MiddlewareHandler = async (c, next) => {
// Only available in development for now
if (!Flag.OPENCODE_EXPERIMENTAL_WORKSPACES) {
if (!Installation.isLocal()) {
return next()
}

View File

@@ -1,5 +1,6 @@
import { BusEvent } from "@/bus/bus-event"
import z from "zod"
import { $ } from "bun"
import { formatPatch, structuredPatch } from "diff"
import path from "path"
import fs from "fs"
@@ -10,8 +11,6 @@ import { Instance } from "../project/instance"
import { Ripgrep } from "./ripgrep"
import fuzzysort from "fuzzysort"
import { Global } from "../global"
import { git } from "@/util/git"
import { Protected } from "./protected"
export namespace File {
const log = Log.create({ service: "file" })
@@ -346,7 +345,10 @@ export namespace File {
if (isGlobalHome) {
const dirs = new Set<string>()
const ignore = Protected.names()
const ignore = new Set<string>()
if (process.platform === "darwin") ignore.add("Library")
if (process.platform === "win32") ignore.add("AppData")
const ignoreNested = new Set(["node_modules", "dist", "build", "target", "vendor"])
const shouldIgnore = (name: string) => name.startsWith(".") || ignore.has(name)
@@ -416,11 +418,11 @@ export namespace File {
const project = Instance.project
if (project.vcs !== "git") return []
const diffOutput = (
await git(["-c", "core.fsmonitor=false", "-c", "core.quotepath=false", "diff", "--numstat", "HEAD"], {
cwd: Instance.directory,
})
).text()
const diffOutput = await $`git -c core.fsmonitor=false -c core.quotepath=false diff --numstat HEAD`
.cwd(Instance.directory)
.quiet()
.nothrow()
.text()
const changedFiles: Info[] = []
@@ -437,14 +439,12 @@ export namespace File {
}
}
const untrackedOutput = (
await git(
["-c", "core.fsmonitor=false", "-c", "core.quotepath=false", "ls-files", "--others", "--exclude-standard"],
{
cwd: Instance.directory,
},
)
).text()
const untrackedOutput =
await $`git -c core.fsmonitor=false -c core.quotepath=false ls-files --others --exclude-standard`
.cwd(Instance.directory)
.quiet()
.nothrow()
.text()
if (untrackedOutput.trim()) {
const untrackedFiles = untrackedOutput.trim().split("\n")
@@ -465,14 +465,12 @@ export namespace File {
}
// Get deleted files
const deletedOutput = (
await git(
["-c", "core.fsmonitor=false", "-c", "core.quotepath=false", "diff", "--name-only", "--diff-filter=D", "HEAD"],
{
cwd: Instance.directory,
},
)
).text()
const deletedOutput =
await $`git -c core.fsmonitor=false -c core.quotepath=false diff --name-only --diff-filter=D HEAD`
.cwd(Instance.directory)
.quiet()
.nothrow()
.text()
if (deletedOutput.trim()) {
const deletedFiles = deletedOutput.trim().split("\n")
@@ -543,14 +541,16 @@ export namespace File {
const content = (await Filesystem.readText(full).catch(() => "")).trim()
if (project.vcs === "git") {
let diff = (await git(["-c", "core.fsmonitor=false", "diff", "--", file], { cwd: Instance.directory })).text()
let diff = await $`git -c core.fsmonitor=false diff ${file}`.cwd(Instance.directory).quiet().nothrow().text()
if (!diff.trim()) {
diff = (
await git(["-c", "core.fsmonitor=false", "diff", "--staged", "--", file], { cwd: Instance.directory })
).text()
diff = await $`git -c core.fsmonitor=false diff --staged ${file}`
.cwd(Instance.directory)
.quiet()
.nothrow()
.text()
}
if (diff.trim()) {
const original = (await git(["show", `HEAD:${file}`], { cwd: Instance.directory })).text()
const original = await $`git show HEAD:${file}`.cwd(Instance.directory).quiet().nothrow().text()
const patch = structuredPatch(file, file, original, content, "old", "new", {
context: Infinity,
ignoreWhitespace: true,

View File

@@ -1,59 +0,0 @@
import path from "path"
import os from "os"
const home = os.homedir()
// macOS directories that trigger TCC (Transparency, Consent, and Control)
// permission prompts when accessed by a non-sandboxed process.
const DARWIN_HOME = [
// Media
"Music",
"Pictures",
"Movies",
// User-managed folders synced via iCloud / subject to TCC
"Downloads",
"Desktop",
"Documents",
// Other system-managed
"Public",
"Applications",
"Library",
]
const DARWIN_LIBRARY = [
"Application Support/AddressBook",
"Calendars",
"Mail",
"Messages",
"Safari",
"Cookies",
"Application Support/com.apple.TCC",
"PersonalizationPortrait",
"Metadata/CoreSpotlight",
"Suggestions",
]
const DARWIN_ROOT = ["/.DocumentRevisions-V100", "/.Spotlight-V100", "/.Trashes", "/.fseventsd"]
const WIN32_HOME = ["AppData", "Downloads", "Desktop", "Documents", "Pictures", "Music", "Videos", "OneDrive"]
export namespace Protected {
/** Directory basenames to skip when scanning the home directory. */
export function names(): ReadonlySet<string> {
if (process.platform === "darwin") return new Set(DARWIN_HOME)
if (process.platform === "win32") return new Set(WIN32_HOME)
return new Set()
}
/** Absolute paths that should never be watched, stated, or scanned. */
export function paths(): string[] {
if (process.platform === "darwin")
return [
...DARWIN_HOME.map((n) => path.join(home, n)),
...DARWIN_LIBRARY.map((n) => path.join(home, "Library", n)),
...DARWIN_ROOT,
]
if (process.platform === "win32") return WIN32_HOME.map((n) => path.join(home, n))
return []
}
}

View File

@@ -5,7 +5,7 @@ import fs from "fs/promises"
import z from "zod"
import { NamedError } from "@opencode-ai/util/error"
import { lazy } from "../util/lazy"
import { $ } from "bun"
import { Filesystem } from "../util/filesystem"
import { Process } from "../util/process"
import { which } from "../util/which"
@@ -338,7 +338,7 @@ export namespace Ripgrep {
limit?: number
follow?: boolean
}) {
const args = [`${await filepath()}`, "--json", "--hidden", "--glob=!.git/*"]
const args = [`${await filepath()}`, "--json", "--hidden", "--glob='!.git/*'"]
if (input.follow) args.push("--follow")
if (input.glob) {
@@ -354,16 +354,14 @@ export namespace Ripgrep {
args.push("--")
args.push(input.pattern)
const result = await Process.text(args, {
cwd: input.cwd,
nothrow: true,
})
if (result.code !== 0) {
const command = args.join(" ")
const result = await $`${{ raw: command }}`.cwd(input.cwd).quiet().nothrow()
if (result.exitCode !== 0) {
return []
}
// Handle both Unix (\n) and Windows (\r\n) line endings
const lines = result.text.trim().split(/\r?\n/).filter(Boolean)
const lines = result.text().trim().split(/\r?\n/).filter(Boolean)
// Parse JSON lines from ripgrep output
return lines

View File

@@ -11,10 +11,9 @@ import { createWrapper } from "@parcel/watcher/wrapper"
import { lazy } from "@/util/lazy"
import { withTimeout } from "@/util/timeout"
import type ParcelWatcher from "@parcel/watcher"
import { $ } from "bun"
import { Flag } from "@/flag/flag"
import { readdir } from "fs/promises"
import { git } from "@/util/git"
import { Protected } from "./protected"
const SUBSCRIBE_TIMEOUT_MS = 10_000
@@ -77,7 +76,7 @@ export namespace FileWatcher {
if (Flag.OPENCODE_EXPERIMENTAL_FILEWATCHER) {
const pending = w.subscribe(Instance.directory, subscribe, {
ignore: [...FileIgnore.PATTERNS, ...cfgIgnores, ...Protected.paths()],
ignore: [...FileIgnore.PATTERNS, ...cfgIgnores],
backend,
})
const sub = await withTimeout(pending, SUBSCRIBE_TIMEOUT_MS).catch((err) => {
@@ -89,10 +88,13 @@ export namespace FileWatcher {
}
if (Instance.project.vcs === "git") {
const result = await git(["rev-parse", "--git-dir"], {
cwd: Instance.worktree,
})
const vcsDir = result.exitCode === 0 ? path.resolve(Instance.worktree, result.text().trim()) : undefined
const vcsDir = await $`git rev-parse --git-dir`
.quiet()
.nothrow()
.cwd(Instance.worktree)
.text()
.then((x) => path.resolve(Instance.worktree, x.trim()))
.catch(() => undefined)
if (vcsDir && !cfgIgnores.includes(".git") && !cfgIgnores.includes(vcsDir)) {
const gitDirContents = await readdir(vcsDir).catch(() => [])
const ignoreList = gitDirContents.filter((entry) => entry !== "HEAD")

View File

@@ -57,7 +57,6 @@ export namespace Flag {
export const OPENCODE_EXPERIMENTAL_LSP_TOOL = OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_LSP_TOOL")
export const OPENCODE_DISABLE_FILETIME_CHECK = truthy("OPENCODE_DISABLE_FILETIME_CHECK")
export const OPENCODE_EXPERIMENTAL_PLAN_MODE = OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_PLAN_MODE")
export const OPENCODE_EXPERIMENTAL_WORKSPACES = OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_WORKSPACES")
export const OPENCODE_EXPERIMENTAL_MARKDOWN = !falsy("OPENCODE_EXPERIMENTAL_MARKDOWN")
export const OPENCODE_MODELS_URL = process.env["OPENCODE_MODELS_URL"]
export const OPENCODE_MODELS_PATH = process.env["OPENCODE_MODELS_PATH"]

View File

@@ -1,12 +1,11 @@
import { BusEvent } from "@/bus/bus-event"
import path from "path"
import { $ } from "bun"
import z from "zod"
import { NamedError } from "@opencode-ai/util/error"
import { Log } from "../util/log"
import { iife } from "@/util/iife"
import { Flag } from "../flag/flag"
import { Process } from "@/util/process"
import { buffer } from "node:stream/consumers"
declare global {
const OPENCODE_VERSION: string
@@ -16,38 +15,6 @@ declare global {
export namespace Installation {
const log = Log.create({ service: "installation" })
async function text(cmd: string[], opts: { cwd?: string; env?: NodeJS.ProcessEnv } = {}) {
return Process.text(cmd, {
cwd: opts.cwd,
env: opts.env,
nothrow: true,
}).then((x) => x.text)
}
async function upgradeCurl(target: string) {
const body = await fetch("https://opencode.ai/install").then((res) => {
if (!res.ok) throw new Error(res.statusText)
return res.text()
})
const proc = Process.spawn(["bash"], {
stdin: "pipe",
stdout: "pipe",
stderr: "pipe",
env: {
...process.env,
VERSION: target,
},
})
if (!proc.stdin || !proc.stdout || !proc.stderr) throw new Error("Process output not available")
proc.stdin.end(body)
const [code, stdout, stderr] = await Promise.all([proc.exited, buffer(proc.stdout), buffer(proc.stderr)])
return {
code,
stdout,
stderr,
}
}
export type Method = Awaited<ReturnType<typeof method>>
export const Event = {
@@ -98,31 +65,31 @@ export namespace Installation {
const checks = [
{
name: "npm" as const,
command: () => text(["npm", "list", "-g", "--depth=0"]),
command: () => $`npm list -g --depth=0`.throws(false).quiet().text(),
},
{
name: "yarn" as const,
command: () => text(["yarn", "global", "list"]),
command: () => $`yarn global list`.throws(false).quiet().text(),
},
{
name: "pnpm" as const,
command: () => text(["pnpm", "list", "-g", "--depth=0"]),
command: () => $`pnpm list -g --depth=0`.throws(false).quiet().text(),
},
{
name: "bun" as const,
command: () => text(["bun", "pm", "ls", "-g"]),
command: () => $`bun pm ls -g`.throws(false).quiet().text(),
},
{
name: "brew" as const,
command: () => text(["brew", "list", "--formula", "opencode"]),
command: () => $`brew list --formula opencode`.throws(false).quiet().text(),
},
{
name: "scoop" as const,
command: () => text(["scoop", "list", "opencode"]),
command: () => $`scoop list opencode`.throws(false).quiet().text(),
},
{
name: "choco" as const,
command: () => text(["choco", "list", "--limit-output", "opencode"]),
command: () => $`choco list --limit-output opencode`.throws(false).quiet().text(),
},
]
@@ -154,70 +121,61 @@ export namespace Installation {
)
async function getBrewFormula() {
const tapFormula = await text(["brew", "list", "--formula", "anomalyco/tap/opencode"])
const tapFormula = await $`brew list --formula anomalyco/tap/opencode`.throws(false).quiet().text()
if (tapFormula.includes("opencode")) return "anomalyco/tap/opencode"
const coreFormula = await text(["brew", "list", "--formula", "opencode"])
const coreFormula = await $`brew list --formula opencode`.throws(false).quiet().text()
if (coreFormula.includes("opencode")) return "opencode"
return "opencode"
}
export async function upgrade(method: Method, target: string) {
let result: Awaited<ReturnType<typeof upgradeCurl>> | undefined
let cmd
switch (method) {
case "curl":
result = await upgradeCurl(target)
cmd = $`curl -fsSL https://opencode.ai/install | bash`.env({
...process.env,
VERSION: target,
})
break
case "npm":
result = await Process.run(["npm", "install", "-g", `opencode-ai@${target}`], { nothrow: true })
cmd = $`npm install -g opencode-ai@${target}`
break
case "pnpm":
result = await Process.run(["pnpm", "install", "-g", `opencode-ai@${target}`], { nothrow: true })
cmd = $`pnpm install -g opencode-ai@${target}`
break
case "bun":
result = await Process.run(["bun", "install", "-g", `opencode-ai@${target}`], { nothrow: true })
cmd = $`bun install -g opencode-ai@${target}`
break
case "brew": {
const formula = await getBrewFormula()
const env = {
if (formula.includes("/")) {
cmd =
$`brew tap anomalyco/tap && cd "$(brew --repo anomalyco/tap)" && git pull --ff-only && brew upgrade ${formula}`.env(
{
HOMEBREW_NO_AUTO_UPDATE: "1",
...process.env,
},
)
break
}
cmd = $`brew upgrade ${formula}`.env({
HOMEBREW_NO_AUTO_UPDATE: "1",
...process.env,
}
if (formula.includes("/")) {
const tap = await Process.run(["brew", "tap", "anomalyco/tap"], { env, nothrow: true })
if (tap.code !== 0) {
result = tap
break
}
const repo = await Process.text(["brew", "--repo", "anomalyco/tap"], { env, nothrow: true })
if (repo.code !== 0) {
result = repo
break
}
const dir = repo.text.trim()
if (dir) {
const pull = await Process.run(["git", "pull", "--ff-only"], { cwd: dir, env, nothrow: true })
if (pull.code !== 0) {
result = pull
break
}
}
}
result = await Process.run(["brew", "upgrade", formula], { env, nothrow: true })
})
break
}
case "choco":
result = await Process.run(["choco", "upgrade", "opencode", `--version=${target}`, "-y"], { nothrow: true })
cmd = $`echo Y | choco upgrade opencode --version=${target}`
break
case "scoop":
result = await Process.run(["scoop", "install", `opencode@${target}`], { nothrow: true })
cmd = $`scoop install opencode@${target}`
break
default:
throw new Error(`Unknown method: ${method}`)
}
if (!result || result.code !== 0) {
const stderr =
method === "choco" ? "not running from an elevated command shell" : result?.stderr.toString("utf8") || ""
const result = await cmd.quiet().throws(false)
if (result.exitCode !== 0) {
const stderr = method === "choco" ? "not running from an elevated command shell" : result.stderr.toString("utf8")
throw new UpgradeFailedError({
stderr: stderr,
})
@@ -228,7 +186,7 @@ export namespace Installation {
stdout: result.stdout.toString(),
stderr: result.stderr.toString(),
})
await Process.text([process.execPath, "--version"], { nothrow: true })
await $`${process.execPath} --version`.nothrow().quiet().text()
}
export const VERSION = typeof OPENCODE_VERSION === "string" ? OPENCODE_VERSION : "local"
@@ -241,7 +199,7 @@ export namespace Installation {
if (detectedMethod === "brew") {
const formula = await getBrewFormula()
if (formula.includes("/")) {
const infoJson = await text(["brew", "info", "--json=v2", formula])
const infoJson = await $`brew info --json=v2 ${formula}`.quiet().text()
const info = JSON.parse(infoJson)
const version = info.formulae?.[0]?.versions?.stable
if (!version) throw new Error(`Could not detect version for tap formula: ${formula}`)
@@ -257,7 +215,7 @@ export namespace Installation {
if (detectedMethod === "npm" || detectedMethod === "bun" || detectedMethod === "pnpm") {
const registry = await iife(async () => {
const r = (await text(["npm", "config", "get", "registry"])).trim()
const r = (await $`npm config get registry`.quiet().nothrow().text()).trim()
const reg = r || "https://registry.npmjs.org"
return reg.endsWith("/") ? reg.slice(0, -1) : reg
})

View File

@@ -4,6 +4,7 @@ import os from "os"
import { Global } from "../global"
import { Log } from "../util/log"
import { BunProc } from "../bun"
import { $ } from "bun"
import { text } from "node:stream/consumers"
import fs from "fs/promises"
import { Filesystem } from "../util/filesystem"
@@ -12,7 +13,6 @@ import { Flag } from "../flag/flag"
import { Archive } from "../util/archive"
import { Process } from "../util/process"
import { which } from "../util/which"
import { Module } from "@opencode-ai/util/module"
export namespace LSPServer {
const log = Log.create({ service: "lsp.server" })
@@ -21,8 +21,6 @@ export namespace LSPServer {
.stat(p)
.then(() => true)
.catch(() => false)
const run = (cmd: string[], opts: Process.RunOptions = {}) => Process.run(cmd, { ...opts, nothrow: true })
const output = (cmd: string[], opts: Process.RunOptions = {}) => Process.text(cmd, { ...opts, nothrow: true })
export interface Handle {
process: ChildProcessWithoutNullStreams
@@ -99,7 +97,7 @@ export namespace LSPServer {
),
extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts"],
async spawn(root) {
const tsserver = Module.resolve("typescript/lib/tsserver.js", Instance.directory)
const tsserver = await Bun.resolve("typescript/lib/tsserver.js", Instance.directory).catch(() => {})
log.info("typescript server", { tsserver })
if (!tsserver) return
const proc = spawn(BunProc.which(), ["x", "typescript-language-server", "--stdio"], {
@@ -174,7 +172,7 @@ export namespace LSPServer {
root: NearestRoot(["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock"]),
extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts", ".vue"],
async spawn(root) {
const eslint = Module.resolve("eslint", Instance.directory)
const eslint = await Bun.resolve("eslint", Instance.directory).catch(() => {})
if (!eslint) return
log.info("spawning eslint server")
const serverPath = path.join(Global.Path.bin, "vscode-eslint", "server", "out", "eslintServer.js")
@@ -207,8 +205,8 @@ export namespace LSPServer {
await fs.rename(extractedPath, finalPath)
const npmCmd = process.platform === "win32" ? "npm.cmd" : "npm"
await Process.run([npmCmd, "install"], { cwd: finalPath })
await Process.run([npmCmd, "run", "compile"], { cwd: finalPath })
await $`${npmCmd} install`.cwd(finalPath).quiet()
await $`${npmCmd} run compile`.cwd(finalPath).quiet()
log.info("installed VS Code ESLint server", { serverPath })
}
@@ -342,7 +340,7 @@ export namespace LSPServer {
let args = ["lsp-proxy", "--stdio"]
if (!bin) {
const resolved = Module.resolve("biome", root)
const resolved = await Bun.resolve("biome", root).catch(() => undefined)
if (!resolved) return
bin = BunProc.which()
args = ["x", "biome", "lsp-proxy", "--stdio"]
@@ -604,11 +602,10 @@ export namespace LSPServer {
recursive: true,
})
const cwd = path.join(Global.Path.bin, "elixir-ls-master")
const env = { MIX_ENV: "prod", ...process.env }
await Process.run(["mix", "deps.get"], { cwd, env })
await Process.run(["mix", "compile"], { cwd, env })
await Process.run(["mix", "elixir_ls.release2", "-o", "release"], { cwd, env })
await $`mix deps.get && mix compile && mix elixir_ls.release2 -o release`
.quiet()
.cwd(path.join(Global.Path.bin, "elixir-ls-master"))
.env({ MIX_ENV: "prod", ...process.env })
log.info(`installed elixir-ls`, {
path: elixirLsPath,
@@ -709,7 +706,7 @@ export namespace LSPServer {
})
if (!ok) return
} else {
await run(["tar", "-xf", tempPath], { cwd: Global.Path.bin })
await $`tar -xf ${tempPath}`.cwd(Global.Path.bin).quiet().nothrow()
}
await fs.rm(tempPath, { force: true })
@@ -722,7 +719,7 @@ export namespace LSPServer {
}
if (platform !== "win32") {
await fs.chmod(bin, 0o755).catch(() => {})
await $`chmod +x ${bin}`.quiet().nothrow()
}
log.info(`installed zls`, { bin })
@@ -834,11 +831,11 @@ export namespace LSPServer {
// This is specific to macOS where sourcekit-lsp is typically installed with Xcode
if (!which("xcrun")) return
const lspLoc = await output(["xcrun", "--find", "sourcekit-lsp"])
const lspLoc = await $`xcrun --find sourcekit-lsp`.quiet().nothrow()
if (lspLoc.code !== 0) return
if (lspLoc.exitCode !== 0) return
const bin = lspLoc.text.trim()
const bin = lspLoc.text().trim()
return {
process: spawn(bin, {
@@ -1013,7 +1010,7 @@ export namespace LSPServer {
if (!ok) return
}
if (tar) {
await run(["tar", "-xf", archive], { cwd: Global.Path.bin })
await $`tar -xf ${archive}`.cwd(Global.Path.bin).quiet().nothrow()
}
await fs.rm(archive, { force: true })
@@ -1024,7 +1021,7 @@ export namespace LSPServer {
}
if (platform !== "win32") {
await fs.chmod(bin, 0o755).catch(() => {})
await $`chmod +x ${bin}`.quiet().nothrow()
}
await fs.unlink(path.join(Global.Path.bin, "clangd")).catch(() => {})
@@ -1085,7 +1082,7 @@ export namespace LSPServer {
extensions: [".astro"],
root: NearestRoot(["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock"]),
async spawn(root) {
const tsserver = Module.resolve("typescript/lib/tsserver.js", Instance.directory)
const tsserver = await Bun.resolve("typescript/lib/tsserver.js", Instance.directory).catch(() => {})
if (!tsserver) {
log.info("typescript not found, required for Astro language server")
return
@@ -1133,30 +1130,7 @@ export namespace LSPServer {
export const JDTLS: Info = {
id: "jdtls",
root: async (file) => {
// Without exclusions, NearestRoot defaults to instance directory so we can't
// distinguish between a) no project found and b) project found at instance dir.
// So we can't choose the root from (potential) monorepo markers first.
// Look for potential subproject markers first while excluding potential monorepo markers.
const settingsMarkers = ["settings.gradle", "settings.gradle.kts"]
const gradleMarkers = ["gradlew", "gradlew.bat"]
const exclusionsForMonorepos = gradleMarkers.concat(settingsMarkers)
const [projectRoot, wrapperRoot, settingsRoot] = await Promise.all([
NearestRoot(
["pom.xml", "build.gradle", "build.gradle.kts", ".project", ".classpath"],
exclusionsForMonorepos,
)(file),
NearestRoot(gradleMarkers, settingsMarkers)(file),
NearestRoot(settingsMarkers)(file),
])
// If projectRoot is undefined we know we are in a monorepo or no project at all.
// So can safely fall through to the other roots
if (projectRoot) return projectRoot
if (wrapperRoot) return wrapperRoot
if (settingsRoot) return settingsRoot
},
root: NearestRoot(["pom.xml", "build.gradle", "build.gradle.kts", ".project", ".classpath"]),
extensions: [".java"],
async spawn(root) {
const java = which("java")
@@ -1164,10 +1138,13 @@ export namespace LSPServer {
log.error("Java 21 or newer is required to run the JDTLS. Please install it first.")
return
}
const javaMajorVersion = await run(["java", "-version"]).then((result) => {
const m = /"(\d+)\.\d+\.\d+"/.exec(result.stderr.toString())
return !m ? undefined : parseInt(m[1])
})
const javaMajorVersion = await $`java -version`
.quiet()
.nothrow()
.then(({ stderr }) => {
const m = /"(\d+)\.\d+\.\d+"/.exec(stderr.toString())
return !m ? undefined : parseInt(m[1])
})
if (javaMajorVersion == null || javaMajorVersion < 21) {
log.error("JDTLS requires at least Java 21.")
return
@@ -1184,27 +1161,27 @@ export namespace LSPServer {
const archiveName = "release.tar.gz"
log.info("Downloading JDTLS archive", { url: releaseURL, dest: distPath })
const download = await fetch(releaseURL)
if (!download.ok || !download.body) {
log.error("Failed to download JDTLS", { status: download.status, statusText: download.statusText })
const curlResult = await $`curl -L -o ${archiveName} '${releaseURL}'`.cwd(distPath).quiet().nothrow()
if (curlResult.exitCode !== 0) {
log.error("Failed to download JDTLS", { exitCode: curlResult.exitCode, stderr: curlResult.stderr.toString() })
return
}
await Filesystem.writeStream(path.join(distPath, archiveName), download.body)
log.info("Extracting JDTLS archive")
const tarResult = await run(["tar", "-xzf", archiveName], { cwd: distPath })
if (tarResult.code !== 0) {
log.error("Failed to extract JDTLS", { exitCode: tarResult.code, stderr: tarResult.stderr.toString() })
const tarResult = await $`tar -xzf ${archiveName}`.cwd(distPath).quiet().nothrow()
if (tarResult.exitCode !== 0) {
log.error("Failed to extract JDTLS", { exitCode: tarResult.exitCode, stderr: tarResult.stderr.toString() })
return
}
await fs.rm(path.join(distPath, archiveName), { force: true })
log.info("JDTLS download and extraction completed")
}
const jarFileName =
(await fs.readdir(launcherDir).catch(() => []))
.find((item) => /^org\.eclipse\.equinox\.launcher_.*\.jar$/.test(item))
?.trim() ?? ""
const jarFileName = await $`ls org.eclipse.equinox.launcher_*.jar`
.cwd(launcherDir)
.quiet()
.nothrow()
.then(({ stdout }) => stdout.toString().trim())
const launcherJar = path.join(launcherDir, jarFileName)
if (!(await pathExists(launcherJar))) {
log.error(`Failed to locate the JDTLS launcher module in the installed directory: ${distPath}.`)
@@ -1317,15 +1294,7 @@ export namespace LSPServer {
await fs.mkdir(distPath, { recursive: true })
const archivePath = path.join(distPath, "kotlin-ls.zip")
const download = await fetch(releaseURL)
if (!download.ok || !download.body) {
log.error("Failed to download Kotlin Language Server", {
status: download.status,
statusText: download.statusText,
})
return
}
await Filesystem.writeStream(archivePath, download.body)
await $`curl -L -o '${archivePath}' '${releaseURL}'`.quiet().nothrow()
const ok = await Archive.extractZip(archivePath, distPath)
.then(() => true)
.catch((error) => {
@@ -1335,7 +1304,7 @@ export namespace LSPServer {
if (!ok) return
await fs.rm(archivePath, { force: true })
if (process.platform !== "win32") {
await fs.chmod(launcherScript, 0o755).catch(() => {})
await $`chmod +x ${launcherScript}`.quiet().nothrow()
}
log.info("Installed Kotlin Language Server", { path: launcherScript })
}
@@ -1499,9 +1468,10 @@ export namespace LSPServer {
})
if (!ok) return
} else {
const ok = await run(["tar", "-xzf", tempPath, "-C", installDir])
.then((result) => result.code === 0)
.catch((error: unknown) => {
const ok = await $`tar -xzf ${tempPath} -C ${installDir}`
.quiet()
.then(() => true)
.catch((error) => {
log.error("Failed to extract lua-language-server archive", { error })
return false
})
@@ -1519,15 +1489,11 @@ export namespace LSPServer {
}
if (platform !== "win32") {
const ok = await fs
.chmod(bin, 0o755)
.then(() => true)
.catch((error: unknown) => {
log.error("Failed to set executable permission for lua-language-server binary", {
error,
})
return false
const ok = await $`chmod +x ${bin}`.quiet().catch((error) => {
log.error("Failed to set executable permission for lua-language-server binary", {
error,
})
})
if (!ok) return
}
@@ -1741,7 +1707,7 @@ export namespace LSPServer {
}
if (platform !== "win32") {
await fs.chmod(bin, 0o755).catch(() => {})
await $`chmod +x ${bin}`.quiet().nothrow()
}
log.info(`installed terraform-ls`, { bin })
@@ -1824,7 +1790,7 @@ export namespace LSPServer {
if (!ok) return
}
if (ext === "tar.gz") {
await run(["tar", "-xzf", tempPath], { cwd: Global.Path.bin })
await $`tar -xzf ${tempPath}`.cwd(Global.Path.bin).quiet().nothrow()
}
await fs.rm(tempPath, { force: true })
@@ -1837,7 +1803,7 @@ export namespace LSPServer {
}
if (platform !== "win32") {
await fs.chmod(bin, 0o755).catch(() => {})
await $`chmod +x ${bin}`.quiet().nothrow()
}
log.info("installed texlab", { bin })
@@ -2029,7 +1995,7 @@ export namespace LSPServer {
})
if (!ok) return
} else {
await run(["tar", "-xzf", tempPath, "--strip-components=1"], { cwd: Global.Path.bin })
await $`tar -xzf ${tempPath} --strip-components=1`.cwd(Global.Path.bin).quiet().nothrow()
}
await fs.rm(tempPath, { force: true })
@@ -2042,7 +2008,7 @@ export namespace LSPServer {
}
if (platform !== "win32") {
await fs.chmod(bin, 0o755).catch(() => {})
await $`chmod +x ${bin}`.quiet().nothrow()
}
log.info("installed tinymist", { bin })

View File

@@ -396,14 +396,8 @@ export namespace MCP {
} catch (error) {
lastError = error instanceof Error ? error : new Error(String(error))
// Handle OAuth-specific errors.
// The SDK throws UnauthorizedError when auth() returns 'REDIRECT',
// but may also throw plain Errors when auth() fails internally
// (e.g. during discovery, registration, or state generation).
// When an authProvider is attached, treat both cases as auth-related.
const isAuthError =
error instanceof UnauthorizedError || (authProvider && lastError.message.includes("OAuth"))
if (isAuthError) {
// Handle OAuth-specific errors
if (error instanceof UnauthorizedError) {
log.info("mcp server requires authentication", { key, transport: name })
// Check if this is a "needs registration" error

View File

@@ -144,19 +144,10 @@ export class McpOAuthProvider implements OAuthClientProvider {
async state(): Promise<string> {
const entry = await McpAuth.get(this.mcpName)
if (entry?.oauthState) {
return entry.oauthState
if (!entry?.oauthState) {
throw new Error(`No OAuth state saved for MCP server: ${this.mcpName}`)
}
// Generate a new state if none exists — the SDK calls state() as a
// generator, not just a reader, so we need to produce a value even when
// startAuth() hasn't pre-saved one (e.g. during automatic auth on first
// connect).
const newState = Array.from(crypto.getRandomValues(new Uint8Array(32)))
.map((b) => b.toString(16).padStart(2, "0"))
.join("")
await McpAuth.updateOAuthState(this.mcpName, newState)
return newState
return entry.oauthState
}
async invalidateCredentials(type: "all" | "client" | "tokens"): Promise<void> {

View File

@@ -25,7 +25,8 @@ export namespace Plugin {
const client = createOpencodeClient({
baseUrl: "http://localhost:4096",
directory: Instance.directory,
fetch: async (...args) => Server.Default().fetch(...args),
// @ts-ignore - fetch type incompatibility
fetch: async (...args) => Server.App().fetch(...args),
})
const config = await Config.get()
const hooks: Hooks[] = []
@@ -34,9 +35,7 @@ export namespace Plugin {
project: Instance.project,
worktree: Instance.worktree,
directory: Instance.directory,
get serverUrl(): URL {
throw new Error("Server URL is no longer supported in plugins")
},
serverUrl: Server.url(),
$: Bun.$,
}

View File

@@ -88,12 +88,6 @@ export namespace Project {
}
}
function readCachedId(dir: string) {
return Filesystem.readText(path.join(dir, "opencode"))
.then((x) => x.trim())
.catch(() => undefined)
}
export async function fromDirectory(directory: string) {
log.info("fromDirectory", { directory })
@@ -107,43 +101,19 @@ export namespace Project {
const gitBinary = which("git")
// cached id calculation
let id = await readCachedId(dotgit)
let id = await Filesystem.readText(path.join(dotgit, "opencode"))
.then((x) => x.trim())
.catch(() => undefined)
if (!gitBinary) {
return {
id: id ?? "global",
worktree: sandbox,
sandbox,
sandbox: sandbox,
vcs: Info.shape.vcs.parse(Flag.OPENCODE_FAKE_VCS),
}
}
const worktree = await git(["rev-parse", "--git-common-dir"], {
cwd: sandbox,
})
.then(async (result) => {
const common = gitpath(sandbox, await result.text())
// Avoid going to parent of sandbox when git-common-dir is empty.
return common === sandbox ? sandbox : path.dirname(common)
})
.catch(() => undefined)
if (!worktree) {
return {
id: id ?? "global",
worktree: sandbox,
sandbox,
vcs: Info.shape.vcs.parse(Flag.OPENCODE_FAKE_VCS),
}
}
// In the case of a git worktree, it can't cache the id
// because `.git` is not a folder, but it always needs the
// same project id as the common dir, so we resolve it now
if (id == null) {
id = await readCachedId(path.join(worktree, ".git"))
}
// generate id from root commit
if (!id) {
const roots = await git(["rev-list", "--max-parents=0", "--all"], {
@@ -162,7 +132,7 @@ export namespace Project {
return {
id: "global",
worktree: sandbox,
sandbox,
sandbox: sandbox,
vcs: Info.shape.vcs.parse(Flag.OPENCODE_FAKE_VCS),
}
}
@@ -177,7 +147,7 @@ export namespace Project {
return {
id: "global",
worktree: sandbox,
sandbox,
sandbox: sandbox,
vcs: "git",
}
}
@@ -191,14 +161,33 @@ export namespace Project {
if (!top) {
return {
id,
worktree: sandbox,
sandbox,
worktree: sandbox,
vcs: Info.shape.vcs.parse(Flag.OPENCODE_FAKE_VCS),
}
}
sandbox = top
const worktree = await git(["rev-parse", "--git-common-dir"], {
cwd: sandbox,
})
.then(async (result) => {
const common = gitpath(sandbox, await result.text())
// Avoid going to parent of sandbox when git-common-dir is empty.
return common === sandbox ? sandbox : path.dirname(common)
})
.catch(() => undefined)
if (!worktree) {
return {
id,
sandbox,
worktree: sandbox,
vcs: Info.shape.vcs.parse(Flag.OPENCODE_FAKE_VCS),
}
}
return {
id,
sandbox,

View File

@@ -1,11 +1,11 @@
import { BusEvent } from "@/bus/bus-event"
import { Bus } from "@/bus"
import { $ } from "bun"
import path from "path"
import z from "zod"
import { Log } from "@/util/log"
import { Instance } from "./instance"
import { FileWatcher } from "@/file/watcher"
import { git } from "@/util/git"
const log = Log.create({ service: "vcs" })
@@ -29,13 +29,13 @@ export namespace Vcs {
export type Info = z.infer<typeof Info>
async function currentBranch() {
const result = await git(["rev-parse", "--abbrev-ref", "HEAD"], {
cwd: Instance.worktree,
})
if (result.exitCode !== 0) return
const text = result.text().trim()
if (!text) return
return text
return $`git rev-parse --abbrev-ref HEAD`
.quiet()
.nothrow()
.cwd(Instance.worktree)
.text()
.then((x) => x.trim())
.catch(() => undefined)
}
const state = Instance.state(

View File

@@ -46,8 +46,6 @@ import { GoogleAuth } from "google-auth-library"
import { ProviderTransform } from "./transform"
import { Installation } from "../installation"
const DEFAULT_CHUNK_TIMEOUT = 120_000
export namespace Provider {
const log = Log.create({ service: "provider" })
@@ -87,54 +85,6 @@ export namespace Provider {
})
}
function wrapSSE(res: Response, ms: number, ctl: AbortController) {
if (typeof ms !== "number" || ms <= 0) return res
if (!res.body) return res
if (!res.headers.get("content-type")?.includes("text/event-stream")) return res
const reader = res.body.getReader()
const body = new ReadableStream<Uint8Array>({
async pull(ctrl) {
const part = await new Promise<Awaited<ReturnType<typeof reader.read>>>((resolve, reject) => {
const id = setTimeout(() => {
const err = new Error("SSE read timed out")
ctl.abort(err)
void reader.cancel(err)
reject(err)
}, ms)
reader.read().then(
(part) => {
clearTimeout(id)
resolve(part)
},
(err) => {
clearTimeout(id)
reject(err)
},
)
})
if (part.done) {
ctrl.close()
return
}
ctrl.enqueue(part.value)
},
async cancel(reason) {
ctl.abort(reason)
await reader.cancel(reason)
},
})
return new Response(body, {
headers: new Headers(res.headers),
status: res.status,
statusText: res.statusText,
})
}
const BUNDLED_PROVIDERS: Record<string, (options: any) => SDK> = {
"@ai-sdk/amazon-bedrock": createAmazonBedrock,
"@ai-sdk/anthropic": createAnthropic,
@@ -530,7 +480,6 @@ export namespace Provider {
const aiGatewayHeaders = {
"User-Agent": `opencode/${Installation.VERSION} gitlab-ai-provider/${GITLAB_PROVIDER_VERSION} (${os.platform()} ${os.release()}; ${os.arch()})`,
"anthropic-beta": "context-1m-2025-08-07",
...(providerConfig?.options?.aiGatewayHeaders || {}),
}
@@ -1142,23 +1091,21 @@ export namespace Provider {
if (existing) return existing
const customFetch = options["fetch"]
const chunkTimeout = options["chunkTimeout"] || DEFAULT_CHUNK_TIMEOUT
delete options["chunkTimeout"]
options["fetch"] = async (input: any, init?: BunFetchRequestInit) => {
// Preserve custom fetch if it exists, wrap it with timeout logic
const fetchFn = customFetch ?? fetch
const opts = init ?? {}
const chunkAbortCtl = typeof chunkTimeout === "number" && chunkTimeout > 0 ? new AbortController() : undefined
const signals: AbortSignal[] = []
if (opts.signal) signals.push(opts.signal)
if (chunkAbortCtl) signals.push(chunkAbortCtl.signal)
if (options["timeout"] !== undefined && options["timeout"] !== null && options["timeout"] !== false)
signals.push(AbortSignal.timeout(options["timeout"]))
if (options["timeout"] !== undefined && options["timeout"] !== null) {
const signals: AbortSignal[] = []
if (opts.signal) signals.push(opts.signal)
if (options["timeout"] !== false) signals.push(AbortSignal.timeout(options["timeout"]))
const combined = signals.length === 0 ? null : signals.length === 1 ? signals[0] : AbortSignal.any(signals)
if (combined) opts.signal = combined
const combined = signals.length > 1 ? AbortSignal.any(signals) : signals[0]
opts.signal = combined
}
// Strip openai itemId metadata following what codex does
// Codex uses #[serde(skip_serializing)] on id fields for all item types:
@@ -1178,14 +1125,11 @@ export namespace Provider {
}
}
const res = await fetchFn(input, {
return fetchFn(input, {
...opts,
// @ts-ignore see here: https://github.com/oven-sh/bun/issues/16682
timeout: false,
})
if (!chunkAbortCtl) return res
return wrapSSE(res, chunkTimeout, chunkAbortCtl)
}
const bundledFn = BUNDLED_PROVIDERS[model.api.npm]
@@ -1344,6 +1288,12 @@ export namespace Provider {
}
}
// Check if opencode provider is available before using it
const opencodeProvider = await state().then((state) => state.providers["opencode"])
if (opencodeProvider && opencodeProvider.models["gpt-5-nano"]) {
return getModel("opencode", "gpt-5-nano")
}
return undefined
}

View File

@@ -440,9 +440,7 @@ export namespace ProviderTransform {
const copilotEfforts = iife(() => {
if (id.includes("5.1-codex-max") || id.includes("5.2") || id.includes("5.3"))
return [...WIDELY_SUPPORTED_EFFORTS, "xhigh"]
const arr = [...WIDELY_SUPPORTED_EFFORTS]
if (id.includes("gpt-5") && model.release_date >= "2025-12-04") arr.push("xhigh")
return arr
return WIDELY_SUPPORTED_EFFORTS
})
return Object.fromEntries(
copilotEfforts.map((effort) => [
@@ -657,21 +655,9 @@ export namespace ProviderTransform {
// https://v5.ai-sdk.dev/providers/ai-sdk-providers/perplexity
return {}
case "@mymediset/sap-ai-provider":
case "@jerome-benoit/sap-ai-provider-v2":
if (model.api.id.includes("anthropic")) {
if (isAnthropicAdaptive) {
return Object.fromEntries(
adaptiveEfforts.map((effort) => [
effort,
{
thinking: {
type: "adaptive",
},
effort,
},
]),
)
}
return {
high: {
thinking: {
@@ -687,26 +673,7 @@ export namespace ProviderTransform {
},
}
}
if (model.api.id.includes("gemini") && id.includes("2.5")) {
return {
high: {
thinkingConfig: {
includeThoughts: true,
thinkingBudget: 16000,
},
},
max: {
thinkingConfig: {
includeThoughts: true,
thinkingBudget: 24576,
},
},
}
}
if (model.api.id.includes("gpt") || /\bo[1-9]/.test(model.api.id)) {
return Object.fromEntries(WIDELY_SUPPORTED_EFFORTS.map((effort) => [effort, { reasoningEffort: effort }]))
}
return {}
return Object.fromEntries(WIDELY_SUPPORTED_EFFORTS.map((effort) => [effort, { reasoningEffort: effort }]))
}
return {}
}

File diff suppressed because it is too large Load Diff

View File

@@ -219,7 +219,6 @@ export namespace Session {
parentID: Identifier.schema("session").optional(),
title: z.string().optional(),
permission: Info.shape.permission,
workspaceID: Identifier.schema("workspace").optional(),
})
.optional(),
async (input) => {
@@ -228,7 +227,6 @@ export namespace Session {
directory: Instance.directory,
title: input?.title,
permission: input?.permission,
workspaceID: input?.workspaceID,
})
},
)
@@ -244,7 +242,6 @@ export namespace Session {
const title = getForkedTitle(original.title)
const session = await createNext({
directory: Instance.directory,
workspaceID: original.workspaceID,
title,
})
const msgs = await messages({ sessionID: input.sessionID })
@@ -295,7 +292,6 @@ export namespace Session {
id?: string
title?: string
parentID?: string
workspaceID?: string
directory: string
permission?: PermissionNext.Ruleset
}) {
@@ -305,7 +301,7 @@ export namespace Session {
version: Installation.VERSION,
projectID: Instance.project.id,
directory: input.directory,
workspaceID: input.workspaceID,
workspaceID: WorkspaceContext.workspaceID,
parentID: input.parentID,
title: input.title ?? createDefaultTitle(!!input.parentID),
permission: input.permission,

View File

@@ -31,8 +31,7 @@ import { Flag } from "../flag/flag"
import { ulid } from "ulid"
import { spawn } from "child_process"
import { Command } from "../command"
import { $ } from "bun"
import { pathToFileURL, fileURLToPath } from "url"
import { $, fileURLToPath, pathToFileURL } from "bun"
import { ConfigMarkdown } from "../config/markdown"
import { SessionSummary } from "./summary"
import { NamedError } from "@opencode-ai/util/error"

View File

@@ -1,3 +1,4 @@
import { $ } from "bun"
import path from "path"
import fs from "fs/promises"
import { Filesystem } from "../util/filesystem"
@@ -8,17 +9,12 @@ import z from "zod"
import { Config } from "../config/config"
import { Instance } from "../project/instance"
import { Scheduler } from "../scheduler"
import { Process } from "@/util/process"
export namespace Snapshot {
const log = Log.create({ service: "snapshot" })
const hour = 60 * 60 * 1000
const prune = "7.days"
function args(git: string, cmd: string[]) {
return ["--git-dir", git, "--work-tree", Instance.worktree, ...cmd]
}
export function init() {
Scheduler.register({
id: "snapshot.cleanup",
@@ -38,13 +34,13 @@ export namespace Snapshot {
.then(() => true)
.catch(() => false)
if (!exists) return
const result = await Process.run(["git", ...args(git, ["gc", `--prune=${prune}`])], {
cwd: Instance.directory,
nothrow: true,
})
if (result.code !== 0) {
const result = await $`git --git-dir ${git} --work-tree ${Instance.worktree} gc --prune=${prune}`
.quiet()
.cwd(Instance.directory)
.nothrow()
if (result.exitCode !== 0) {
log.warn("cleanup failed", {
exitCode: result.code,
exitCode: result.exitCode,
stderr: result.stderr.toString(),
stdout: result.stdout.toString(),
})
@@ -59,27 +55,27 @@ export namespace Snapshot {
if (cfg.snapshot === false) return
const git = gitdir()
if (await fs.mkdir(git, { recursive: true })) {
await Process.run(["git", "init"], {
env: {
await $`git init`
.env({
...process.env,
GIT_DIR: git,
GIT_WORK_TREE: Instance.worktree,
},
nothrow: true,
})
})
.quiet()
.nothrow()
// Configure git to not convert line endings on Windows
await Process.run(["git", "--git-dir", git, "config", "core.autocrlf", "false"], { nothrow: true })
await Process.run(["git", "--git-dir", git, "config", "core.longpaths", "true"], { nothrow: true })
await Process.run(["git", "--git-dir", git, "config", "core.symlinks", "true"], { nothrow: true })
await Process.run(["git", "--git-dir", git, "config", "core.fsmonitor", "false"], { nothrow: true })
await $`git --git-dir ${git} config core.autocrlf false`.quiet().nothrow()
await $`git --git-dir ${git} config core.longpaths true`.quiet().nothrow()
await $`git --git-dir ${git} config core.symlinks true`.quiet().nothrow()
await $`git --git-dir ${git} config core.fsmonitor false`.quiet().nothrow()
log.info("initialized")
}
await add(git)
const hash = await Process.text(["git", ...args(git, ["write-tree"])], {
cwd: Instance.directory,
nothrow: true,
}).then((x) => x.text)
const hash = await $`git --git-dir ${git} --work-tree ${Instance.worktree} write-tree`
.quiet()
.cwd(Instance.directory)
.nothrow()
.text()
log.info("tracking", { hash, cwd: Instance.directory, git })
return hash.trim()
}
@@ -93,32 +89,19 @@ export namespace Snapshot {
export async function patch(hash: string): Promise<Patch> {
const git = gitdir()
await add(git)
const result = await Process.text(
[
"git",
"-c",
"core.autocrlf=false",
"-c",
"core.longpaths=true",
"-c",
"core.symlinks=true",
"-c",
"core.quotepath=false",
...args(git, ["diff", "--no-ext-diff", "--name-only", hash, "--", "."]),
],
{
cwd: Instance.directory,
nothrow: true,
},
)
const result =
await $`git -c core.autocrlf=false -c core.longpaths=true -c core.symlinks=true -c core.quotepath=false --git-dir ${git} --work-tree ${Instance.worktree} diff --no-ext-diff --name-only ${hash} -- .`
.quiet()
.cwd(Instance.directory)
.nothrow()
// If git diff fails, return empty patch
if (result.code !== 0) {
log.warn("failed to get diff", { hash, exitCode: result.code })
if (result.exitCode !== 0) {
log.warn("failed to get diff", { hash, exitCode: result.exitCode })
return { hash, files: [] }
}
const files = result.text
const files = result.text()
return {
hash,
files: files
@@ -133,37 +116,20 @@ export namespace Snapshot {
export async function restore(snapshot: string) {
log.info("restore", { commit: snapshot })
const git = gitdir()
const result = await Process.run(
["git", "-c", "core.longpaths=true", "-c", "core.symlinks=true", ...args(git, ["read-tree", snapshot])],
{
cwd: Instance.worktree,
nothrow: true,
},
)
if (result.code === 0) {
const checkout = await Process.run(
["git", "-c", "core.longpaths=true", "-c", "core.symlinks=true", ...args(git, ["checkout-index", "-a", "-f"])],
{
cwd: Instance.worktree,
nothrow: true,
},
)
if (checkout.code === 0) return
const result =
await $`git -c core.longpaths=true -c core.symlinks=true --git-dir ${git} --work-tree ${Instance.worktree} read-tree ${snapshot} && git -c core.longpaths=true -c core.symlinks=true --git-dir ${git} --work-tree ${Instance.worktree} checkout-index -a -f`
.quiet()
.cwd(Instance.worktree)
.nothrow()
if (result.exitCode !== 0) {
log.error("failed to restore snapshot", {
snapshot,
exitCode: checkout.code,
stderr: checkout.stderr.toString(),
stdout: checkout.stdout.toString(),
exitCode: result.exitCode,
stderr: result.stderr.toString(),
stdout: result.stdout.toString(),
})
return
}
log.error("failed to restore snapshot", {
snapshot,
exitCode: result.code,
stderr: result.stderr.toString(),
stdout: result.stdout.toString(),
})
}
export async function revert(patches: Patch[]) {
@@ -173,37 +139,19 @@ export namespace Snapshot {
for (const file of item.files) {
if (files.has(file)) continue
log.info("reverting", { file, hash: item.hash })
const result = await Process.run(
[
"git",
"-c",
"core.longpaths=true",
"-c",
"core.symlinks=true",
...args(git, ["checkout", item.hash, "--", file]),
],
{
cwd: Instance.worktree,
nothrow: true,
},
)
if (result.code !== 0) {
const result =
await $`git -c core.longpaths=true -c core.symlinks=true --git-dir ${git} --work-tree ${Instance.worktree} checkout ${item.hash} -- ${file}`
.quiet()
.cwd(Instance.worktree)
.nothrow()
if (result.exitCode !== 0) {
const relativePath = path.relative(Instance.worktree, file)
const checkTree = await Process.text(
[
"git",
"-c",
"core.longpaths=true",
"-c",
"core.symlinks=true",
...args(git, ["ls-tree", item.hash, "--", relativePath]),
],
{
cwd: Instance.worktree,
nothrow: true,
},
)
if (checkTree.code === 0 && checkTree.text.trim()) {
const checkTree =
await $`git -c core.longpaths=true -c core.symlinks=true --git-dir ${git} --work-tree ${Instance.worktree} ls-tree ${item.hash} -- ${relativePath}`
.quiet()
.cwd(Instance.worktree)
.nothrow()
if (checkTree.exitCode === 0 && checkTree.text().trim()) {
log.info("file existed in snapshot but checkout failed, keeping", {
file,
})
@@ -220,36 +168,23 @@ export namespace Snapshot {
export async function diff(hash: string) {
const git = gitdir()
await add(git)
const result = await Process.text(
[
"git",
"-c",
"core.autocrlf=false",
"-c",
"core.longpaths=true",
"-c",
"core.symlinks=true",
"-c",
"core.quotepath=false",
...args(git, ["diff", "--no-ext-diff", hash, "--", "."]),
],
{
cwd: Instance.worktree,
nothrow: true,
},
)
const result =
await $`git -c core.autocrlf=false -c core.longpaths=true -c core.symlinks=true -c core.quotepath=false --git-dir ${git} --work-tree ${Instance.worktree} diff --no-ext-diff ${hash} -- .`
.quiet()
.cwd(Instance.worktree)
.nothrow()
if (result.code !== 0) {
if (result.exitCode !== 0) {
log.warn("failed to get diff", {
hash,
exitCode: result.code,
exitCode: result.exitCode,
stderr: result.stderr.toString(),
stdout: result.stdout.toString(),
})
return ""
}
return result.text.trim()
return result.text().trim()
}
export const FileDiff = z
@@ -270,24 +205,12 @@ export namespace Snapshot {
const result: FileDiff[] = []
const status = new Map<string, "added" | "deleted" | "modified">()
const statuses = await Process.text(
[
"git",
"-c",
"core.autocrlf=false",
"-c",
"core.longpaths=true",
"-c",
"core.symlinks=true",
"-c",
"core.quotepath=false",
...args(git, ["diff", "--no-ext-diff", "--name-status", "--no-renames", from, to, "--", "."]),
],
{
cwd: Instance.directory,
nothrow: true,
},
).then((x) => x.text)
const statuses =
await $`git -c core.autocrlf=false -c core.longpaths=true -c core.symlinks=true -c core.quotepath=false --git-dir ${git} --work-tree ${Instance.worktree} diff --no-ext-diff --name-status --no-renames ${from} ${to} -- .`
.quiet()
.cwd(Instance.directory)
.nothrow()
.text()
for (const line of statuses.trim().split("\n")) {
if (!line) continue
@@ -297,57 +220,26 @@ export namespace Snapshot {
status.set(file, kind)
}
for (const line of await Process.lines(
[
"git",
"-c",
"core.autocrlf=false",
"-c",
"core.longpaths=true",
"-c",
"core.symlinks=true",
"-c",
"core.quotepath=false",
...args(git, ["diff", "--no-ext-diff", "--no-renames", "--numstat", from, to, "--", "."]),
],
{
cwd: Instance.directory,
nothrow: true,
},
)) {
for await (const line of $`git -c core.autocrlf=false -c core.longpaths=true -c core.symlinks=true -c core.quotepath=false --git-dir ${git} --work-tree ${Instance.worktree} diff --no-ext-diff --no-renames --numstat ${from} ${to} -- .`
.quiet()
.cwd(Instance.directory)
.nothrow()
.lines()) {
if (!line) continue
const [additions, deletions, file] = line.split("\t")
const isBinaryFile = additions === "-" && deletions === "-"
const before = isBinaryFile
? ""
: await Process.text(
[
"git",
"-c",
"core.autocrlf=false",
"-c",
"core.longpaths=true",
"-c",
"core.symlinks=true",
...args(git, ["show", `${from}:${file}`]),
],
{ nothrow: true },
).then((x) => x.text)
: await $`git -c core.autocrlf=false -c core.longpaths=true -c core.symlinks=true --git-dir ${git} --work-tree ${Instance.worktree} show ${from}:${file}`
.quiet()
.nothrow()
.text()
const after = isBinaryFile
? ""
: await Process.text(
[
"git",
"-c",
"core.autocrlf=false",
"-c",
"core.longpaths=true",
"-c",
"core.symlinks=true",
...args(git, ["show", `${to}:${file}`]),
],
{ nothrow: true },
).then((x) => x.text)
: await $`git -c core.autocrlf=false -c core.longpaths=true -c core.symlinks=true --git-dir ${git} --work-tree ${Instance.worktree} show ${to}:${file}`
.quiet()
.nothrow()
.text()
const added = isBinaryFile ? 0 : parseInt(additions)
const deleted = isBinaryFile ? 0 : parseInt(deletions)
result.push({
@@ -369,22 +261,10 @@ export namespace Snapshot {
async function add(git: string) {
await syncExclude(git)
await Process.run(
[
"git",
"-c",
"core.autocrlf=false",
"-c",
"core.longpaths=true",
"-c",
"core.symlinks=true",
...args(git, ["add", "."]),
],
{
cwd: Instance.directory,
nothrow: true,
},
)
await $`git -c core.autocrlf=false -c core.longpaths=true -c core.symlinks=true --git-dir ${git} --work-tree ${Instance.worktree} add .`
.quiet()
.cwd(Instance.directory)
.nothrow()
}
async function syncExclude(git: string) {
@@ -401,10 +281,11 @@ export namespace Snapshot {
}
async function excludes() {
const file = await Process.text(["git", "rev-parse", "--path-format=absolute", "--git-path", "info/exclude"], {
cwd: Instance.worktree,
nothrow: true,
}).then((x) => x.text)
const file = await $`git rev-parse --path-format=absolute --git-path info/exclude`
.quiet()
.cwd(Instance.worktree)
.nothrow()
.text()
if (!file.trim()) return
const exists = await fs
.stat(file.trim())

View File

@@ -5,10 +5,10 @@ import { Global } from "../global"
import { Filesystem } from "../util/filesystem"
import { lazy } from "../util/lazy"
import { Lock } from "../util/lock"
import { $ } from "bun"
import { NamedError } from "@opencode-ai/util/error"
import z from "zod"
import { Glob } from "../util/glob"
import { git } from "@/util/git"
export namespace Storage {
const log = Log.create({ service: "storage" })
@@ -49,15 +49,18 @@ export namespace Storage {
}
if (!worktree) continue
if (!(await Filesystem.isDir(worktree))) continue
const result = await git(["rev-list", "--max-parents=0", "--all"], {
cwd: worktree,
})
const [id] = result
const [id] = await $`git rev-list --max-parents=0 --all`
.quiet()
.nothrow()
.cwd(worktree)
.text()
.split("\n")
.filter(Boolean)
.map((x) => x.trim())
.toSorted()
.then((x) =>
x
.split("\n")
.filter(Boolean)
.map((x) => x.trim())
.toSorted(),
)
if (!id) continue
projectID = id

View File

@@ -7,8 +7,8 @@ import { Log } from "../util/log"
import { Instance } from "../project/instance"
import { lazy } from "@/util/lazy"
import { Language } from "web-tree-sitter"
import fs from "fs/promises"
import { $ } from "bun"
import { Filesystem } from "@/util/filesystem"
import { fileURLToPath } from "url"
import { Flag } from "@/flag/flag.ts"
@@ -116,7 +116,12 @@ export const BashTool = Tool.define("bash", async () => {
if (["cd", "rm", "cp", "mv", "mkdir", "touch", "chmod", "chown", "cat"].includes(command[0])) {
for (const arg of command.slice(1)) {
if (arg.startsWith("-") || (command[0] === "chmod" && arg.startsWith("+"))) continue
const resolved = await fs.realpath(path.resolve(cwd, arg)).catch(() => "")
const resolved = await $`realpath ${arg}`
.cwd(cwd)
.quiet()
.nothrow()
.text()
.then((x) => x.trim())
log.info("resolved path", { arg, resolved })
if (resolved) {
const normalized =

View File

@@ -1,5 +1,5 @@
import { $ } from "bun"
import path from "path"
import { Process } from "./process"
export namespace Archive {
export async function extractZip(zipPath: string, destDir: string) {
@@ -8,10 +8,9 @@ export namespace Archive {
const winDestDir = path.resolve(destDir)
// $global:ProgressPreference suppresses PowerShell's blue progress bar popup
const cmd = `$global:ProgressPreference = 'SilentlyContinue'; Expand-Archive -Path '${winZipPath}' -DestinationPath '${winDestDir}' -Force`
await Process.run(["powershell", "-NoProfile", "-NonInteractive", "-Command", cmd])
return
await $`powershell -NoProfile -NonInteractive -Command ${cmd}`.quiet()
} else {
await $`unzip -o -q ${zipPath} -d ${destDir}`.quiet()
}
await Process.run(["unzip", "-o", "-q", zipPath, "-d", destDir])
}
}

View File

@@ -23,7 +23,7 @@ export namespace Keybind {
*/
export function fromParsedKey(key: ParsedKey, leader = false): Info {
return {
name: key.name === " " ? "space" : key.name,
name: key.name,
ctrl: key.ctrl,
meta: key.meta,
shift: key.shift,

View File

@@ -25,10 +25,6 @@ export namespace Process {
stderr: Buffer
}
export interface TextResult extends Result {
text: string
}
export class RunFailedError extends Error {
readonly cmd: string[]
readonly code: number
@@ -118,33 +114,13 @@ export namespace Process {
if (!proc.stdout || !proc.stderr) throw new Error("Process output not available")
const out = await Promise.all([proc.exited, buffer(proc.stdout), buffer(proc.stderr)])
.then(([code, stdout, stderr]) => ({
code,
stdout,
stderr,
}))
.catch((err: unknown) => {
if (!opts.nothrow) throw err
return {
code: 1,
stdout: Buffer.alloc(0),
stderr: Buffer.from(err instanceof Error ? err.message : String(err)),
}
})
const [code, stdout, stderr] = await Promise.all([proc.exited, buffer(proc.stdout), buffer(proc.stderr)])
const out = {
code,
stdout,
stderr,
}
if (out.code === 0 || opts.nothrow) return out
throw new RunFailedError(cmd, out.code, out.stdout, out.stderr)
}
export async function text(cmd: string[], opts: RunOptions = {}): Promise<TextResult> {
const out = await run(cmd, opts)
return {
...out,
text: out.stdout.toString(),
}
}
export async function lines(cmd: string[], opts: RunOptions = {}): Promise<string[]> {
return (await text(cmd, opts)).text.split(/\r?\n/).filter(Boolean)
}
}

View File

@@ -1,3 +1,4 @@
import { $ } from "bun"
import fs from "fs/promises"
import path from "path"
import z from "zod"
@@ -10,8 +11,6 @@ import { Database, eq } from "../storage/db"
import { ProjectTable } from "../project/project.sql"
import { fn } from "../util/fn"
import { Log } from "../util/log"
import { Process } from "../util/process"
import { git } from "../util/git"
import { BusEvent } from "@/bus/bus-event"
import { GlobalBus } from "@/bus/global"
@@ -249,14 +248,14 @@ export namespace Worktree {
}
async function sweep(root: string) {
const first = await git(["clean", "-ffdx"], { cwd: root })
const first = await $`git clean -ffdx`.quiet().nothrow().cwd(root)
if (first.exitCode === 0) return first
const entries = failed(first)
if (!entries.length) return first
await prune(root, entries)
return git(["clean", "-ffdx"], { cwd: root })
return $`git clean -ffdx`.quiet().nothrow().cwd(root)
}
async function canonical(input: string) {
@@ -275,9 +274,7 @@ export namespace Worktree {
if (await exists(directory)) continue
const ref = `refs/heads/${branch}`
const branchCheck = await git(["show-ref", "--verify", "--quiet", ref], {
cwd: Instance.worktree,
})
const branchCheck = await $`git show-ref --verify --quiet ${ref}`.quiet().nothrow().cwd(Instance.worktree)
if (branchCheck.exitCode === 0) continue
return Info.parse({ name, branch, directory })
@@ -288,9 +285,9 @@ export namespace Worktree {
async function runStartCommand(directory: string, cmd: string) {
if (process.platform === "win32") {
return Process.run(["cmd", "/c", cmd], { cwd: directory, nothrow: true })
return $`cmd /c ${cmd}`.nothrow().cwd(directory)
}
return Process.run(["bash", "-lc", cmd], { cwd: directory, nothrow: true })
return $`bash -lc ${cmd}`.nothrow().cwd(directory)
}
type StartKind = "project" | "worktree"
@@ -300,7 +297,7 @@ export namespace Worktree {
if (!text) return true
const ran = await runStartCommand(directory, text)
if (ran.code === 0) return true
if (ran.exitCode === 0) return true
log.error("worktree start command failed", {
kind,
@@ -347,9 +344,10 @@ export namespace Worktree {
}
export async function createFromInfo(info: Info, startCommand?: string) {
const created = await git(["worktree", "add", "--no-checkout", "-b", info.branch, info.directory], {
cwd: Instance.worktree,
})
const created = await $`git worktree add --no-checkout -b ${info.branch} ${info.directory}`
.quiet()
.nothrow()
.cwd(Instance.worktree)
if (created.exitCode !== 0) {
throw new CreateFailedError({ message: errorText(created) || "Failed to create git worktree" })
}
@@ -361,7 +359,7 @@ export namespace Worktree {
return () => {
const start = async () => {
const populated = await git(["reset", "--hard"], { cwd: info.directory })
const populated = await $`git reset --hard`.quiet().nothrow().cwd(info.directory)
if (populated.exitCode !== 0) {
const message = errorText(populated) || "Failed to populate worktree"
log.error("worktree checkout failed", { directory: info.directory, message })
@@ -413,7 +411,7 @@ export namespace Worktree {
await runStartScripts(info.directory, { projectID, extra })
}
return start().catch((error) => {
void start().catch((error) => {
log.error("worktree start task failed", { directory: info.directory, error })
})
}
@@ -478,10 +476,10 @@ export namespace Worktree {
const stop = async (target: string) => {
if (!(await exists(target))) return
await git(["fsmonitor--daemon", "stop"], { cwd: target })
await $`git fsmonitor--daemon stop`.quiet().nothrow().cwd(target)
}
const list = await git(["worktree", "list", "--porcelain"], { cwd: Instance.worktree })
const list = await $`git worktree list --porcelain`.quiet().nothrow().cwd(Instance.worktree)
if (list.exitCode !== 0) {
throw new RemoveFailedError({ message: errorText(list) || "Failed to read git worktrees" })
}
@@ -498,11 +496,9 @@ export namespace Worktree {
}
await stop(entry.path)
const removed = await git(["worktree", "remove", "--force", entry.path], {
cwd: Instance.worktree,
})
const removed = await $`git worktree remove --force ${entry.path}`.quiet().nothrow().cwd(Instance.worktree)
if (removed.exitCode !== 0) {
const next = await git(["worktree", "list", "--porcelain"], { cwd: Instance.worktree })
const next = await $`git worktree list --porcelain`.quiet().nothrow().cwd(Instance.worktree)
if (next.exitCode !== 0) {
throw new RemoveFailedError({
message: errorText(removed) || errorText(next) || "Failed to remove git worktree",
@@ -519,7 +515,7 @@ export namespace Worktree {
const branch = entry.branch?.replace(/^refs\/heads\//, "")
if (branch) {
const deleted = await git(["branch", "-D", branch], { cwd: Instance.worktree })
const deleted = await $`git branch -D ${branch}`.quiet().nothrow().cwd(Instance.worktree)
if (deleted.exitCode !== 0) {
throw new RemoveFailedError({ message: errorText(deleted) || "Failed to delete worktree branch" })
}
@@ -539,7 +535,7 @@ export namespace Worktree {
throw new ResetFailedError({ message: "Cannot reset the primary workspace" })
}
const list = await git(["worktree", "list", "--porcelain"], { cwd: Instance.worktree })
const list = await $`git worktree list --porcelain`.quiet().nothrow().cwd(Instance.worktree)
if (list.exitCode !== 0) {
throw new ResetFailedError({ message: errorText(list) || "Failed to read git worktrees" })
}
@@ -572,7 +568,7 @@ export namespace Worktree {
throw new ResetFailedError({ message: "Worktree not found" })
}
const remoteList = await git(["remote"], { cwd: Instance.worktree })
const remoteList = await $`git remote`.quiet().nothrow().cwd(Instance.worktree)
if (remoteList.exitCode !== 0) {
throw new ResetFailedError({ message: errorText(remoteList) || "Failed to list git remotes" })
}
@@ -591,19 +587,18 @@ export namespace Worktree {
: ""
const remoteHead = remote
? await git(["symbolic-ref", `refs/remotes/${remote}/HEAD`], { cwd: Instance.worktree })
? await $`git symbolic-ref refs/remotes/${remote}/HEAD`.quiet().nothrow().cwd(Instance.worktree)
: { exitCode: 1, stdout: undefined, stderr: undefined }
const remoteRef = remoteHead.exitCode === 0 ? outputText(remoteHead.stdout) : ""
const remoteTarget = remoteRef ? remoteRef.replace(/^refs\/remotes\//, "") : ""
const remoteBranch = remote && remoteTarget.startsWith(`${remote}/`) ? remoteTarget.slice(`${remote}/`.length) : ""
const mainCheck = await git(["show-ref", "--verify", "--quiet", "refs/heads/main"], {
cwd: Instance.worktree,
})
const masterCheck = await git(["show-ref", "--verify", "--quiet", "refs/heads/master"], {
cwd: Instance.worktree,
})
const mainCheck = await $`git show-ref --verify --quiet refs/heads/main`.quiet().nothrow().cwd(Instance.worktree)
const masterCheck = await $`git show-ref --verify --quiet refs/heads/master`
.quiet()
.nothrow()
.cwd(Instance.worktree)
const localBranch = mainCheck.exitCode === 0 ? "main" : masterCheck.exitCode === 0 ? "master" : ""
const target = remoteBranch ? `${remote}/${remoteBranch}` : localBranch
@@ -612,7 +607,7 @@ export namespace Worktree {
}
if (remoteBranch) {
const fetch = await git(["fetch", remote, remoteBranch], { cwd: Instance.worktree })
const fetch = await $`git fetch ${remote} ${remoteBranch}`.quiet().nothrow().cwd(Instance.worktree)
if (fetch.exitCode !== 0) {
throw new ResetFailedError({ message: errorText(fetch) || `Failed to fetch ${target}` })
}
@@ -624,7 +619,7 @@ export namespace Worktree {
const worktreePath = entry.path
const resetToTarget = await git(["reset", "--hard", target], { cwd: worktreePath })
const resetToTarget = await $`git reset --hard ${target}`.quiet().nothrow().cwd(worktreePath)
if (resetToTarget.exitCode !== 0) {
throw new ResetFailedError({ message: errorText(resetToTarget) || "Failed to reset worktree to target" })
}
@@ -634,26 +629,22 @@ export namespace Worktree {
throw new ResetFailedError({ message: errorText(clean) || "Failed to clean worktree" })
}
const update = await git(["submodule", "update", "--init", "--recursive", "--force"], { cwd: worktreePath })
const update = await $`git submodule update --init --recursive --force`.quiet().nothrow().cwd(worktreePath)
if (update.exitCode !== 0) {
throw new ResetFailedError({ message: errorText(update) || "Failed to update submodules" })
}
const subReset = await git(["submodule", "foreach", "--recursive", "git", "reset", "--hard"], {
cwd: worktreePath,
})
const subReset = await $`git submodule foreach --recursive git reset --hard`.quiet().nothrow().cwd(worktreePath)
if (subReset.exitCode !== 0) {
throw new ResetFailedError({ message: errorText(subReset) || "Failed to reset submodules" })
}
const subClean = await git(["submodule", "foreach", "--recursive", "git", "clean", "-fdx"], {
cwd: worktreePath,
})
const subClean = await $`git submodule foreach --recursive git clean -fdx`.quiet().nothrow().cwd(worktreePath)
if (subClean.exitCode !== 0) {
throw new ResetFailedError({ message: errorText(subClean) || "Failed to clean submodules" })
}
const status = await git(["-c", "core.fsmonitor=false", "status", "--porcelain=v1"], { cwd: worktreePath })
const status = await $`git -c core.fsmonitor=false status --porcelain=v1`.quiet().nothrow().cwd(worktreePath)
if (status.exitCode !== 0) {
throw new ResetFailedError({ message: errorText(status) || "Failed to read git status" })
}

View File

@@ -1,157 +0,0 @@
import { describe, expect, mock, test } from "bun:test"
import fs from "fs/promises"
import path from "path"
import { tmpdir } from "../../fixture/fixture"
const stop = new Error("stop")
const seen = {
tui: [] as string[],
inst: [] as string[],
}
mock.module("../../../src/cli/cmd/tui/app", () => ({
tui: async (input: { directory: string }) => {
seen.tui.push(input.directory)
throw stop
},
}))
mock.module("@/util/rpc", () => ({
Rpc: {
client: () => ({
call: async () => ({ url: "http://127.0.0.1" }),
on: () => {},
}),
},
}))
mock.module("@/cli/ui", () => ({
UI: {
error: () => {},
},
}))
mock.module("@/util/log", () => ({
Log: {
init: async () => {},
create: () => ({
error: () => {},
info: () => {},
warn: () => {},
debug: () => {},
time: () => ({ stop: () => {} }),
}),
Default: {
error: () => {},
info: () => {},
warn: () => {},
debug: () => {},
},
},
}))
mock.module("@/util/timeout", () => ({
withTimeout: <T>(input: Promise<T>) => input,
}))
mock.module("@/cli/network", () => ({
withNetworkOptions: <T>(input: T) => input,
resolveNetworkOptions: async () => ({
mdns: false,
port: 0,
hostname: "127.0.0.1",
}),
}))
mock.module("../../../src/cli/cmd/tui/win32", () => ({
win32DisableProcessedInput: () => {},
win32InstallCtrlCGuard: () => undefined,
}))
mock.module("@/config/tui", () => ({
TuiConfig: {
get: () => ({}),
},
}))
mock.module("@/project/instance", () => ({
Instance: {
provide: async (input: { directory: string; fn: () => Promise<unknown> | unknown }) => {
seen.inst.push(input.directory)
return input.fn()
},
},
}))
describe("tui thread", () => {
async function call(project?: string) {
const { TuiThreadCommand } = await import("../../../src/cli/cmd/tui/thread")
const args: Parameters<NonNullable<typeof TuiThreadCommand.handler>>[0] = {
_: [],
$0: "opencode",
project,
prompt: "hi",
model: undefined,
agent: undefined,
session: undefined,
continue: false,
fork: false,
port: 0,
hostname: "127.0.0.1",
mdns: false,
"mdns-domain": "opencode.local",
mdnsDomain: "opencode.local",
cors: [],
}
return TuiThreadCommand.handler(args)
}
async function check(project?: string) {
await using tmp = await tmpdir({ git: true })
const cwd = process.cwd()
const pwd = process.env.PWD
const worker = globalThis.Worker
const tty = Object.getOwnPropertyDescriptor(process.stdin, "isTTY")
const link = path.join(path.dirname(tmp.path), path.basename(tmp.path) + "-link")
const type = process.platform === "win32" ? "junction" : "dir"
seen.tui.length = 0
seen.inst.length = 0
await fs.symlink(tmp.path, link, type)
Object.defineProperty(process.stdin, "isTTY", {
configurable: true,
value: true,
})
globalThis.Worker = class extends EventTarget {
onerror = null
onmessage = null
onmessageerror = null
postMessage() {}
terminate() {}
} as unknown as typeof Worker
try {
process.chdir(tmp.path)
process.env.PWD = link
await expect(call(project)).rejects.toBe(stop)
expect(seen.inst[0]).toBe(tmp.path)
expect(seen.tui[0]).toBe(tmp.path)
} finally {
process.chdir(cwd)
if (pwd === undefined) delete process.env.PWD
else process.env.PWD = pwd
if (tty) Object.defineProperty(process.stdin, "isTTY", tty)
else delete (process.stdin as { isTTY?: boolean }).isTTY
globalThis.Worker = worker
await fs.rm(link, { recursive: true, force: true }).catch(() => undefined)
}
}
test("uses the real cwd when PWD points at a symlink", async () => {
await check()
})
test("uses the real cwd after resolving a relative project from PWD", async () => {
await check(".")
})
})

View File

@@ -10,22 +10,12 @@ import { Database } from "../../src/storage/db"
import { resetDatabase } from "../fixture/db"
import * as adaptors from "../../src/control-plane/adaptors"
import type { Adaptor } from "../../src/control-plane/types"
import { Flag } from "../../src/flag/flag"
afterEach(async () => {
mock.restore()
await resetDatabase()
})
const original = Flag.OPENCODE_EXPERIMENTAL_WORKSPACES
// @ts-expect-error don't do this normally, but it works
Flag.OPENCODE_EXPERIMENTAL_WORKSPACES = true
afterEach(() => {
// @ts-expect-error don't do this normally, but it works
Flag.OPENCODE_EXPERIMENTAL_WORKSPACES = original
})
type State = {
workspace?: "first" | "second"
calls: Array<{ method: string; url: string; body?: string }>

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